From b3e71ca562e687ca1d6daf7cc907cacef6526fc2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 11 Mar 2026 12:14:12 -0500 Subject: [PATCH] Polish transcript event widgets Co-Authored-By: Paperclip --- .../transcript/RunTranscriptView.tsx | 340 +++++++++++++----- ui/src/pages/RunTranscriptUxLab.tsx | 14 +- 2 files changed, 253 insertions(+), 101 deletions(-) diff --git a/ui/src/components/transcript/RunTranscriptView.tsx b/ui/src/components/transcript/RunTranscriptView.tsx index 05defa14..77dbbea0 100644 --- a/ui/src/components/transcript/RunTranscriptView.tsx +++ b/ui/src/components/transcript/RunTranscriptView.tsx @@ -1,8 +1,9 @@ -import { useEffect, useMemo, useState, type ReactNode } from "react"; +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, @@ -49,6 +50,13 @@ type TranscriptBlock = isError?: boolean; status: "running" | "completed" | "error"; } + | { + type: "activity"; + ts: string; + activityId?: string; + name: string; + status: "running" | "completed"; + } | { type: "event"; ts: string; @@ -81,6 +89,21 @@ function stripMarkdown(value: string): string { ); } +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 ""; @@ -132,13 +155,25 @@ function summarizeRecord(record: Record, keys: string[]): strin function summarizeToolInput(name: string, input: unknown, density: TranscriptDensity): string { const compactMax = density === "compact" ? 72 : 120; - if (typeof input === "string") return truncate(compactWhitespace(input), compactMax); + 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"]) @@ -158,8 +193,61 @@ function summarizeToolInput(name: string, input: unknown, density: TranscriptDen 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)) @@ -168,9 +256,20 @@ function summarizeToolResult(result: string | undefined, isError: boolean | unde 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]; @@ -214,7 +313,7 @@ function normalizeTranscript(entries: TranscriptEntry[], streaming: boolean): Tr const toolBlock: Extract = { type: "tool", ts: entry.ts, - name: entry.name, + name: displayToolName(entry.name, entry.input), toolUseId: entry.toolUseId ?? extractToolUseId(entry.input), input: entry.input, status: "running", @@ -259,25 +358,18 @@ function normalizeTranscript(entries: TranscriptEntry[], streaming: boolean): Tr ts: entry.ts, label: "init", tone: "info", - text: `Model ${entry.model}${entry.sessionId ? ` • session ${entry.sessionId}` : ""}`, + text: `model ${entry.model}${entry.sessionId ? ` • session ${entry.sessionId}` : ""}`, }); continue; } if (entry.kind === "result") { - const summary = `tokens in ${formatTokens(entry.inputTokens)} • out ${formatTokens(entry.outputTokens)} • cached ${formatTokens(entry.cachedTokens)} • $${entry.costUsd.toFixed(6)}`; - const detailParts = [ - entry.text.trim(), - entry.subtype ? `subtype=${entry.subtype}` : "", - entry.errors.length > 0 ? `errors=${entry.errors.join(" | ")}` : "", - ].filter(Boolean); blocks.push({ type: "event", ts: entry.ts, label: "result", tone: entry.isError ? "error" : "info", - text: summary, - detail: detailParts.join("\n\n") || undefined, + text: entry.text.trim() || entry.errors[0] || (entry.isError ? "Run failed" : "Completed"), }); continue; } @@ -294,6 +386,33 @@ function normalizeTranscript(entries: TranscriptEntry[], streaming: boolean): Tr } 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, @@ -316,40 +435,6 @@ function normalizeTranscript(entries: TranscriptEntry[], streaming: boolean): Tr return blocks; } -function TranscriptDisclosure({ - defaultOpen, - children, -}: { - defaultOpen: boolean; - children: ReactNode; -}) { - const [open, setOpen] = useState(defaultOpen); - const [touched, setTouched] = useState(false); - - useEffect(() => { - if (!touched) { - setOpen(defaultOpen); - } - }, [defaultOpen, touched]); - - return ( -
- - {open &&
{children}
} -
- ); -} - function TranscriptMessageBlock({ block, density, @@ -416,7 +501,11 @@ function TranscriptToolCard({ 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" @@ -433,59 +522,129 @@ function TranscriptToolCard({ "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} - - {statusLabel} - - {block.toolUseId && ( - - {truncate(block.toolUseId, compact ? 24 : 40)} + {!commandTool && ( + + {statusLabel} )}
-
- {block.status === "running" - ? summarizeToolInput(block.name, block.input, density) - : summarizeToolResult(block.result, block.isError, density)} +
+ {commandTool ? ( + + {summary} + + ) : ( + + {summary} + + )}
+ {commandTool && block.status !== "running" && ( +
+ {block.status === "error" + ? "Command failed" + : parsedResult?.status === "completed" + ? "Command completed" + : statusLabel} +
+ )}
+
- -
-
-
-
- Input + {open && ( +
+
+
+
+
+ Input +
+
+                  {formatToolPayload(block.input) || ""}
+                
-
-                {formatToolPayload(block.input) || ""}
-              
-
-
-
- Result +
+
+ Result +
+
+                  {block.result ? formatToolPayload(block.result) : "Waiting for result..."}
+                
-
-                {block.result ? formatToolPayload(block.result) : "Waiting for result..."}
-              
- + )} +
+ ); +} + +function TranscriptActivityRow({ + block, + density, +}: { + block: Extract; + density: TranscriptDensity; +}) { + return ( +
+ {block.status === "completed" ? ( + + ) : ( + + + + + )} +
+ {block.name} +
); } @@ -518,14 +677,18 @@ function TranscriptEventRow({ )}
-
- - {block.label} - -
-
- {block.text} -
+ {block.label === "result" && block.tone !== "error" ? ( +
+ {block.text} +
+ ) : ( +
+ + {block.label} + + {block.text ? {block.text} : null} +
+ )} {block.detail && (
               {block.detail}
@@ -614,6 +777,7 @@ export function RunTranscriptView({
           {block.type === "message" && }
           {block.type === "thinking" && }
           {block.type === "tool" && }
+          {block.type === "activity" && }
           {block.type === "event" && }
         
))} diff --git a/ui/src/pages/RunTranscriptUxLab.tsx b/ui/src/pages/RunTranscriptUxLab.tsx index 14556482..3885431a 100644 --- a/ui/src/pages/RunTranscriptUxLab.tsx +++ b/ui/src/pages/RunTranscriptUxLab.tsx @@ -6,7 +6,7 @@ import { Identity } from "../components/Identity"; import { StatusBadge } from "../components/StatusBadge"; import { RunTranscriptView, type TranscriptDensity, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { runTranscriptFixtureEntries, runTranscriptFixtureMeta } from "../fixtures/runTranscriptFixtures"; -import { ExternalLink, FlaskConical, LayoutPanelLeft, MonitorCog, PanelsTopLeft, RadioTower, ShieldCheck } from "lucide-react"; +import { ExternalLink, FlaskConical, LayoutPanelLeft, MonitorCog, PanelsTopLeft, RadioTower } from "lucide-react"; type SurfaceId = "detail" | "live" | "dashboard"; @@ -247,18 +247,6 @@ export function RunTranscriptUxLab() { ); })}
- -
-
- - Sanitization rules -
-
    -
  • No real home-directory segments or workstation names.
  • -
  • No API keys, env var payloads, or secret-bearing commands.
  • -
  • Only safe, representative command/result excerpts remain.
  • -
-