import { useMemo, useState } from "react"; import type { TranscriptEntry } from "../../adapters"; import { MarkdownBody } from "../MarkdownBody"; import { cn, formatTokens } from "../../lib/utils"; import { Check, ChevronDown, ChevronRight, CircleAlert, TerminalSquare, User, Wrench, } from "lucide-react"; export type TranscriptMode = "nice" | "raw"; export type TranscriptDensity = "comfortable" | "compact"; interface RunTranscriptViewProps { entries: TranscriptEntry[]; mode?: TranscriptMode; density?: TranscriptDensity; limit?: number; streaming?: boolean; emptyMessage?: string; className?: string; } type TranscriptBlock = | { type: "message"; role: "assistant" | "user"; ts: string; text: string; streaming: boolean; } | { type: "thinking"; ts: string; text: string; streaming: boolean; } | { type: "tool"; ts: string; endTs?: string; name: string; toolUseId?: string; input: unknown; result?: string; isError?: boolean; status: "running" | "completed" | "error"; } | { type: "activity"; ts: string; activityId?: string; name: string; status: "running" | "completed"; } | { type: "event"; ts: string; label: string; tone: "info" | "warn" | "error" | "neutral"; text: string; detail?: string; }; function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; } function compactWhitespace(value: string): string { return value.replace(/\s+/g, " ").trim(); } function truncate(value: string, max: number): string { return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}…` : value; } function stripMarkdown(value: string): string { return compactWhitespace( value .replace(/```[\s\S]*?```/g, " code ") .replace(/`([^`]+)`/g, "$1") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(/[*_#>-]/g, " "), ); } function humanizeLabel(value: string): string { return value .replace(/[_-]+/g, " ") .trim() .replace(/\b\w/g, (char) => char.toUpperCase()); } function stripWrappedShell(command: string): string { const trimmed = compactWhitespace(command); const shellWrapped = trimmed.match(/^(?:(?:\/bin\/)?(?:zsh|bash|sh)|cmd(?:\.exe)?(?:\s+\/d)?(?:\s+\/s)?(?:\s+\/c)?)\s+(?:-lc|\/c)\s+(.+)$/i); const inner = shellWrapped?.[1] ?? trimmed; const quoted = inner.match(/^(['"])([\s\S]*)\1$/); return compactWhitespace(quoted?.[2] ?? inner); } function formatUnknown(value: unknown): string { if (typeof value === "string") return value; if (value === null || value === undefined) return ""; try { return JSON.stringify(value, null, 2); } catch { return String(value); } } function formatToolPayload(value: unknown): string { if (typeof value === "string") { try { return JSON.stringify(JSON.parse(value), null, 2); } catch { return value; } } return formatUnknown(value); } function extractToolUseId(input: unknown): string | undefined { const record = asRecord(input); if (!record) return undefined; const candidates = [ record.toolUseId, record.tool_use_id, record.callId, record.call_id, record.id, ]; for (const candidate of candidates) { if (typeof candidate === "string" && candidate.trim()) { return candidate; } } return undefined; } function summarizeRecord(record: Record, keys: string[]): string | null { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) { return truncate(compactWhitespace(value), 120); } } return null; } function summarizeToolInput(name: string, input: unknown, density: TranscriptDensity): string { const compactMax = density === "compact" ? 72 : 120; if (typeof input === "string") { const normalized = isCommandTool(name, input) ? stripWrappedShell(input) : compactWhitespace(input); return truncate(normalized, compactMax); } const record = asRecord(input); if (!record) { const serialized = compactWhitespace(formatUnknown(input)); return serialized ? truncate(serialized, compactMax) : `Inspect ${name} input`; } const command = typeof record.command === "string" ? record.command : typeof record.cmd === "string" ? record.cmd : null; if (command && isCommandTool(name, record)) { return truncate(stripWrappedShell(command), compactMax); } const direct = summarizeRecord(record, ["command", "cmd", "path", "filePath", "file_path", "query", "url", "prompt", "message"]) ?? summarizeRecord(record, ["pattern", "name", "title", "target", "tool"]) ?? null; if (direct) return truncate(direct, compactMax); if (Array.isArray(record.paths) && record.paths.length > 0) { const first = record.paths.find((value): value is string => typeof value === "string" && value.trim().length > 0); if (first) { return truncate(`${record.paths.length} paths, starting with ${first}`, compactMax); } } const keys = Object.keys(record); if (keys.length === 0) return `No ${name} input`; if (keys.length === 1) return truncate(`${keys[0]} payload`, compactMax); return truncate(`${keys.length} fields: ${keys.slice(0, 3).join(", ")}`, compactMax); } function parseStructuredToolResult(result: string | undefined) { if (!result) return null; const lines = result.split(/\r?\n/); const metadata = new Map(); let bodyStartIndex = lines.findIndex((line) => line.trim() === ""); if (bodyStartIndex === -1) bodyStartIndex = lines.length; for (let index = 0; index < bodyStartIndex; index += 1) { const match = lines[index]?.match(/^([a-z_]+):\s*(.+)$/i); if (match) { metadata.set(match[1].toLowerCase(), compactWhitespace(match[2])); } } const body = lines.slice(Math.min(bodyStartIndex + 1, lines.length)) .map((line) => compactWhitespace(line)) .filter(Boolean) .join("\n"); return { command: metadata.get("command") ?? null, status: metadata.get("status") ?? null, exitCode: metadata.get("exit_code") ?? null, body, }; } function isCommandTool(name: string, input: unknown): boolean { if (name === "command_execution" || name === "shell" || name === "shellToolCall" || name === "bash") { return true; } if (typeof input === "string") { return /\b(?:bash|zsh|sh|cmd|powershell)\b/i.test(input); } const record = asRecord(input); return Boolean(record && (typeof record.command === "string" || typeof record.cmd === "string")); } function displayToolName(name: string, input: unknown): string { if (isCommandTool(name, input)) return "Executing command"; return humanizeLabel(name); } function summarizeToolResult(result: string | undefined, isError: boolean | undefined, density: TranscriptDensity): string { if (!result) return isError ? "Tool failed" : "Waiting for result"; const structured = parseStructuredToolResult(result); if (structured) { if (structured.body) { return truncate(structured.body.split("\n")[0] ?? structured.body, density === "compact" ? 84 : 140); } if (structured.status === "completed") return "Completed"; if (structured.status === "failed" || structured.status === "error") { return structured.exitCode ? `Failed with exit code ${structured.exitCode}` : "Failed"; } } const lines = result .split(/\r?\n/) .map((line) => compactWhitespace(line)) .filter(Boolean); const firstLine = lines[0] ?? result; return truncate(firstLine, density === "compact" ? 84 : 140); } function parseSystemActivity(text: string): { activityId?: string; name: string; status: "running" | "completed" } | null { const match = text.match(/^item (started|completed):\s*([a-z0-9_-]+)(?:\s+\(id=([^)]+)\))?$/i); if (!match) return null; return { status: match[1].toLowerCase() === "started" ? "running" : "completed", name: humanizeLabel(match[2] ?? "Activity"), activityId: match[3] || undefined, }; } function normalizeTranscript(entries: TranscriptEntry[], streaming: boolean): TranscriptBlock[] { const blocks: TranscriptBlock[] = []; const pendingToolBlocks = new Map>(); const pendingActivityBlocks = new Map>(); for (const entry of entries) { const previous = blocks[blocks.length - 1]; if (entry.kind === "assistant" || entry.kind === "user") { const isStreaming = streaming && entry.kind === "assistant" && entry.delta === true; if (previous?.type === "message" && previous.role === entry.kind) { previous.text += previous.text.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`; previous.ts = entry.ts; previous.streaming = previous.streaming || isStreaming; } else { blocks.push({ type: "message", role: entry.kind, ts: entry.ts, text: entry.text, streaming: isStreaming, }); } continue; } if (entry.kind === "thinking") { const isStreaming = streaming && entry.delta === true; if (previous?.type === "thinking") { previous.text += previous.text.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`; previous.ts = entry.ts; previous.streaming = previous.streaming || isStreaming; } else { blocks.push({ type: "thinking", ts: entry.ts, text: entry.text, streaming: isStreaming, }); } continue; } if (entry.kind === "tool_call") { const toolBlock: Extract = { type: "tool", ts: entry.ts, name: displayToolName(entry.name, entry.input), toolUseId: entry.toolUseId ?? extractToolUseId(entry.input), input: entry.input, status: "running", }; blocks.push(toolBlock); if (toolBlock.toolUseId) { pendingToolBlocks.set(toolBlock.toolUseId, toolBlock); } continue; } if (entry.kind === "tool_result") { const matched = pendingToolBlocks.get(entry.toolUseId) ?? [...blocks].reverse().find((block): block is Extract => block.type === "tool" && block.status === "running"); if (matched) { matched.result = entry.content; matched.isError = entry.isError; matched.status = entry.isError ? "error" : "completed"; matched.endTs = entry.ts; pendingToolBlocks.delete(entry.toolUseId); } else { blocks.push({ type: "tool", ts: entry.ts, endTs: entry.ts, name: "tool", toolUseId: entry.toolUseId, input: null, result: entry.content, isError: entry.isError, status: entry.isError ? "error" : "completed", }); } continue; } if (entry.kind === "init") { blocks.push({ type: "event", ts: entry.ts, label: "init", tone: "info", text: `model ${entry.model}${entry.sessionId ? ` • session ${entry.sessionId}` : ""}`, }); continue; } if (entry.kind === "result") { blocks.push({ type: "event", ts: entry.ts, label: "result", tone: entry.isError ? "error" : "info", text: entry.text.trim() || entry.errors[0] || (entry.isError ? "Run failed" : "Completed"), }); continue; } if (entry.kind === "stderr") { blocks.push({ type: "event", ts: entry.ts, label: "stderr", tone: "error", text: entry.text, }); continue; } if (entry.kind === "system") { if (compactWhitespace(entry.text).toLowerCase() === "turn started") { continue; } const activity = parseSystemActivity(entry.text); if (activity) { const existing = activity.activityId ? pendingActivityBlocks.get(activity.activityId) : undefined; if (existing) { existing.status = activity.status; existing.ts = entry.ts; if (activity.status === "completed" && activity.activityId) { pendingActivityBlocks.delete(activity.activityId); } } else { const block: Extract = { type: "activity", ts: entry.ts, activityId: activity.activityId, name: activity.name, status: activity.status, }; blocks.push(block); if (activity.status === "running" && activity.activityId) { pendingActivityBlocks.set(activity.activityId, block); } } continue; } blocks.push({ type: "event", ts: entry.ts, label: "system", tone: "warn", text: entry.text, }); continue; } blocks.push({ type: "event", ts: entry.ts, label: "stdout", tone: "neutral", text: entry.text, }); } return blocks; } function TranscriptMessageBlock({ block, density, }: { block: Extract; density: TranscriptDensity; }) { const isAssistant = block.role === "assistant"; const compact = density === "compact"; return (
{!isAssistant && (
User
)} {compact ? (
{truncate(stripMarkdown(block.text), 360)}
) : ( {block.text} )} {block.streaming && (
Streaming
)}
); } function TranscriptThinkingBlock({ block, density, }: { block: Extract; density: TranscriptDensity; }) { return (
{block.text}
); } function TranscriptToolCard({ block, density, }: { block: Extract; density: TranscriptDensity; }) { const [open, setOpen] = useState(block.status === "error"); const compact = density === "compact"; const commandTool = isCommandTool(block.name, block.input); const parsedResult = parseStructuredToolResult(block.result); const commandPreview = summarizeToolInput(block.name, block.input, density); const statusLabel = block.status === "running" ? "Running" : block.status === "error" ? "Errored" : "Completed"; const statusTone = block.status === "running" ? "text-cyan-700 dark:text-cyan-300" : block.status === "error" ? "text-red-700 dark:text-red-300" : "text-emerald-700 dark:text-emerald-300"; const detailsClass = cn( "space-y-3", block.status === "error" && "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3", ); const iconClass = cn( "mt-0.5 h-3.5 w-3.5 shrink-0", block.status === "error" ? "text-red-600 dark:text-red-300" : block.status === "completed" ? "text-emerald-600 dark:text-emerald-300" : "text-cyan-600 dark:text-cyan-300", ); const summary = block.status === "running" ? commandTool ? commandPreview : summarizeToolInput(block.name, block.input, density) : block.status === "completed" && parsedResult?.body ? truncate(parsedResult.body.split("\n")[0] ?? parsedResult.body, compact ? 84 : 140) : summarizeToolResult(block.result, block.isError, density); return (
{block.status === "error" ? ( ) : block.status === "completed" && !commandTool ? ( ) : commandTool ? ( ) : ( )}
{block.name} {!commandTool && ( {statusLabel} )}
{commandTool ? ( {summary} ) : ( {summary} )}
{commandTool && block.status !== "running" && (
{block.status === "error" ? "Command failed" : parsedResult?.status === "completed" ? "Command completed" : statusLabel}
)}
{open && (
Input
                  {formatToolPayload(block.input) || ""}
                
Result
                  {block.result ? formatToolPayload(block.result) : "Waiting for result..."}
                
)}
); } function TranscriptActivityRow({ block, density, }: { block: Extract; density: TranscriptDensity; }) { return (
{block.status === "completed" ? ( ) : ( )}
{block.name}
); } function TranscriptEventRow({ block, density, }: { block: Extract; density: TranscriptDensity; }) { const compact = density === "compact"; const toneClasses = block.tone === "error" ? "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3 text-red-700 dark:text-red-300" : block.tone === "warn" ? "text-amber-700 dark:text-amber-300" : block.tone === "info" ? "text-sky-700 dark:text-sky-300" : "text-foreground/75"; return (
{block.tone === "error" ? ( ) : block.tone === "warn" ? ( ) : ( )}
{block.label === "result" && block.tone !== "error" ? (
{block.text}
) : (
{block.label} {block.text ? {block.text} : null}
)} {block.detail && (
              {block.detail}
            
)}
); } function RawTranscriptView({ entries, density, }: { entries: TranscriptEntry[]; density: TranscriptDensity; }) { const compact = density === "compact"; return (
{entries.map((entry, idx) => (
{entry.kind}
            {entry.kind === "tool_call"
              ? `${entry.name}\n${formatToolPayload(entry.input)}`
              : entry.kind === "tool_result"
                ? formatToolPayload(entry.content)
                : entry.kind === "result"
                  ? `${entry.text}\n${formatTokens(entry.inputTokens)} / ${formatTokens(entry.outputTokens)} / $${entry.costUsd.toFixed(6)}`
                  : entry.kind === "init"
                    ? `model=${entry.model}${entry.sessionId ? ` session=${entry.sessionId}` : ""}`
                    : entry.text}
          
))}
); } export function RunTranscriptView({ entries, mode = "nice", density = "comfortable", limit, streaming = false, emptyMessage = "No transcript yet.", className, }: RunTranscriptViewProps) { const blocks = useMemo(() => normalizeTranscript(entries, streaming), [entries, streaming]); const visibleBlocks = limit ? blocks.slice(-limit) : blocks; const visibleEntries = limit ? entries.slice(-limit) : entries; if (entries.length === 0) { return (
{emptyMessage}
); } if (mode === "raw") { return (
); } return (
{visibleBlocks.map((block, index) => (
{block.type === "message" && } {block.type === "thinking" && } {block.type === "tool" && } {block.type === "activity" && } {block.type === "event" && }
))}
); }