import React, { useState, useMemo } from "react"; import type { ParsedMessage } from "../lib/types"; import { parseAgentEvents, summarizeToolCall, stripLineNumbers, type AgentEvent, type ParsedAgentProgress, } from "../lib/agent-progress-parser"; import { renderMarkdown } from "../lib/markdown"; interface Props { events: ParsedMessage[]; } // ── Helpers ──────────────────────────────────────────────── function formatTime(ts?: string): string { if (!ts) return ""; const d = new Date(ts); if (isNaN(d.getTime())) return ""; return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", }); } function formatTimeRange(first?: string, last?: string): string { if (!first) return ""; const f = formatTime(first); const l = formatTime(last); if (!l || f === l) return f; return `${f}\u2013${l}`; } function shortToolName(name: string): string { if (name.startsWith("mcp__morph-mcp__")) { const short = name.slice("mcp__morph-mcp__".length); if (short === "warpgrep_codebase_search") return "WarpGrep"; if (short === "edit_file") return "FastEdit"; return short; } if (name.startsWith("mcp__")) { const parts = name.split("__"); return parts[parts.length - 1] || name; } return name; } // ── SVG Icons (16x16) ───────────────────────────────────── function IconFile() { return ( ); } function IconSearch() { return ( ); } function IconFolder() { return ( ); } function IconTerminal() { return ( ); } function IconPencil() { return ( ); } function IconAgent() { return ( ); } function IconGlobe() { return ( ); } function IconWrench() { return ( ); } function IconCheck() { return ( ); } function IconChat() { return ( ); } function IconNote() { return ( ); } // ── Icon selection ──────────────────────────────────────── function ToolIcon({ name }: { name: string }) { const short = shortToolName(name); switch (short) { case "Read": return ; case "Grep": case "WarpGrep": return ; case "Glob": return ; case "Bash": return ; case "Write": case "Edit": case "FastEdit": return ; case "Task": return ; case "WebFetch": case "WebSearch": return ; default: return ; } } function EventIcon({ event }: { event: AgentEvent }) { switch (event.kind) { case "tool_call": return ; case "tool_result": return ; case "text_response": return ; case "user_text": return ; case "raw_content": return ; } } // ── Summary row label ───────────────────────────────────── function summaryLabel(event: AgentEvent): string { switch (event.kind) { case "tool_call": return summarizeToolCall(event.toolName, event.input); case "tool_result": return `Result (${event.content.length.toLocaleString()} chars)`; case "text_response": return `Text response (${event.lineCount} lines)`; case "user_text": return event.text.length > 80 ? event.text.slice(0, 79) + "\u2026" : event.text; case "raw_content": return event.content.length > 60 ? event.content.slice(0, 59) + "\u2026" : event.content; } } function summaryToolName(event: AgentEvent): string { if (event.kind === "tool_call") return shortToolName(event.toolName); if (event.kind === "tool_result") return "Result"; if (event.kind === "text_response") return "Response"; if (event.kind === "user_text") return "Prompt"; return "Raw"; } // ── Drill-down content ──────────────────────────────────── // All content originates from local JSONL session files owned by the user. // Same trust model as MessageBubble's markdown rendering. // This is a local-only developer tool, not exposed to untrusted input. function RenderedMarkdown({ content, label }: { content: string; label?: string }) { const html = useMemo(() => renderMarkdown(content), [content]); return (
{label && (
{label}
)}
); } function DrillDown({ event }: { event: AgentEvent }) { if (event.kind === "tool_call") { const jsonBlock = "```json\n" + JSON.stringify(event.input, null, 2) + "\n```"; return ; } if (event.kind === "tool_result") { // Strip cat-n line number prefixes so hljs can detect syntax, // then wrap in a language-tagged code fence for highlighting. const stripped = stripLineNumbers(event.content); const lang = event.language || ""; const wrapped = "```" + lang + "\n" + stripped + "\n```"; return ; } if (event.kind === "text_response") { return ; } if (event.kind === "user_text") { return ; } // raw_content return ; } // ── Main component ──────────────────────────────────────── export function AgentProgressView({ events }: Props) { const [expandedIndex, setExpandedIndex] = useState(null); const parsed: ParsedAgentProgress = useMemo( () => parseAgentEvents(events), [events] ); const promptPreview = parsed.prompt ? parsed.prompt.length > 100 ? parsed.prompt.slice(0, 99) + "\u2026" : parsed.prompt : null; return (
{/* Header */}
{promptPreview && (
“{promptPreview}”
)}
Agent {parsed.agentId || "unknown"} {" \u00B7 "} {parsed.turnCount} turn{parsed.turnCount !== 1 ? "s" : ""} {parsed.firstTimestamp && ( <> {" \u00B7 "} {formatTimeRange(parsed.firstTimestamp, parsed.lastTimestamp)} )}
{/* Activity feed */}
{parsed.events.map((event, i) => { const isExpanded = expandedIndex === i; return (
{isExpanded && (
)}
); })}
); }