From 9d599d98bfbaf85b72fa394e905eaccf5e878b1c Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 17 Apr 2026 16:16:09 -0400 Subject: [PATCH] feat: add --plain flag for unformatted terminal output Rename StreamRenderer to Renderer and move stats rendering into the class. Support plain mode that skips ANSI formatting and glow preview when output is not a TTY. --- index.ts | 34 ++++++++++++++++------------------ src/render/index.ts | 36 +++++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/index.ts b/index.ts index 6a9fecd..c7fb1fe 100644 --- a/index.ts +++ b/index.ts @@ -2,9 +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 chalk from "chalk"; -import StreamRenderer from "./src/render"; +import Renderer from "./src/render"; const { values: args, positionals } = parseArgs({ args: Bun.argv, @@ -15,6 +14,8 @@ const { values: args, positionals } = parseArgs({ n: { type: "boolean" }, save: { type: "boolean" }, s: { type: "boolean" }, + plain: { type: "boolean" }, + p: { type: "boolean" }, }, strict: true, allowPositionals: true, @@ -26,6 +27,7 @@ const configFile = Bun.file(`${home}/.aicl_config.json`); async function main() { const newConvo = args.new ?? args.n; + const plain = args.plain ?? args.p ?? !process.stdout.isTTY; let saveConfig = args.save ?? args.s; let config: { model?: string }; @@ -47,6 +49,8 @@ async function main() { await configFile.write(JSON.stringify(config, null, " ")); } + const renderer = new Renderer(plain); + const provider = createOpenAICompatible({ name: "llama.cpp", apiKey: "local", @@ -78,28 +82,22 @@ async function main() { }); const start = Date.now(); - await new StreamRenderer().renderStream(fullStream); + await renderer.renderStream(fullStream); const stop = Date.now(); convo.messages.push({ role: "assistant", content: await text }); await convoFile.write(JSON.stringify(convo)); - const [width] = process.stdout.getWindowSize(); - await Bun.spawn({ - cmd: ["glow", "-w", `${width}`, "-"], - stdin: textStream, - stdout: "inherit", - }).exited; + if (!plain) { + const [width] = process.stdout.getWindowSize(); + await Bun.spawn({ + cmd: ["glow", "-w", `${width}`, "-"], + stdin: textStream, + stdout: "inherit", + }).exited; + } - const { inputTokens, outputTokens } = await totalUsage; - const context = (inputTokens ?? 0) / 1000; - const output = (outputTokens ?? 0) / 1000; - const time = (stop - start) / 1000; - process.stderr.write( - chalk.dim( - `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Time: ${time.toFixed(2)}s\n`, - ), - ); + renderer.renderStats(await totalUsage, start, stop); } await main(); diff --git a/src/render/index.ts b/src/render/index.ts index 58b964e..b474955 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -1,19 +1,24 @@ import os from "node:os"; import type { WriteStream } from "node:tty"; -import type { AsyncIterableStream, TextStreamPart, ToolSet } from "ai"; +import type { + AsyncIterableStream, + LanguageModelUsage, + TextStreamPart, + ToolSet, +} from "ai"; import styles from "ansi-styles"; -import { supportsColorStderr } from "chalk"; +import { chalkStderr, supportsColorStderr } from "chalk"; const home = os.homedir(); const reasoningFile = Bun.file(`${home}/.aicl_reasoning.md`); const reasoningWriter = reasoningFile.writer(); -export default class StreamRenderer { +export default class Renderer { private lineCount = 0; private charsSinceNL = 0; private memOk = true; - constructor() { + constructor(private isPlain = false) { process.stdout.addListener("drain", () => { this.memOk = true; }); @@ -50,7 +55,7 @@ export default class StreamRenderer { this.charsSinceNL = this.charsSinceNL - columns; } - if (this.lineCount >= Math.floor(rows * 0.75)) { + if (this.lineCount >= rows - 4) { this.moveCursor(-this.lineCount, pipe); this.clearScreenDown(pipe); this.resetCounts(); @@ -96,13 +101,17 @@ export default class StreamRenderer { } case "text-delta": { this.write(part.text); - this.checkContainment(part.text); + if (!this.isPlain) this.checkContainment(part.text); break; } case "text-end": { - this.moveCursor(-this.lineCount - 1); - this.clearScreenDown(process.stderr); - this.resetCounts(); + if (!this.isPlain) { + this.moveCursor(-this.lineCount - 1); + this.clearScreenDown(process.stderr); + this.resetCounts(); + } else { + this.write("\n\n"); + } break; } } @@ -110,4 +119,13 @@ export default class StreamRenderer { while (!this.memOk) await Bun.sleep(10); } } + + renderStats(usage: LanguageModelUsage, start: number, stop: number) { + const { inputTokens, outputTokens } = usage; + const context = (inputTokens ?? 0) / 1000; + const output = (outputTokens ?? 0) / 1000; + const time = (stop - start) / 1000; + const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Time: ${time.toFixed(2)}s\n`; + process.stderr.write(chalkStderr.dim(template)); + } }