/** * result-renderer.ts — Pure per-status rendering functions for Agent tool results. * * All functions are stateless: they receive AgentDetails and a Theme, returning * formatted strings. No SDK types, no timers, no side effects. * Consumed by the renderResult hook registered at the extension root. */ import type { AgentStatus as SubagentStatus, TurnBudget, } from "./view-types.js"; import type { AgentDetails, Theme } from "./display.js"; import { formatMs, formatTurnBudget } from "./display.js"; import { GLYPHS, spinnerAt } from "./glyphs.js"; import { wrappedUpAtTurnLimit } from "./outcome-presentation.js"; // ---- Dispatcher ---- /** Dispatch to the per-status renderer based on details.status and isPartial. */ export function renderAgentResult( details: AgentDetails, resultText: string, expanded: boolean, isPartial: boolean, theme: Theme, ): string { if (isPartial || details.status === "running") return renderRunning(details, theme); if (details.status === "background") return renderBackground(details, theme); if (details.status === "completed") return renderCompleted(details, resultText, expanded, theme); if (details.status === "stopped") return renderStopped(details, theme); return renderFailed(details, theme); } // ---- Per-status renderers ---- /** Render running/partial status: spinner + stats + activity line. */ export function renderRunning(details: AgentDetails, theme: Theme): string { const frame = spinnerAt(details.spinnerFrame ?? 0); const s = renderStats(details, theme); let line = theme.fg("accent", frame) + (s ? " " + s : ""); line += "\n" + theme.fg( "dim", ` ${GLYPHS.subLine} ${details.activity ?? "thinking\u2026"}`, ); return line; } /** Render background launch status. */ export function renderBackground(details: AgentDetails, theme: Theme): string { return theme.fg( "dim", ` ${GLYPHS.subLine} Running in background (ID: ${details.agentId})`, ); } /** Render completed status, with a turn-limit caveat when it wrapped up, and optional expanded result text. */ export function renderCompleted( details: AgentDetails, resultText: string, expanded: boolean, theme: Theme, ): string { const duration = formatMs(details.durationMs); const wrappedUp = wrappedUpAtTurnLimit(details); const icon = wrappedUp ? renderWrappedUpIcon(theme) : renderStatusIcon("completed", theme); const s = renderStats(details, theme); let line = icon + (s ? " " + s : ""); line += " " + theme.fg("dim", "\u00B7") + " " + theme.fg("dim", duration); if (expanded) { if (resultText) { const lines = resultText.split("\n").slice(0, 50); for (const l of lines) { line += "\n" + theme.fg("dim", ` ${l}`); } if (resultText.split("\n").length > 50) { line += "\n" + theme.fg( "muted", " ... (use get_subagent_result with verbose for full output)", ); } } } else { const doneText = wrappedUp ? "Wrapped up (budget warning)" : "Done"; line += "\n" + theme.fg("dim", ` ${GLYPHS.subLine} ${doneText}`); } return line; } /** Render stopped status: dim stop icon + stats + "Stopped". */ export function renderStopped(details: AgentDetails, theme: Theme): string { const s = renderStats(details, theme); let line = renderStatusIcon("stopped", theme) + (s ? " " + s : ""); line += "\n" + theme.fg("dim", ` ${GLYPHS.subLine} Stopped`); return line; } /** Render error or aborted status: error icon + stats + status message. */ export function renderFailed(details: AgentDetails, theme: Theme): string { const s = renderStats(details, theme); let line = renderStatusIcon(details.status === "error" ? "error" : "aborted", theme) + (s ? " " + s : ""); if (details.status === "error") { line += "\n" + theme.fg( "error", ` ${GLYPHS.subLine} Error: ${details.error ?? "unknown"}`, ); } else { line += "\n" + theme.fg("warning", ` ${GLYPHS.subLine} Aborted (turn limit reached)`); } return line; } // ---- Shared helpers ---- /** * The themed glyph for an outcome: its status glyph, drawn in the warning color * for a run that wrapped up at its turn limit. */ export function renderOutcomeIcon( // A read view over a live record, so the optional takes an explicit // `undefined` — see the note on `StatusOutcome`. outcome: { readonly status: SubagentStatus; readonly turnBudget?: TurnBudget | undefined; }, theme: Theme, ): string { if (wrappedUpAtTurnLimit(outcome)) return renderWrappedUpIcon(theme); return renderStatusIcon(outcome.status, theme); } /** A success glyph in the warning color: finished, with a turn-limit caveat. */ function renderWrappedUpIcon(theme: Theme): string { return theme.fg("warning", GLYPHS.success); } /** * The themed status glyph for a settled or pending agent. * * Exhaustive over `SubagentStatus`, so a status added later fails to compile * here rather than falling through to an unmarked icon. Shared with * `get-result-renderer.ts`, which draws the same vocabulary for the same enum. */ export function renderStatusIcon(status: SubagentStatus, theme: Theme): string { switch (status) { case "completed": return theme.fg("success", GLYPHS.success); case "stopped": return theme.fg("dim", GLYPHS.stopped); case "error": case "aborted": return theme.fg("error", GLYPHS.failure); case "queued": return theme.fg("dim", GLYPHS.queued); case "running": return theme.fg("dim", GLYPHS.streaming); } } /** * Build the stats string: "anthropic/claude-haiku-4-5 · thinking: high · ↻5≤30 · 3 tool uses · 33.8k token". * Returns an empty string when all fields are absent or zero. */ export function renderStats(details: AgentDetails, theme: Theme): string { const parts: string[] = []; if (details.modelName) parts.push(details.modelName); if (details.tags) parts.push(...details.tags); if (details.turnBudget) { parts.push(formatTurnBudget(details.turnBudget)); } if (details.toolUses > 0) parts.push( `${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`, ); if (details.tokens) parts.push(details.tokens); return parts .map((p) => theme.fg("dim", p)) .join(" " + theme.fg("dim", "\u00B7") + " "); }