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 (
);
}
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 && (
)}
);
})}
);
}