import { Bot, Check, Clipboard, FilePenLine, FileText, Folder, Globe, Lightbulb, Pencil, Search, Terminal, Workflow, Wrench, X, } from "lucide-react"; import { Fragment, type ReactNode, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; import type { WebLiveMessage, WebMessagePart, WebSnapshot, } from "../../../../protocol/types.ts"; import { Markdown } from "../../components/Markdown.tsx"; import { copyText } from "../../lib/clipboard.ts"; import { compactSummary, formatElapsedMs, formatTurnTime, turnTitle, } from "../../lib/format.ts"; import type { LiveEntry } from "../../store/web-store.ts"; type PersistedEntry = NonNullable< WebSnapshot["selectedSession"] >["entries"][number]; interface DisplayEntry { key: string; timestamp?: string; message: WebLiveMessage; } interface TranscriptProps { snapshot: WebSnapshot; liveMessages: LiveEntry[]; liveRunning: boolean; livePhase: "idle" | "preparing" | "running"; liveRetry: { attempt: number; maxAttempts: number } | null; thinkingStarts: Record; thinkingDurations: Record; scrollToBottom: number; onResend: (content: string) => Promise; } type Status = "running" | "done" | "error" | "warn" | "unknown"; interface RenderRow { key: string; content: ReactNode; groupable?: boolean; error?: boolean; icon?: ReactNode; } function record(value: unknown): Record { return value && typeof value === "object" ? (value as Record) : {}; } function parseArguments(raw: string) { try { return record(JSON.parse(raw)); } catch { return {}; } } function canonicalStatus(value: unknown): Status { if (value === "running") return "running"; if (value === "done" || value === "completed") return "done"; if ( ["error", "failed", "aborted", "killed", "timed_out"].includes( String(value), ) ) { return "error"; } if (value === "uncertain") return "warn"; return "unknown"; } function resultStatus(message?: WebLiveMessage): Status { if (!message) return "running"; if (message.isError) return "error"; const status = canonicalStatus(record(message.details).status); if (status !== "unknown") return status; return message.isError === false ? "done" : "unknown"; } function StatusMark({ status }: { status: Status }) { if (status === "running") { return ( ); } if (status === "done") return ; if (status === "error") return ; if (status === "warn") return ( ? ); return null; } function iconForTool(name: string) { const lowered = name.toLowerCase(); if (lowered === "bash") return ; if (lowered === "read") return ; if (lowered === "write" || lowered === "edit") return ; if (lowered === "grep") return ; if (lowered === "glob" || lowered === "ls") return ; if (lowered === "webfetch" || lowered === "websearch") return ; return ; } function toolSummary(name: string, args: Record) { const value = name === "bash" ? args.command : ["read", "write", "edit", "ls"].includes(name) ? args.path : ["grep", "glob"].includes(name) ? args.pattern : name === "webfetch" ? args.url : name === "websearch" ? args.query : ""; return typeof value === "string" ? compactSummary(value.split("\n").find(Boolean), 90) : ""; } function EvidenceDetails({ body, icon, name, status, summary, thinking = false, }: { body: string; icon: ReactNode; name: string; status: Status; summary?: string; thinking?: boolean; }) { return (
{body}
); } function ActivityCard({ body, family, meta, status, title, }: { body: string; family: "subagent" | "workflow"; meta?: string; status: Status; title: string; }) { return (
{title} {meta && {meta}}
{body}
); } function familyCard( part: Extract, result?: WebLiveMessage, ) { const name = part.name || ""; const args = parseArguments(part.arguments); const details = record(result?.details); const status = resultStatus(result); if (name === "subagent_spawn") { const meta = [args.agent_type, args.model, args.working_dir] .filter(Boolean) .join(" · "); return ( ); } if (name.startsWith("subagent")) { const action = name.replaceAll("_", " ").replace(/^subagent /u, ""); return ( ); } if (name === "workflow") { const script = typeof args.script === "string" ? args.script : part.arguments; const workflowName = String( details.name || script.match(/\bname:\s*["'`]([^"'`]+)["'`]/u)?.[1] || "unnamed", ); const agents = record(details.agents); const meta = [ details.runId, details.status, agents.total ? `${Number(agents.total) - Number(agents.running || 0)}/${agents.total} agents` : "", ] .filter(Boolean) .join(" · "); return ( ); } if (name.startsWith("workflow")) { return ( ); } return null; } function useElapsed(start: number | undefined, active: boolean) { const [now, setNow] = useState(Date.now()); useEffect(() => { if (!active) return; const interval = window.setInterval(() => setNow(Date.now()), 1_000); return () => window.clearInterval(interval); }, [active]); return start ? formatElapsedMs(start, active ? now : Date.now()) : ""; } function ThinkingEvidence({ body, start, duration, active, }: { body: string; start?: number; duration?: number; active: boolean; }) { const { t } = useTranslation(); const elapsed = useElapsed(start, active); const settled = duration ? formatElapsedMs(0, duration) : elapsed; return ( } name={active ? t("thinkingActive") : t("thinkingDone")} status={active ? "running" : "done"} summary={settled ? `· ${settled}` : undefined} thinking /> ); } function MessageActions({ content, editable, timestamp, onResend, }: { content: string; editable: boolean; timestamp?: string; onResend: (value: string) => Promise; }) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const [copyFailed, setCopyFailed] = useState(false); const [copying, setCopying] = useState(false); const copyGeneration = useRef(0); const copyTimer = useRef(undefined); useEffect( () => () => { copyGeneration.current += 1; window.clearTimeout(copyTimer.current); }, [], ); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(content); const editInput = useRef(null); useEffect(() => { if (editing) editInput.current?.focus(); }, [editing]); if (editing) { return (