refactor: convert render module to StreamRenderer class

This commit is contained in:
mitchell 2026-04-16 16:15:38 -04:00
parent bcd9771cd5
commit 2ec00018bd
2 changed files with 71 additions and 69 deletions

View file

@ -4,80 +4,81 @@ import type { AsyncIterableStream, TextStreamPart, ToolSet } from "ai";
const home = os.homedir();
const reasoningWriter = Bun.file(`${home}/.aicl_reasoning.md`).writer();
let lineCount = 0;
let charsSinceNL = 0;
export default class StreamRenderer {
private lineCount = 0;
private charsSinceNL = 0;
private memOk = true;
function resetCounts() {
lineCount = 0;
charsSinceNL = 0;
}
constructor() {
process.stdout.addListener("drain", () => {
this.memOk = true;
});
}
let memOk = true;
private write(text: string) {
this.memOk = process.stdout.write(text);
}
process.stdout.addListener("drain", () => {
memOk = true;
});
private moveCursor(x: number, y: number) {
this.memOk = process.stdout.moveCursor(x, y);
}
function write(text: string) {
memOk = process.stdout.write(text);
}
private clearScreenDown() {
this.memOk = process.stdout.clearScreenDown();
}
function moveCursor(x: number, y: number) {
memOk = process.stdout.moveCursor(x, y);
}
private resetCounts() {
this.lineCount = 0;
this.charsSinceNL = 0;
}
function clearScreenDown() {
memOk = process.stdout.clearScreenDown();
}
export async function renderStream(
stream: AsyncIterableStream<TextStreamPart<ToolSet>>,
) {
resetCounts();
for await (const part of stream) {
if (part.type === "reasoning-start") {
write("\u001b[2m\u001b[3m\n");
} else if (part.type === "reasoning-delta") {
write(part.text);
reasoningWriter.write(part.text);
checkContainment(part.text);
} else if (part.type === "reasoning-end") {
write("\n\u001b[22m\u001b[23m");
moveCursor(0, -lineCount - 2);
clearScreenDown();
resetCounts();
reasoningWriter.end();
} else if (part.type === "text-start") {
write("\n");
} else if (part.type === "text-delta") {
write(part.text);
checkContainment(part.text);
} else if (part.type === "text-end") {
moveCursor(0, -lineCount - 1);
clearScreenDown();
resetCounts();
private checkContainment(text: string) {
const [columns, rows] = process.stdout.getWindowSize();
this.charsSinceNL += text.length;
if (text.includes("\n")) {
this.lineCount += text.match(/\n/g)?.length ?? 1;
this.charsSinceNL = text.length - text.lastIndexOf("\n") - 1;
} else if (this.charsSinceNL >= columns) {
this.lineCount++;
this.charsSinceNL = this.charsSinceNL - columns;
}
while (!memOk) await Bun.sleep(10);
}
}
function checkContainment(text: string) {
const [columns, rows] = process.stdout.getWindowSize();
charsSinceNL += text.length;
if (text.includes("\n")) {
lineCount += text.match(/\n/g)?.length ?? 1;
charsSinceNL = text.length - text.lastIndexOf("\n") - 1;
} else if (charsSinceNL >= columns) {
lineCount++;
charsSinceNL = charsSinceNL - columns;
if (this.lineCount >= rows / 2) {
this.moveCursor(0, -this.lineCount);
this.clearScreenDown();
this.lineCount = 0;
this.charsSinceNL = 0;
}
}
if (lineCount >= rows / 2) {
moveCursor(0, -lineCount);
clearScreenDown();
lineCount = 0;
charsSinceNL = 0;
async renderStream(stream: AsyncIterableStream<TextStreamPart<ToolSet>>) {
this.resetCounts();
for await (const part of stream) {
if (part.type === "reasoning-start") {
this.write("\u001b[2m\u001b[3m\n");
} else if (part.type === "reasoning-delta") {
this.write(part.text);
reasoningWriter.write(part.text);
this.checkContainment(part.text);
} else if (part.type === "reasoning-end") {
this.write("\n\u001b[22m\u001b[23m");
this.moveCursor(0, -this.lineCount - 2);
this.clearScreenDown();
this.resetCounts();
reasoningWriter.end();
} else if (part.type === "text-start") {
this.write("\n");
} else if (part.type === "text-delta") {
this.write(part.text);
this.checkContainment(part.text);
} else if (part.type === "text-end") {
this.moveCursor(0, -this.lineCount - 1);
this.clearScreenDown();
this.resetCounts();
}
while (!this.memOk) await Bun.sleep(10);
}
}
}