/** Braille spinner frames for animated running indicator. */ export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; /** Tool name → human-readable action for activity descriptions. */ const TOOL_DISPLAY: Record = { read: "reading", bash: "running command", edit: "editing", write: "writing", grep: "searching", find: "finding files", ls: "listing", }; /** Format milliseconds as human-readable duration: "11.2s". */ export function formatMs(ms: number): string { return `${(ms / 1000).toFixed(1)}s`; } /** Format a token count compactly: "500 token", "12.3k token", "1.2M token". */ export function formatTokens(count: number): string { if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`; if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`; return `${count} token`; } /** Format turn count with optional max limit: "↻5≤30" or "↻5". */ export function formatTurns(turnCount: number, maxTurns?: number | null): string { return maxTurns != null ? `↻${turnCount}≤${maxTurns}` : `↻${turnCount}`; } /** Truncate text to a single line, max `len` chars. */ function truncateLine(text: string, len = 60): string { const line = text .split("\n") .find((l) => l.trim()) ?.trim() ?? ""; if (line.length <= len) return line; return `${line.slice(0, len)}...`; } /** Build a human-readable activity string from currently-running tools or response text. */ export function describeActivity( activeTools: readonly string[], responseText?: string, ): string { if (activeTools.length > 0) { const groups = new Map(); for (const toolName of activeTools) { const action = TOOL_DISPLAY[toolName] ?? toolName; groups.set(action, (groups.get(action) ?? 0) + 1); } const parts: string[] = []; for (const [action, count] of groups) { if (count > 1) { parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`); } else { parts.push(action); } } return `${parts.join(", ")}…`; } if (responseText && responseText.trim().length > 0) { return truncateLine(responseText); } return "thinking…"; }