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

@ -2,10 +2,8 @@ import os from "node:os";
import { parseArgs } from "node:util"; import { parseArgs } from "node:util";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { type ModelMessage, streamText } from "ai"; import { type ModelMessage, streamText } from "ai";
import { renderStream } from "./src/render";
const home = os.homedir(); import StreamRenderer from "./src/render";
const convoFile = Bun.file(`${home}/.aicl_convo.json`);
const { values: args, positionals } = parseArgs({ const { values: args, positionals } = parseArgs({
args: Bun.argv, args: Bun.argv,
@ -21,6 +19,9 @@ const { values: args, positionals } = parseArgs({
allowPositionals: true, allowPositionals: true,
}); });
const home = os.homedir();
const convoFile = Bun.file(`${home}/.aicl_convo.json`);
async function main() { async function main() {
const provider = createOpenAICompatible({ const provider = createOpenAICompatible({
name: "llama.cpp", name: "llama.cpp",
@ -48,7 +49,7 @@ async function main() {
const start = Date.now(); const start = Date.now();
const streamRender = renderStream(fullStream); const streamRender = new StreamRenderer().renderStream(fullStream);
const [width] = process.stdout.getWindowSize(); const [width] = process.stdout.getWindowSize();
const glow = Bun.spawn({ const glow = Bun.spawn({

View file

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