From 2ec00018bd1e8ab4a046129af6c037545dcb067a Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 16 Apr 2026 16:15:38 -0400 Subject: [PATCH] refactor: convert render module to StreamRenderer class --- index.ts | 9 +-- src/render/index.ts | 131 ++++++++++++++++++++++---------------------- 2 files changed, 71 insertions(+), 69 deletions(-) diff --git a/index.ts b/index.ts index 0cd87ed..8f2d945 100644 --- a/index.ts +++ b/index.ts @@ -2,10 +2,8 @@ import os from "node:os"; import { parseArgs } from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { type ModelMessage, streamText } from "ai"; -import { renderStream } from "./src/render"; -const home = os.homedir(); -const convoFile = Bun.file(`${home}/.aicl_convo.json`); +import StreamRenderer from "./src/render"; const { values: args, positionals } = parseArgs({ args: Bun.argv, @@ -21,6 +19,9 @@ const { values: args, positionals } = parseArgs({ allowPositionals: true, }); +const home = os.homedir(); +const convoFile = Bun.file(`${home}/.aicl_convo.json`); + async function main() { const provider = createOpenAICompatible({ name: "llama.cpp", @@ -48,7 +49,7 @@ async function main() { const start = Date.now(); - const streamRender = renderStream(fullStream); + const streamRender = new StreamRenderer().renderStream(fullStream); const [width] = process.stdout.getWindowSize(); const glow = Bun.spawn({ diff --git a/src/render/index.ts b/src/render/index.ts index 25d4a9d..5a6e854 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -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>, -) { - 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>) { + 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); + } } }