import { useEffect, useMemo, useRef, useState } from "react"; import type { TranscriptEntry } from "../../adapters"; import type { ToolRunDecision } from "@paperclipai/shared"; import { MarkdownBody, type MarkdownExternalReferenceMap } from "../MarkdownBody"; import { cn, formatTokens } from "../../lib/utils"; import { runningLabelText } from "../../lib/status-colors"; import { Check, ChevronDown, ChevronRight, CircleAlert, GitCompare, TerminalSquare, User, } from "lucide-react"; import { toolTaxonomy } from "../task-chat/tool-taxonomy"; /** Family glyph for a tool block/row; the taxonomy falls back to Wrench. */ function ToolFamilyIcon({ name, className }: { name: string; className?: string }) { const Icon = toolTaxonomy(name).icon; return ; } export type TranscriptMode = "nice" | "raw"; export type TranscriptDensity = "comfortable" | "compact"; const RAW_VIRTUALIZATION_THRESHOLD = 300; const RAW_OVERSCAN_ROWS = 40; const RAW_ESTIMATED_ROW_HEIGHT = 36; const RAW_INITIAL_ROWS = 180; interface RunTranscriptViewProps { entries: TranscriptEntry[]; toolDecisions?: readonly ToolRunDecision[]; mode?: TranscriptMode; density?: TranscriptDensity; limit?: number; streaming?: boolean; collapseStdout?: boolean; emptyMessage?: string; className?: string; thinkingClassName?: string; externalReferences?: MarkdownExternalReferenceMap; } type TranscriptBlock = | { type: "message"; role: "assistant" | "user"; ts: string; // Timestamp of the first entry that opened this block. `ts` tracks the // latest merged delta and mutates every chunk; `startTs` stays fixed so // the React key is stable and the streaming block does not remount (and // restart its fade) on each delta. startTs: string; text: string; streaming: boolean; } | { type: "thinking"; ts: string; startTs: string; text: string; streaming: boolean; } | { type: "tool"; ts: string; endTs?: string; name: string; toolUseId?: string; invocationId?: string; actionRequestId?: string; input: unknown; result?: string; isError?: boolean; status: "running" | "completed" | "error"; } | { type: "activity"; ts: string; startTs: string; activityId?: string; name: string; status: "running" | "completed"; } | { type: "command_group"; ts: string; endTs?: string; items: Array<{ ts: string; endTs?: string; toolUseId?: string; invocationId?: string; actionRequestId?: string; input: unknown; result?: string; isError?: boolean; status: "running" | "completed" | "error"; }>; } | { type: "tool_group"; ts: string; endTs?: string; items: Array<{ ts: string; endTs?: string; name: string; toolUseId?: string; invocationId?: string; actionRequestId?: string; input: unknown; result?: string; isError?: boolean; status: "running" | "completed" | "error"; }>; } | { type: "stderr_group"; ts: string; endTs?: string; lines: Array<{ ts: string; text: string }>; } | { type: "system_group"; ts: string; endTs?: string; lines: Array<{ ts: string; text: string }>; } | { type: "stdout"; ts: string; startTs: string; text: string; } | { type: "event"; ts: string; label: string; tone: "info" | "warn" | "error" | "neutral"; text: string; detail?: string; } | { type: "provider_activity"; ts: string; family: Extract["family"]; eventType: string; status: Extract["status"]; title: string; summary: string; payload: Record; } | { type: "diff_group"; ts: string; endTs?: string; filePath?: string; hunks: Array<{ changeType: "add" | "remove" | "context" | "hunk" | "file_header" | "truncation"; text: 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 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; } /** Merge a streamed tool_call status update into the input captured so far. */ function mergeToolInput(previous: unknown, incoming: unknown): unknown { if (incoming === null || incoming === undefined) return previous; if (typeof incoming === "string") { return incoming.trim().length > 0 ? incoming : previous; } const previousRecord = asRecord(previous); const incomingRecord = asRecord(incoming); if (incomingRecord) { if (Object.keys(incomingRecord).length === 0) return previous; return previousRecord ? { ...previousRecord, ...incomingRecord } : incoming; } return incoming; } 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", "text"]) ?? 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); } type ToolDecisionRefs = { toolUseId?: string; invocationId?: string; actionRequestId?: string; }; type ToolDecisionMaps = { byInvocationId: Map; byActionRequestId: Map; }; function buildToolDecisionMaps(decisions: readonly ToolRunDecision[] | undefined): ToolDecisionMaps { const byInvocationId = new Map(); const byActionRequestId = new Map(); for (const decision of decisions ?? []) { byInvocationId.set(decision.invocation.id, decision); if (decision.actionRequest?.id) { byActionRequestId.set(decision.actionRequest.id, decision); } if (decision.latestAuditEvent?.actionRequestId) { byActionRequestId.set(decision.latestAuditEvent.actionRequestId, decision); } } return { byInvocationId, byActionRequestId }; } function findToolDecision(maps: ToolDecisionMaps, refs: ToolDecisionRefs): ToolRunDecision | null { if (refs.invocationId) { const decision = maps.byInvocationId.get(refs.invocationId); if (decision) return decision; } if (refs.actionRequestId) { const decision = maps.byActionRequestId.get(refs.actionRequestId); if (decision) return decision; } if (refs.toolUseId) { return maps.byInvocationId.get(refs.toolUseId) ?? maps.byActionRequestId.get(refs.toolUseId) ?? null; } return null; } function summarizeToolDecision(decision: ToolRunDecision | null): { label: string; className: string; detail?: string } | null { if (!decision) return null; if (decision.pendingAction) { return { label: "Needs approval", className: "text-amber-700 dark:text-amber-300", detail: `Action request ${decision.pendingAction.actionRequestId.slice(0, 8)}`, }; } if (decision.denialReason || decision.invocation.status === "denied" || decision.outcome === "denied") { return { label: "Denied", className: "text-red-700 dark:text-red-300", detail: decision.denialReason ?? decision.reasonCode ?? undefined, }; } if (decision.invocation.status === "failed" || decision.invocation.status === "timed_out" || decision.outcome === "failure" || decision.outcome === "timeout") { return { label: decision.invocation.status === "timed_out" || decision.outcome === "timeout" ? "Timed out" : "Failed", className: "text-red-700 dark:text-red-300", detail: decision.denialReason ?? decision.reasonCode ?? undefined, }; } if (decision.actionRequest?.status === "approved") { return { label: "Approved", className: "text-emerald-700 dark:text-emerald-300" }; } if (decision.actionRequest?.status === "executed") { return { label: "Executed", className: "text-emerald-700 dark:text-emerald-300" }; } if (decision.decision === "allow" || decision.invocation.status === "authorized" || decision.invocation.status === "executing" || decision.invocation.status === "succeeded") { return { label: "Allowed", className: "text-emerald-700 dark:text-emerald-300" }; } if (decision.decision === "require_approval" || decision.invocation.approvalState === "pending") { return { label: "Needs approval", className: "text-amber-700 dark:text-amber-300" }; } return { label: humanizeLabel(decision.invocation.status), className: "text-foreground/70", detail: decision.reasonCode ?? undefined, }; } 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 shouldHideNiceModeStderr(text: string): boolean { const normalized = compactWhitespace(text).toLowerCase(); return normalized.startsWith("[paperclip] skipping saved session resume"); } function groupCommandBlocks(blocks: TranscriptBlock[]): TranscriptBlock[] { const grouped: TranscriptBlock[] = []; let pending: Array["items"][number]> = []; let groupTs: string | null = null; let groupEndTs: string | undefined; const flush = () => { if (pending.length === 0 || !groupTs) return; grouped.push({ type: "command_group", ts: groupTs, endTs: groupEndTs, items: pending, }); pending = []; groupTs = null; groupEndTs = undefined; }; for (const block of blocks) { if (block.type === "tool" && isCommandTool(block.name, block.input)) { if (!groupTs) { groupTs = block.ts; } groupEndTs = block.endTs ?? block.ts; pending.push({ ts: block.ts, endTs: block.endTs, toolUseId: block.toolUseId, invocationId: block.invocationId, actionRequestId: block.actionRequestId, input: block.input, result: block.result, isError: block.isError, status: block.status, }); continue; } flush(); grouped.push(block); } flush(); return grouped; } /** Group consecutive non-command tool blocks into a single tool_group accordion. */ function groupToolBlocks(blocks: TranscriptBlock[]): TranscriptBlock[] { const grouped: TranscriptBlock[] = []; let pending: Array["items"][number]> = []; let groupTs: string | null = null; let groupEndTs: string | undefined; const flush = () => { if (pending.length === 0 || !groupTs) return; grouped.push({ type: "tool_group", ts: groupTs, endTs: groupEndTs, items: pending, }); pending = []; groupTs = null; groupEndTs = undefined; }; for (const block of blocks) { if (block.type === "tool" && !isCommandTool(block.name, block.input)) { if (!groupTs) groupTs = block.ts; groupEndTs = block.endTs ?? block.ts; pending.push({ ts: block.ts, endTs: block.endTs, name: block.name, toolUseId: block.toolUseId, invocationId: block.invocationId, actionRequestId: block.actionRequestId, input: block.input, result: block.result, isError: block.isError, status: block.status, }); continue; } flush(); grouped.push(block); } flush(); return grouped; } export 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, startTs: 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, startTs: entry.ts, text: entry.text, streaming: isStreaming, }); } continue; } if (entry.kind === "provider_activity") { blocks.push({ type: "provider_activity", ts: entry.ts, family: entry.family, eventType: entry.eventType, status: entry.status, title: entry.title, summary: entry.summary, payload: entry.payload }); continue; } if (entry.kind === "workspace_change") { blocks.push({ type: "event", ts: entry.ts, label: entry.complete ? "workspace diff" : "workspace changes", tone: "info", text: `${entry.totals.files} changed ${entry.totals.files === 1 ? "file" : "files"}`, detail: entry.source === "runner_verified" ? "Verified from workspace" : "Reported by harness", }); continue; } if (entry.kind === "workspace_file_reference") { blocks.push({ type: "event", ts: entry.ts, label: "file reference", tone: "info", text: entry.displayName, detail: entry.path }); continue; } if (entry.kind === "runtime_request") { blocks.push({ type: "event", ts: entry.ts, label: `runtime ${entry.requestType}`, tone: entry.status === "pending" ? "warn" : "info", text: entry.prompt, detail: entry.status }); continue; } if (entry.kind === "run_result") { blocks.push({ type: "event", ts: entry.ts, label: `result · ${entry.disposition}`, tone: entry.disposition === "blocked" ? "warn" : "info", text: entry.summary }); continue; } if (entry.kind === "run_terminal") { blocks.push({ type: "event", ts: entry.ts, label: "terminal", tone: entry.runState === "failed" ? "error" : "info", text: `${entry.runState} · ${entry.disposition}`, detail: entry.stopReason }); continue; } if (entry.kind === "tool_call") { const toolUseId = entry.toolUseId ?? extractToolUseId(entry.input); // Streaming runtimes (e.g. ACPX) re-emit the same tool call as its // status progresses. Fold updates into the existing running card // instead of stacking duplicate "Running" blocks. const pending = toolUseId ? pendingToolBlocks.get(toolUseId) : undefined; if (pending && pending.status === "running") { pending.input = mergeToolInput(pending.input, entry.input); continue; } const toolBlock: Extract = { type: "tool", ts: entry.ts, name: displayToolName(entry.name, entry.input), toolUseId, invocationId: entry.invocationId, actionRequestId: entry.actionRequestId, 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: entry.toolName ?? "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"), detail: !entry.isError && entry.text.trim().length > 0 ? `${formatTokens(entry.inputTokens)} / ${formatTokens(entry.outputTokens)} / $${entry.costUsd.toFixed(6)}` : undefined, }); continue; } if (entry.kind === "stderr") { if (shouldHideNiceModeStderr(entry.text)) { continue; } // Batch consecutive stderr entries into a single group const prev = blocks[blocks.length - 1]; if (prev && prev.type === "stderr_group") { prev.lines.push({ ts: entry.ts, text: entry.text }); prev.endTs = entry.ts; } else { blocks.push({ type: "stderr_group", ts: entry.ts, endTs: entry.ts, lines: [{ ts: entry.ts, 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, startTs: 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; } // Batch consecutive system events into a single collapsible group const prev = blocks[blocks.length - 1]; if (prev && prev.type === "system_group") { prev.lines.push({ ts: entry.ts, text: entry.text }); prev.endTs = entry.ts; } else { blocks.push({ type: "system_group", ts: entry.ts, endTs: entry.ts, lines: [{ ts: entry.ts, text: entry.text }], }); } continue; } const activeCommandBlock = [...blocks].reverse().find( (block): block is Extract => block.type === "tool" && block.status === "running" && isCommandTool(block.name, block.input), ); if (activeCommandBlock) { activeCommandBlock.result = activeCommandBlock.result ? `${activeCommandBlock.result}${activeCommandBlock.result.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`}` : entry.text; continue; } // ── Diff entries — accumulate into diff_group blocks ────────── if (entry.kind === "diff") { const prev = blocks[blocks.length - 1]; if (prev && prev.type === "diff_group") { if (entry.changeType === "file_header") { // New file in the same diff block — update filePath prev.filePath = entry.text; } prev.hunks.push({ changeType: entry.changeType, text: entry.text }); prev.endTs = entry.ts; } else { blocks.push({ type: "diff_group", ts: entry.ts, endTs: entry.ts, filePath: entry.changeType === "file_header" ? entry.text : undefined, hunks: [{ changeType: entry.changeType, text: entry.text }], }); } continue; } if (previous?.type === "stdout") { previous.text += previous.text.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`; previous.ts = entry.ts; } else { blocks.push({ type: "stdout", ts: entry.ts, startTs: entry.ts, text: entry.text, }); } } return groupToolBlocks(groupCommandBlocks(blocks)); } /** * Stable identity for a block's React key. Anchored to the block's opening * timestamp (`startTs`) or a durable id (`toolUseId`, `activityId`) rather than * its latest `ts`, which mutates on every streamed delta. A mutating key * remounts the block, restarting its 300ms fade-in so the text visibly blinks * out and back each chunk; a stable one keeps the streaming tail mounted. */ function transcriptBlockIdentity(block: TranscriptBlock): string { switch (block.type) { case "message": return `message:${block.role}:${block.startTs}`; case "thinking": return `thinking:${block.startTs}`; case "stdout": return `stdout:${block.startTs}`; case "activity": return `activity:${block.activityId ?? block.startTs}`; case "tool": return `tool:${block.toolUseId ?? block.ts}`; case "command_group": return `command_group:${block.ts}`; case "tool_group": return `tool_group:${block.ts}`; case "stderr_group": return `stderr_group:${block.ts}`; case "system_group": return `system_group:${block.ts}`; case "diff_group": return `diff_group:${block.ts}`; case "event": return `event:${block.label}:${block.ts}`; case "provider_activity": return `provider_activity:${block.eventType}:${block.ts}`; } } function TranscriptProviderActivity({ block, density }: { block: Extract; density: TranscriptDensity }) { const [open, setOpen] = useState(block.status === "running"); const steps = Array.isArray(block.payload.steps) ? block.payload.steps.map(asRecord).filter((value): value is Record => value !== null) : []; const children = Array.isArray(block.payload.children) ? block.payload.children.map(asRecord).filter((value): value is Record => value !== null) : []; const sources = Array.isArray(block.payload.sources) ? block.payload.sources.map(asRecord).filter((value): value is Record => value !== null) : []; const output = typeof block.payload.output === "string" ? block.payload.output.slice(-(8 * 1024)) : ""; return
{open ?
{steps.length > 0 ?
    {steps.map((step, index) =>
  1. {String(step.body ?? "")}
  2. )}
: null} {children.length > 0 ?
    {children.map((child, index) =>
  • {String(child.role ?? "Child agent")} · {String(child.status ?? "unknown")}
    {String(child.summary ?? "")}
  • )}
: null} {sources.length > 0 ?
    {sources.map((source, index) => { const url = typeof source.url === "string" && /^https?:\/\//.test(source.url) ? source.url : null; return
  • {url ? {String(source.title ?? url)} : String(source.title ?? "Unavailable source")} Provider-reported
  • ; })}
: null} {output ?
{output}
: null} {block.family === "provider_notice" && Array.isArray(block.payload.details) ?
{block.payload.details.slice(0, 64).map((value, index) => { const detail = asRecord(value); return detail && typeof detail.name === "string" && typeof detail.value === "string" ?
{detail.name}
{detail.value}
: null; })}
: null} {block.family === "model_identity" ?
Requested {String(block.payload.requestedModel ?? "—")} · Effective {String(block.payload.effectiveModel ?? "—")}
: null}
: null}
; } /** * Assign each block a stable, unique React key. Identity is position-independent * so earlier blocks collapsing (truncation) does not remount the survivors; a * per-render occurrence counter disambiguates the rare identity collision. */ export function keyTranscriptBlocks(blocks: TranscriptBlock[]): Array<{ block: TranscriptBlock; key: string }> { const seen = new Map(); return blocks.map((block) => { const identity = transcriptBlockIdentity(block); const occurrence = seen.get(identity) ?? 0; seen.set(identity, occurrence + 1); return { block, key: occurrence === 0 ? identity : `${identity}#${occurrence}` }; }); } function TranscriptMessageBlock({ block, density, externalReferences, }: { block: Extract; density: TranscriptDensity; externalReferences?: MarkdownExternalReferenceMap; }) { const isAssistant = block.role === "assistant"; const compact = density === "compact"; return (
{!isAssistant && (
User
)} *:first-child]:mt-0 [&>*:last-child]:mb-0", // Match the default view's chat message body (IssueChatThread: // `text-sm leading-6`) so streamed text reads identically (PAP-461, A2). compact ? "text-xs leading-5 text-foreground/85" : "text-sm leading-6", )} externalReferences={externalReferences} > {block.text} {block.streaming && (
Streaming
)}
); } function TranscriptThinkingBlock({ block, density, className, externalReferences, }: { block: Extract; density: TranscriptDensity; className?: string; externalReferences?: MarkdownExternalReferenceMap; }) { return ( *:first-child]:mt-0 [&>*:last-child]:mb-0", density === "compact" ? "text-(length:--text-micro) leading-5" : "text-(length:--text-compact) leading-5", className, )} externalReferences={externalReferences} > {block.text} ); } function ToolDecisionBadge({ decision }: { decision: ToolRunDecision | null }) { const summary = summarizeToolDecision(decision); if (!summary) return null; return ( {summary.label} ); } function ToolDecisionInlineDetail({ decision }: { decision: ToolRunDecision | null }) { const summary = summarizeToolDecision(decision); if (!summary?.detail) return null; return (
{summary.detail}
); } function ToolDecisionDetails({ decision, compact }: { decision: ToolRunDecision | null; compact: boolean }) { if (!decision) return null; const actionRequest = decision.actionRequest; return (
Decision {decision.reasonCode && {decision.reasonCode}}
{decision.denialReason && (
{decision.denialReason}
)}
invocation {decision.invocation.id.slice(0, 8)} audit {decision.auditEvents.length} {actionRequest && action {actionRequest.status} {actionRequest.id.slice(0, 8)}} {actionRequest?.interactionId && card {actionRequest.interactionId.slice(0, 8)}}
{decision.pendingAction?.previewMarkdown && ( {decision.pendingAction.previewMarkdown} )}
); } function TranscriptToolCard({ block, density, decision, }: { block: Extract; density: TranscriptDensity; decision: ToolRunDecision | null; }) { const [open, setOpen] = useState(block.status === "error" || Boolean(decision?.pendingAction || decision?.denialReason)); const compact = density === "compact"; const parsedResult = parseStructuredToolResult(block.result); const statusLabel = block.status === "running" ? "Running" : block.status === "error" ? "Errored" : "Completed"; const statusTone = block.status === "running" ? "text-blue-700 dark:text-blue-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-blue-600 dark:text-blue-300", ); const summary = block.status === "running" ? 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" ? ( ) : ( )}
{block.name} {statusLabel}
{summary}
{open && (
Input
                  {formatToolPayload(block.input) || ""}
                
Result
                  {block.result ? formatToolPayload(block.result) : "Waiting for result..."}
                
)}
); } function hasSelectedText() { if (typeof window === "undefined") return false; return (window.getSelection()?.toString().length ?? 0) > 0; } function TranscriptCommandGroup({ block, density, toolDecisionMaps, }: { block: Extract; density: TranscriptDensity; toolDecisionMaps: ToolDecisionMaps; }) { const [open, setOpen] = useState(false); const compact = density === "compact"; const runningItem = [...block.items].reverse().find((item) => item.status === "running"); const latestItem = block.items[block.items.length - 1] ?? null; const highlightedDecision = block.items .map((item) => findToolDecision(toolDecisionMaps, item)) .find((decision) => decision?.pendingAction || decision?.denialReason) ?? block.items.map((item) => findToolDecision(toolDecisionMaps, item)).find(Boolean) ?? null; const hasError = block.items.some((item) => item.status === "error"); const isRunning = Boolean(runningItem); const showExpandedErrorState = open && hasError; const title = isRunning ? "Executing command" : block.items.length === 1 ? "Executed command" : `Executed ${block.items.length} commands`; const subtitle = runningItem ? summarizeToolInput("command_execution", runningItem.input, density) : null; const statusTone = isRunning ? "text-blue-700 dark:text-blue-300" : "text-foreground/70"; return (
{ if (hasSelectedText()) return; setOpen((value) => !value); }} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setOpen((value) => !value); } }} >
{block.items.slice(0, Math.min(block.items.length, 3)).map((_, index) => ( 0 && "-ml-1.5", isRunning ? "border-blue-500/25 bg-blue-500/[0.08] text-blue-600 dark:text-blue-300" : "border-border/70 bg-background text-foreground/55", isRunning && "animate-pulse", )} > ))}
{title}
{highlightedDecision && (
)} {subtitle && (
{subtitle}
)} {!subtitle && latestItem?.status === "error" && open && (
Command failed
)}
{open && (
{block.items.map((item, index) => (
{summarizeToolInput("command_execution", item.input, density)}
{item.result && (
                  {formatToolPayload(item.result)}
                
)}
))}
)}
); } function TranscriptToolGroup({ block, density, toolDecisionMaps, }: { block: Extract; density: TranscriptDensity; toolDecisionMaps: ToolDecisionMaps; }) { const [open, setOpen] = useState(false); const compact = density === "compact"; const runningItem = [...block.items].reverse().find((item) => item.status === "running"); const hasError = block.items.some((item) => item.status === "error"); const isRunning = Boolean(runningItem); const highlightedDecision = block.items .map((item) => findToolDecision(toolDecisionMaps, item)) .find((decision) => decision?.pendingAction || decision?.denialReason) ?? block.items.map((item) => findToolDecision(toolDecisionMaps, item)).find(Boolean) ?? null; const uniqueNames = [...new Set(block.items.map((item) => item.name))]; const toolLabel = uniqueNames.length === 1 ? humanizeLabel(uniqueNames[0]) : `${uniqueNames.length} tools`; const title = isRunning ? `Using ${toolLabel}` : block.items.length === 1 ? `Used ${toolLabel}` : `Used ${toolLabel} (${block.items.length} calls)`; const subtitle = runningItem ? summarizeToolInput(runningItem.name, runningItem.input, density) : null; const statusTone = isRunning ? "text-blue-700 dark:text-blue-300" : "text-foreground/70"; return (
{ if (hasSelectedText()) return; setOpen((v) => !v); }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen((v) => !v); } }} >
{block.items.slice(0, Math.min(block.items.length, 3)).map((item, index) => { const isItemRunning = item.status === "running"; const isItemError = item.status === "error"; return ( 0 && "-ml-1.5", isItemRunning ? "border-blue-500/25 bg-blue-500/[0.08] text-blue-600 dark:text-blue-300" : isItemError ? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300" : "border-border/70 bg-background text-foreground/55", isItemRunning && "animate-pulse", )} > ); })}
{title}
{highlightedDecision && (
)} {subtitle && (
{subtitle}
)}
{open && (
{block.items.map((item, index) => (
{humanizeLabel(item.name)} {item.status === "running" ? "Running" : item.status === "error" ? "Errored" : "Completed"}
Input
                    {formatToolPayload(item.input) || ""}
                  
{item.result && (
Result
                      {formatToolPayload(item.result)}
                    
)}
))}
)}
); } function TranscriptActivityRow({ block, density, }: { block: Extract; density: TranscriptDensity; }) { return (
{block.status === "completed" ? ( ) : ( )}
{block.name}
); } function TranscriptEventRow({ block, density, externalReferences, }: { block: Extract; density: TranscriptDensity; externalReferences?: MarkdownExternalReferenceMap; }) { 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" ? ( *:first-child]:mt-0 [&>*:last-child]:mb-0 text-sky-700 dark:text-sky-300", compact ? "text-(length:--text-micro) leading-5" : "text-xs leading-5", )} externalReferences={externalReferences} > {block.text} ) : (
{block.label} {block.text ? {block.text} : null}
)} {block.detail && (
              {block.detail}
            
)}
); } function TranscriptDiffGroup({ block, density, }: { block: Extract; density: TranscriptDensity; }) { const [open, setOpen] = useState(false); const compact = density === "compact"; // Count add/remove lines (exclude context, hunk, file_header, truncation) const addCount = block.hunks.filter((h) => h.changeType === "add").length; const removeCount = block.hunks.filter((h) => h.changeType === "remove").length; const hasChanges = addCount > 0 || removeCount > 0; // Extract a short file name from the path const shortFile = block.filePath ? block.filePath.split("/").pop() ?? block.filePath : "diff"; return (
setOpen((v) => !v)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen((v) => !v); } }} > {shortFile} {hasChanges && ( +{addCount} {" "} -{removeCount} )} {open ? : }
{open && (
          {block.hunks.map((hunk, i) => {
            const key = `${i}-${hunk.changeType}`;
            switch (hunk.changeType) {
              case "remove":
                return (
                  
                    -
                    {hunk.text}
                    {"\n"}
                  
                );
              case "add":
                return (
                  
                    +
                    {hunk.text}
                    {"\n"}
                  
                );
              case "file_header":
                return (
                  
                    {hunk.text}
                    {"\n"}
                  
                );
              case "truncation":
                return (
                  
                    {hunk.text}
                    {"\n"}
                  
                );
              case "context":
              default:
                return (
                  
                    {" "}
                    {hunk.text}
                    {"\n"}
                  
                );
            }
          })}
        
)}
); } function TranscriptStderrGroup({ block, density, }: { block: Extract; density: TranscriptDensity; }) { const [open, setOpen] = useState(false); const compact = density === "compact"; return (
setOpen((v) => !v)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen((v) => !v); } }} > {block.lines.length} log {block.lines.length === 1 ? "line" : "lines"} {open ? : }
{open && (
          {block.lines.map((line, i) => (
            
              {i > 0 ? "\n" : ""}
              {line.text}
            
          ))}
        
)}
); } function TranscriptSystemGroup({ block, density, }: { block: Extract; density: TranscriptDensity; }) { const [open, setOpen] = useState(false); return (
setOpen((v) => !v)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen((v) => !v); } }} > {block.lines.length} system {block.lines.length === 1 ? "message" : "messages"} {open ? : }
{open && (
          {block.lines.map((line, i) => (
            
              {i > 0 ? "\n" : ""}
              {line.text}
            
          ))}
        
)}
); } function TranscriptStdoutRow({ block, density, collapseByDefault, }: { block: Extract; density: TranscriptDensity; collapseByDefault: boolean; }) { const [open, setOpen] = useState(!collapseByDefault); return (
stdout
{open && (
          {block.text}
        
)}
); } function findScrollParent(element: HTMLElement): HTMLElement | Window { let current = element.parentElement; while (current) { const style = window.getComputedStyle(current); if (/(auto|scroll)/.test(style.overflowY) && current.scrollHeight > current.clientHeight) { return current; } current = current.parentElement; } return window; } function rawEntryContent(entry: TranscriptEntry): string { if (entry.kind === "provider_activity") { return `${entry.eventType}\n${entry.title}: ${entry.summary}`; } if (entry.kind === "tool_call") { return `${entry.name}\n${formatToolPayload(entry.input)}`; } if (entry.kind === "tool_result") { return formatToolPayload(entry.content); } if (entry.kind === "result") { return `${entry.text}\n${formatTokens(entry.inputTokens)} / ${formatTokens(entry.outputTokens)} / $${entry.costUsd.toFixed(6)}`; } if (entry.kind === "init") { return `model=${entry.model}${entry.sessionId ? ` session=${entry.sessionId}` : ""}`; } if (entry.kind === "workspace_change") return `${entry.totals.files} files · ${entry.source} · revision ${entry.revision}`; if (entry.kind === "workspace_file_reference") return `${entry.displayName}\n${entry.path}`; if (entry.kind === "runtime_request") return `${entry.requestType} · ${entry.status}\n${entry.prompt}`; if (entry.kind === "run_result") return `${entry.disposition}\n${entry.summary}`; if (entry.kind === "run_terminal") return `${entry.runState} · ${entry.turnState} · ${entry.disposition}${entry.stopReason ? `\n${entry.stopReason}` : ""}`; return entry.text; } function RawTranscriptView({ entries, density, }: { entries: TranscriptEntry[]; density: TranscriptDensity; }) { const compact = density === "compact"; const listRef = useRef(null); const shouldVirtualize = entries.length > RAW_VIRTUALIZATION_THRESHOLD; const [range, setRange] = useState(() => ({ start: 0, end: Math.min(entries.length, shouldVirtualize ? RAW_INITIAL_ROWS : entries.length), })); useEffect(() => { if (!shouldVirtualize) { setRange({ start: 0, end: entries.length }); return; } const list = listRef.current; if (!list) return; const scrollParent = findScrollParent(list); const updateRange = () => { const scrollElement: HTMLElement | null = scrollParent === window ? null : (scrollParent as HTMLElement); const scrollerTop = scrollElement ? scrollElement.getBoundingClientRect().top : 0; const scrollerHeight = scrollElement ? scrollElement.clientHeight : window.innerHeight; const listTop = list.getBoundingClientRect().top; const visibleTop = Math.max(0, scrollerTop - listTop); const visibleBottom = Math.max(visibleTop + scrollerHeight, 0); const nextStart = Math.max(0, Math.floor(visibleTop / RAW_ESTIMATED_ROW_HEIGHT) - RAW_OVERSCAN_ROWS); const nextEnd = Math.min( entries.length, Math.ceil(visibleBottom / RAW_ESTIMATED_ROW_HEIGHT) + RAW_OVERSCAN_ROWS, ); setRange((current) => ( current.start === nextStart && current.end === nextEnd ? current : { start: nextStart, end: nextEnd } )); }; updateRange(); const frame = window.requestAnimationFrame(updateRange); scrollParent.addEventListener("scroll", updateRange, { passive: true }); window.addEventListener("resize", updateRange); return () => { window.cancelAnimationFrame(frame); scrollParent.removeEventListener("scroll", updateRange); window.removeEventListener("resize", updateRange); }; }, [entries.length, shouldVirtualize]); const visibleEntries = shouldVirtualize ? entries.slice(range.start, range.end) : entries; const topSpacer = shouldVirtualize ? range.start * RAW_ESTIMATED_ROW_HEIGHT : 0; const bottomSpacer = shouldVirtualize ? Math.max(0, entries.length - range.end) * RAW_ESTIMATED_ROW_HEIGHT : 0; return (
{topSpacer > 0 &&