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.
This commit is contained in:
mitchell 2026-04-17 16:16:09 -04:00
parent e35e32f396
commit 9d599d98bf
2 changed files with 43 additions and 27 deletions

View file

@ -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));
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();

View file

@ -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": {
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));
}
}