import { inspect } from "node:util"; import type { CommandUsage } from "./openai.js"; const tty = process.stdout.isTTY; const color = (code: number, text: string) => tty ? `\u001b[${code}m${text}\u001b[0m` : text; const cyan = (text: string) => color(36, text); const green = (text: string) => color(32, text); const yellow = (text: string) => color(33, text); const dim = (text: string) => color(2, text); const tokenNumber = new Intl.NumberFormat("zh-CN"); const cumulativeUsage: Required = { inputTokens: 0, outputTokens: 0 }; let cumulativeUsagePrinted = false; function tokenCount(value: unknown): number | undefined { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined; } function readUsage(value: unknown): Required | undefined { if (typeof value !== "object" || value === null) return undefined; const usage = value as Record; const inputTokens = tokenCount(usage.inputTokens); const outputTokens = tokenCount(usage.outputTokens); if (inputTokens === undefined && outputTokens === undefined) return undefined; return { inputTokens: inputTokens ?? 0, outputTokens: outputTokens ?? 0, }; } function formatUsage(usage: Required): string { const totalTokens = usage.inputTokens + usage.outputTokens; return `输入 ${tokenNumber.format(usage.inputTokens)} · 输出 ${tokenNumber.format(usage.outputTokens)} · 合计 ${tokenNumber.format(totalTokens)}`; } export function printCumulativeUsage(): void { if (cumulativeUsagePrinted) return; cumulativeUsagePrinted = true; process.stdout.write(`\n${cyan("━━━━━━━━ Token 累计 ━━━━━━━━")}\n`); process.stdout.write(`${formatUsage(cumulativeUsage)}\n`); process.stdout.write(`${cyan("━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`); } function details(event: Record): string { const copy = { ...event }; delete copy.type; return inspect(copy, { colors: tty, depth: 8, compact: false, breakLength: 120 }); } export class TerminalRenderer { private answerStarted = false; private thinkingShown = false; begin(model: string, effort: string, proxy?: string): void { this.answerStarted = false; this.thinkingShown = false; process.stdout.write(`\n${cyan("━━━━━━━━ Command Code 请求 ━━━━━━━━")}\n`); process.stdout.write(`${dim("模型")} ${model}\n`); process.stdout.write(`${dim("思考深度")} ${effort}\n`); if (proxy) process.stdout.write(`${dim("代理")} ${proxy}\n`); } event(event: Record): void { const type = typeof event.type === "string" ? event.type : "unknown"; switch (type) { case "run_start": process.stdout.write(`${dim("会话")} ${String(event.sessionId ?? "-")}\n`); return; case "turn_start": process.stdout.write(`${cyan(`\n[Turn ${String(event.turnNumber ?? "?")}]`)}\n`); return; case "model_request_start": process.stdout.write(`${dim("模型请求开始")} ${String(event.model ?? "")}\n`); return; case "thinking_start": if (!this.thinkingShown) { process.stdout.write(`${dim("思考中…")}\n`); this.thinkingShown = true; } return; case "thinking_delta": case "thinking_end": case "message_update": case "model_trace": case "run_end": return; case "text_delta": { if (!this.answerStarted) { process.stdout.write(`${green("\n最终回答:")}\n`); this.answerStarted = true; } process.stdout.write(String(event.delta ?? "")); return; } case "model_request_end": process.stdout.write(`\n${dim(`模型请求结束 · ${String(event.stopReason ?? "unknown")}`)}\n`); return; case "turn_end": { const usage = readUsage(event.usage); if (usage) { cumulativeUsage.inputTokens += usage.inputTokens; cumulativeUsage.outputTokens += usage.outputTokens; } process.stdout.write(`${dim(`Turn ${String(event.turnNumber ?? "?")} 结束`)}\n`); process.stdout.write(`${dim("Token 本轮")} · ${usage ? formatUsage(usage) : "不可用"}\n`); process.stdout.write(`${dim("Token 累计")} · ${formatUsage(cumulativeUsage)}\n`); return; } default: if (type.includes("tool") || type.includes("permission") || type.includes("question")) { process.stdout.write(`${yellow(`\n[${type}]`)}\n${details(event)}\n`); } } } stderr(chunk: string): void { process.stderr.write(chunk); } finish(durationMs: number): void { if (this.answerStarted) process.stdout.write("\n"); process.stdout.write(`${dim(`完成 · ${(durationMs / 1000).toFixed(2)}s`)}\n`); process.stdout.write(`${cyan("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n\n`); } fail(message: string): void { process.stderr.write(`${color(31, `\nCommand Code 失败:${message}`)}\n`); } }