import { homedir } from "node:os"; import type { TranscriptMessage } from "../backends/types.ts"; export const COLLAPSED_ITEM_COUNT = 10; export function formatTokens(count: number): string { if (count < 1000) return count.toString(); if (count < 10000) return `${(count / 1000).toFixed(1)}k`; if (count < 1000000) return `${Math.round(count / 1000)}k`; return `${(count / 1000000).toFixed(1)}M`; } export function formatUsageStats( usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; contextTokens?: number; turns?: number; }, model?: string, ): string { const parts: string[] = []; if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`); if (usage.input) parts.push(`↑${formatTokens(usage.input)}`); if (usage.output) parts.push(`↓${formatTokens(usage.output)}`); if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`); if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`); if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`); if (usage.contextTokens && usage.contextTokens > 0) { parts.push(`ctx:${formatTokens(usage.contextTokens)}`); } if (model) parts.push(model); return parts.join(" "); } export interface Theme { fg(color: string, text: string): string; } export function formatToolCall( toolName: string, args: Record, theme: Theme, ): string { const shortenPath = (p: string) => { const home = homedir(); return p.startsWith(home) ? `~${p.slice(home.length)}` : p; }; switch (toolName) { case "bash": { const command = (args.command as string) || "..."; const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command; return theme.fg("muted", "$ ") + theme.fg("toolOutput", preview); } case "read": { const rawPath = (args.file_path || args.path || "...") as string; const filePath = shortenPath(rawPath); const offset = args.offset as number | undefined; const limit = args.limit as number | undefined; let text = theme.fg("accent", filePath); if (offset !== undefined || limit !== undefined) { const startLine = offset ?? 1; const endLine = limit !== undefined ? startLine + limit - 1 : ""; text += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`); } return theme.fg("muted", "read ") + text; } case "write": { const rawPath = (args.file_path || args.path || "...") as string; const filePath = shortenPath(rawPath); const content = (args.content || "") as string; const lines = content.split("\n").length; let text = theme.fg("muted", "write ") + theme.fg("accent", filePath); if (lines > 1) text += theme.fg("dim", ` (${lines} lines)`); return text; } case "edit": { const rawPath = (args.file_path || args.path || "...") as string; return theme.fg("muted", "edit ") + theme.fg("accent", shortenPath(rawPath)); } case "ls": { const rawPath = (args.path || ".") as string; return theme.fg("muted", "ls ") + theme.fg("accent", shortenPath(rawPath)); } case "find": case "glob": { const pattern = (args.pattern || "*") as string; const rawPath = (args.path || ".") as string; return ( theme.fg("muted", "find ") + theme.fg("accent", pattern) + theme.fg("dim", ` in ${shortenPath(rawPath)}`) ); } case "grep": { const pattern = (args.pattern || "") as string; const rawPath = (args.path || ".") as string; return ( theme.fg("muted", "grep ") + theme.fg("accent", `/${pattern}/`) + theme.fg("dim", ` in ${shortenPath(rawPath)}`) ); } default: { const argsStr = JSON.stringify(args); const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr; return theme.fg("accent", toolName) + theme.fg("dim", ` ${preview}`); } } } export type DisplayItem = | { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record }; export function extractDisplayItems(transcript: TranscriptMessage[]): DisplayItem[] { const items: DisplayItem[] = []; for (const msg of transcript) { if (msg.role === "assistant") { for (const part of msg.content) { if (part.type === "text") { items.push({ type: "text", text: part.text }); } else if (part.type === "toolCall") { items.push({ type: "toolCall", name: part.name, args: part.arguments as Record }); } } } } return items; } export function getFinalOutput(transcript: TranscriptMessage[]): string { for (let i = transcript.length - 1; i >= 0; i--) { const msg = transcript[i]; if (msg.role === "assistant") { for (const part of msg.content ?? []) { if (part.type === "text") return part.text; } } } return ""; }