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:
parent
e35e32f396
commit
9d599d98bf
2 changed files with 43 additions and 27 deletions
34
index.ts
34
index.ts
|
|
@ -2,9 +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 chalk from "chalk";
|
|
||||||
|
|
||||||
import StreamRenderer from "./src/render";
|
import Renderer from "./src/render";
|
||||||
|
|
||||||
const { values: args, positionals } = parseArgs({
|
const { values: args, positionals } = parseArgs({
|
||||||
args: Bun.argv,
|
args: Bun.argv,
|
||||||
|
|
@ -15,6 +14,8 @@ const { values: args, positionals } = parseArgs({
|
||||||
n: { type: "boolean" },
|
n: { type: "boolean" },
|
||||||
save: { type: "boolean" },
|
save: { type: "boolean" },
|
||||||
s: { type: "boolean" },
|
s: { type: "boolean" },
|
||||||
|
plain: { type: "boolean" },
|
||||||
|
p: { type: "boolean" },
|
||||||
},
|
},
|
||||||
strict: true,
|
strict: true,
|
||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
|
|
@ -26,6 +27,7 @@ const configFile = Bun.file(`${home}/.aicl_config.json`);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const newConvo = args.new ?? args.n;
|
const newConvo = args.new ?? args.n;
|
||||||
|
const plain = args.plain ?? args.p ?? !process.stdout.isTTY;
|
||||||
let saveConfig = args.save ?? args.s;
|
let saveConfig = args.save ?? args.s;
|
||||||
|
|
||||||
let config: { model?: string };
|
let config: { model?: string };
|
||||||
|
|
@ -47,6 +49,8 @@ async function main() {
|
||||||
await configFile.write(JSON.stringify(config, null, " "));
|
await configFile.write(JSON.stringify(config, null, " "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderer = new Renderer(plain);
|
||||||
|
|
||||||
const provider = createOpenAICompatible({
|
const provider = createOpenAICompatible({
|
||||||
name: "llama.cpp",
|
name: "llama.cpp",
|
||||||
apiKey: "local",
|
apiKey: "local",
|
||||||
|
|
@ -78,28 +82,22 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
await new StreamRenderer().renderStream(fullStream);
|
await renderer.renderStream(fullStream);
|
||||||
const stop = Date.now();
|
const stop = Date.now();
|
||||||
|
|
||||||
convo.messages.push({ role: "assistant", content: await text });
|
convo.messages.push({ role: "assistant", content: await text });
|
||||||
await convoFile.write(JSON.stringify(convo));
|
await convoFile.write(JSON.stringify(convo));
|
||||||
|
|
||||||
const [width] = process.stdout.getWindowSize();
|
if (!plain) {
|
||||||
await Bun.spawn({
|
const [width] = process.stdout.getWindowSize();
|
||||||
cmd: ["glow", "-w", `${width}`, "-"],
|
await Bun.spawn({
|
||||||
stdin: textStream,
|
cmd: ["glow", "-w", `${width}`, "-"],
|
||||||
stdout: "inherit",
|
stdin: textStream,
|
||||||
}).exited;
|
stdout: "inherit",
|
||||||
|
}).exited;
|
||||||
|
}
|
||||||
|
|
||||||
const { inputTokens, outputTokens } = await totalUsage;
|
renderer.renderStats(await totalUsage, start, stop);
|
||||||
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`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import type { WriteStream } from "node:tty";
|
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 styles from "ansi-styles";
|
||||||
import { supportsColorStderr } from "chalk";
|
import { chalkStderr, supportsColorStderr } from "chalk";
|
||||||
|
|
||||||
const home = os.homedir();
|
const home = os.homedir();
|
||||||
const reasoningFile = Bun.file(`${home}/.aicl_reasoning.md`);
|
const reasoningFile = Bun.file(`${home}/.aicl_reasoning.md`);
|
||||||
const reasoningWriter = reasoningFile.writer();
|
const reasoningWriter = reasoningFile.writer();
|
||||||
|
|
||||||
export default class StreamRenderer {
|
export default class Renderer {
|
||||||
private lineCount = 0;
|
private lineCount = 0;
|
||||||
private charsSinceNL = 0;
|
private charsSinceNL = 0;
|
||||||
private memOk = true;
|
private memOk = true;
|
||||||
|
|
||||||
constructor() {
|
constructor(private isPlain = false) {
|
||||||
process.stdout.addListener("drain", () => {
|
process.stdout.addListener("drain", () => {
|
||||||
this.memOk = true;
|
this.memOk = true;
|
||||||
});
|
});
|
||||||
|
|
@ -50,7 +55,7 @@ export default class StreamRenderer {
|
||||||
this.charsSinceNL = this.charsSinceNL - columns;
|
this.charsSinceNL = this.charsSinceNL - columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lineCount >= Math.floor(rows * 0.75)) {
|
if (this.lineCount >= rows - 4) {
|
||||||
this.moveCursor(-this.lineCount, pipe);
|
this.moveCursor(-this.lineCount, pipe);
|
||||||
this.clearScreenDown(pipe);
|
this.clearScreenDown(pipe);
|
||||||
this.resetCounts();
|
this.resetCounts();
|
||||||
|
|
@ -96,13 +101,17 @@ export default class StreamRenderer {
|
||||||
}
|
}
|
||||||
case "text-delta": {
|
case "text-delta": {
|
||||||
this.write(part.text);
|
this.write(part.text);
|
||||||
this.checkContainment(part.text);
|
if (!this.isPlain) this.checkContainment(part.text);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text-end": {
|
case "text-end": {
|
||||||
this.moveCursor(-this.lineCount - 1);
|
if (!this.isPlain) {
|
||||||
this.clearScreenDown(process.stderr);
|
this.moveCursor(-this.lineCount - 1);
|
||||||
this.resetCounts();
|
this.clearScreenDown(process.stderr);
|
||||||
|
this.resetCounts();
|
||||||
|
} else {
|
||||||
|
this.write("\n\n");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,4 +119,13 @@ export default class StreamRenderer {
|
||||||
while (!this.memOk) await Bun.sleep(10);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue