/** * Agent Dashboard — Unified observability across all agent interfaces * * Passively tracks agent activity from team dispatches, subagent spawns, * and chain pipeline runs. Provides a compact always-visible widget plus * a full-screen overlay with four switchable views. * * Hooks into: dispatch_agent, subagent_create, subagent_continue, run_chain * tool calls and their completions. Completely passive — never blocks. * * Commands: * /dashboard — toggle full-screen overlay * /dashboard clear — reset all tracked state * * Usage: pi -e extensions/agent-dashboard.ts */ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { DynamicBorder } from "@mariozechner/pi-coding-agent"; import { Container, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui"; import { applyExtensionDefaults } from "./themeMap.ts"; // ── Data Types ───────────────────────────────────────────────────────── type AgentInterface = "team" | "subagent" | "chain"; interface TrackedAgent { id: string; name: string; iface: AgentInterface; status: "running" | "done" | "error"; task: string; startedAt: number; endedAt?: number; elapsed: number; toolCount: number; lastText: string; turnCount: number; chainStep?: number; chainName?: string; teamName?: string; } interface AgentRun { id: string; name: string; iface: AgentInterface; task: string; status: "done" | "error"; startedAt: number; endedAt: number; duration: number; toolCount: number; resultPreview: string; chainStep?: number; chainName?: string; teamName?: string; } interface DashboardStats { totalRuns: number; totalSuccess: number; totalError: number; totalDuration: number; agentRunCounts: Record; ifaceCounts: Record; } // ── Helpers ──────────────────────────────────────────────────────────── function fmtDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const secs = Math.floor(ms / 1000); if (secs < 60) return `${secs}s`; const mins = Math.floor(secs / 60); const remSecs = secs % 60; if (mins < 60) return `${mins}m ${remSecs}s`; const hrs = Math.floor(mins / 60); const remMins = mins % 60; return `${hrs}h ${remMins}m`; } function shortId(): string { return Math.random().toString(36).slice(2, 6); } function truncate(s: string, max: number): string { return s.length > max ? s.slice(0, max - 1) + "…" : s; } function emptyStats(): DashboardStats { return { totalRuns: 0, totalSuccess: 0, totalError: 0, totalDuration: 0, agentRunCounts: {}, ifaceCounts: { team: 0, subagent: 0, chain: 0 }, }; } // ── Extension ────────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { // ── State ────────────────────────────────────────────────────────── const activeAgents: Map = new Map(); let history: AgentRun[] = []; let stats: DashboardStats = emptyStats(); let widgetCtx: ExtensionContext | null = null; let tickTimer: ReturnType | null = null; // Mapping from toolCallId → tracked agent info (with timestamp for staleness) const pendingCalls: Map = new Map(); // Staleness threshold: 10 minutes const STALE_TIMEOUT_MS = 10 * 60 * 1000; // Inactivity auto-stop: stop tick after 30s with no active agents let lastActivityTs = Date.now(); // ── Tracked tool names ───────────────────────────────────────────── const TRACKED_TOOLS = new Set([ "dispatch_agent", "subagent_create", "subagent_continue", "run_chain", ]); // ── State Management ─────────────────────────────────────────────── function clearState() { activeAgents.clear(); history = []; stats = emptyStats(); pendingCalls.clear(); lastActivityTs = Date.now(); } function addToHistory(agent: TrackedAgent) { const run: AgentRun = { id: agent.id, name: agent.name, iface: agent.iface, task: agent.task, status: agent.status === "error" ? "error" : "done", startedAt: agent.startedAt, endedAt: agent.endedAt || Date.now(), duration: agent.elapsed, toolCount: agent.toolCount, resultPreview: truncate(agent.lastText, 200), chainStep: agent.chainStep, chainName: agent.chainName, teamName: agent.teamName, }; history.push(run); // Ring buffer capped at 200 if (history.length > 200) { history = history.slice(-200); } // Update stats stats.totalRuns++; if (run.status === "done") stats.totalSuccess++; else stats.totalError++; stats.totalDuration += run.duration; stats.agentRunCounts[run.name] = (stats.agentRunCounts[run.name] || 0) + 1; stats.ifaceCounts[run.iface] = (stats.ifaceCounts[run.iface] || 0) + 1; } // ── Tick Timer ───────────────────────────────────────────────────── function startTick() { if (tickTimer) return; tickTimer = setInterval(() => { const now = Date.now(); // Update elapsed on running agents for (const agent of activeAgents.values()) { if (agent.status === "running") { agent.elapsed = now - agent.startedAt; } } // Staleness check: expire pending calls older than 10 minutes for (const [callId, pending] of pendingCalls) { if (now - pending.ts > STALE_TIMEOUT_MS) { pendingCalls.delete(callId); const agent = activeAgents.get(pending.agentId); if (agent && agent.status === "running") { agent.status = "error"; agent.endedAt = now; agent.elapsed = now - agent.startedAt; agent.lastText = "Timed out (no completion after 10m)"; addToHistory(agent); activeAgents.delete(pending.agentId); } } } // Auto-stop tick after 30s of inactivity (no active agents, no pending calls) if (activeAgents.size === 0 && pendingCalls.size === 0) { if (now - lastActivityTs > 30_000) { stopTick(); return; } } else { lastActivityTs = now; } updateWidget(); }, 1000); } function stopTick() { if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } } // ── Widget Rendering ─────────────────────────────────────────────── function updateWidget() { if (!widgetCtx) return; try { widgetCtx.ui.setWidget("agent-dashboard", (_tui, theme) => { const container = new Container(); const borderFn = (s: string) => theme.fg("accent", s); container.addChild(new DynamicBorder(borderFn)); const headerText = new Text("", 1, 0); container.addChild(headerText); const agentLines: Text[] = []; // Pre-allocate up to 4 lines for active agents for (let i = 0; i < 4; i++) { const t = new Text("", 1, 0); agentLines.push(t); container.addChild(t); } const hintText = new Text("", 1, 0); container.addChild(hintText); container.addChild(new DynamicBorder(borderFn)); return { render(width: number): string[] { const activeCount = activeAgents.size; const doneCount = stats.totalSuccess; const errorCount = stats.totalError; // Line 1: summary bar const line1 = theme.fg("accent", " 📊 Dashboard") + theme.fg("dim", " │ Active: ") + theme.fg(activeCount > 0 ? "accent" : "muted", `${activeCount}`) + theme.fg("dim", " │ Done: ") + theme.fg("success", `${doneCount}`) + theme.fg("dim", " │ Errors: ") + theme.fg(errorCount > 0 ? "error" : "muted", `${errorCount}`); headerText.setText(truncateToWidth(line1, width - 4)); // Active agent lines const agents = Array.from(activeAgents.values()); for (let i = 0; i < agentLines.length; i++) { if (i < agents.length) { const a = agents[i]; const icon = a.status === "running" ? "⟳" : a.status === "done" ? "✓" : "✗"; const statusColor = a.status === "running" ? "accent" : a.status === "done" ? "success" : "error"; const ifaceTag = theme.fg("dim", `[${a.iface}]`); const elapsed = theme.fg("muted", fmtDuration(a.elapsed)); const tools = theme.fg("dim", `🔧${a.toolCount}`); const lastText = a.lastText ? theme.fg("muted", truncate(a.lastText, Math.max(20, width - 60))) : ""; const line = " " + theme.fg(statusColor, icon) + " " + theme.fg("accent", truncate(a.name, 16)) + " " + ifaceTag + " " + elapsed + " " + tools + (lastText ? theme.fg("dim", " │ ") + lastText : ""); agentLines[i].setText(truncateToWidth(line, width - 4)); } else { agentLines[i].setText(""); } } // Hint line const hintLine = theme.fg("dim", " /dashboard") + theme.fg("muted", " — full view") + theme.fg("dim", " │ ") + theme.fg("muted", `${stats.totalRuns} total runs`) + (stats.totalDuration > 0 ? theme.fg("dim", " │ avg ") + theme.fg("muted", fmtDuration(Math.round(stats.totalDuration / Math.max(1, stats.totalRuns)))) : ""); hintText.setText(truncateToWidth(hintLine, width - 4)); return container.render(width); }, invalidate() { container.invalidate(); }, }; }); } catch {} } // ── Overlay ──────────────────────────────────────────────────────── async function openOverlay(ctx: ExtensionContext) { if (!ctx.hasUI) return; let currentView = 0; // 0=Live, 1=History, 2=Interfaces, 3=Stats let scrollOffset = 0; const viewNames = ["1:Live", "2:History", "3:Interfaces", "4:Stats"]; await ctx.ui.custom((_tui, theme, _kb, done) => { return { render(width: number): string[] { const lines: string[] = []; // ── Header ── lines.push(""); const tabs = viewNames.map((name, i) => i === currentView ? theme.fg("accent", theme.bold(`[${name}]`)) : theme.fg("dim", `[${name}]`) ).join(" "); lines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("📊 Agent Dashboard")) + " ".repeat(Math.max(1, width - 20 - viewNames.join(" ").length - 2)) + tabs, width, )); lines.push(theme.fg("dim", "─".repeat(width))); // ── View content ── const contentLines = renderView(currentView, width, theme, scrollOffset); lines.push(...contentLines); // ── Footer controls ── lines.push(""); lines.push(theme.fg("dim", "─".repeat(width))); lines.push(truncateToWidth( " " + theme.fg("dim", "1-4/Tab: views │ j/k: scroll │ c: clear │ q/Esc: close"), width, )); lines.push(""); return lines; }, handleInput(data: string) { if (matchesKey(data, "escape") || data === "q") { done(undefined); return; } if (data === "1") { currentView = 0; scrollOffset = 0; } else if (data === "2") { currentView = 1; scrollOffset = 0; } else if (data === "3") { currentView = 2; scrollOffset = 0; } else if (data === "4") { currentView = 3; scrollOffset = 0; } else if (data === "\t") { currentView = (currentView + 1) % 4; scrollOffset = 0; } else if (matchesKey(data, "up") || data === "k") { scrollOffset = Math.max(0, scrollOffset - 1); } else if (matchesKey(data, "down") || data === "j") { scrollOffset++; } else if (matchesKey(data, "pageUp")) { scrollOffset = Math.max(0, scrollOffset - 20); } else if (matchesKey(data, "pageDown")) { scrollOffset += 20; } else if (data === "c") { clearState(); scrollOffset = 0; } _tui.requestRender(); }, invalidate() {}, }; }, { overlay: true, overlayOptions: { width: "90%", anchor: "center" }, }); } // ── View Renderers ───────────────────────────────────────────────── function renderView(view: number, width: number, theme: any, offset: number): string[] { switch (view) { case 0: return renderLiveView(width, theme, offset); case 1: return renderHistoryView(width, theme, offset); case 2: return renderInterfacesView(width, theme, offset); case 3: return renderStatsView(width, theme, offset); default: return []; } } // ── View 1: Live ─────────────────────────────────────────────────── function renderLiveView(width: number, theme: any, offset: number): string[] { const lines: string[] = []; const agents = Array.from(activeAgents.values()); lines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Active Agents")) + theme.fg("dim", ` (${agents.length} running)`), width, )); lines.push(""); if (agents.length === 0) { lines.push(truncateToWidth( " " + theme.fg("dim", "No agents currently running. Activity will appear here when"), width, )); lines.push(truncateToWidth( " " + theme.fg("dim", "dispatch_agent, subagent_create, subagent_continue, or run_chain is called."), width, )); lines.push(""); // Show recent completions as context if (history.length > 0) { lines.push(truncateToWidth( " " + theme.fg("muted", `Last completed: ${history.length} agents`), width, )); const recent = history.slice(-3).reverse(); for (const run of recent) { const icon = run.status === "done" ? "✓" : "✗"; const color = run.status === "done" ? "success" : "error"; lines.push(truncateToWidth( " " + theme.fg(color, `${icon} ${run.name}`) + theme.fg("dim", ` [${run.iface}] `) + theme.fg("muted", fmtDuration(run.duration)) + theme.fg("dim", " — ") + theme.fg("muted", truncate(run.task, 50)), width, )); } } return lines; } const allLines: string[] = []; for (const agent of agents) { const icon = agent.status === "running" ? "●" : agent.status === "done" ? "✓" : "✗"; const statusColor = agent.status === "running" ? "accent" : agent.status === "done" ? "success" : "error"; // Card top allLines.push(truncateToWidth( " " + theme.fg("dim", "┌─ ") + theme.fg(statusColor, `${icon} ${agent.name}`) + theme.fg("dim", ` [${agent.iface}]`) + (agent.chainName ? theme.fg("dim", ` chain:${agent.chainName}`) : "") + (agent.teamName ? theme.fg("dim", ` team:${agent.teamName}`) : "") + (agent.chainStep !== undefined ? theme.fg("dim", ` step:${agent.chainStep}`) : "") + theme.fg("dim", " ─".repeat(Math.max(0, Math.floor((width - 50) / 2)))), width, )); // Task allLines.push(truncateToWidth( " " + theme.fg("dim", "│ ") + theme.fg("muted", "Task: ") + theme.fg("accent", truncate(agent.task, width - 20)), width, )); // Metrics allLines.push(truncateToWidth( " " + theme.fg("dim", "│ ") + theme.fg("muted", "Elapsed: ") + theme.fg("success", fmtDuration(agent.elapsed)) + theme.fg("dim", " │ ") + theme.fg("muted", "Tools: ") + theme.fg("accent", `${agent.toolCount}`) + theme.fg("dim", " │ ") + theme.fg("muted", "Turns: ") + theme.fg("accent", `${agent.turnCount}`), width, )); // Streaming text if (agent.lastText) { allLines.push(truncateToWidth( " " + theme.fg("dim", "│ ") + theme.fg("muted", truncate(agent.lastText, width - 10)), width, )); } // Card bottom allLines.push(truncateToWidth( " " + theme.fg("dim", "└" + "─".repeat(Math.max(0, width - 5))), width, )); allLines.push(""); } const visible = allLines.slice(offset); lines.push(...visible); return lines; } // ── View 2: History ──────────────────────────────────────────────── function renderHistoryView(width: number, theme: any, offset: number): string[] { const lines: string[] = []; lines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Completed Runs")) + theme.fg("dim", ` (${history.length} total)`), width, )); lines.push(""); if (history.length === 0) { lines.push(truncateToWidth(" " + theme.fg("dim", "No completed runs yet."), width)); return lines; } // Table header const hdr = theme.fg("accent", " Status") + theme.fg("accent", " │ Name ") + theme.fg("accent", " │ Interface ") + theme.fg("accent", " │ Duration ") + theme.fg("accent", " │ Tools ") + theme.fg("accent", " │ Task"); lines.push(truncateToWidth(hdr, width)); lines.push(truncateToWidth(" " + theme.fg("dim", "─".repeat(Math.min(80, width - 4))), width)); // Show newest first const rows: string[] = []; const reversed = [...history].reverse(); for (const run of reversed) { const icon = run.status === "done" ? "✓" : "✗"; const color = run.status === "done" ? "success" : "error"; const ifaceLabel = run.iface.padEnd(9); const nameLabel = truncate(run.name, 14).padEnd(14); const durLabel = fmtDuration(run.duration).padEnd(8); const toolLabel = String(run.toolCount).padStart(5); const taskPreview = truncate(run.task, Math.max(10, width - 70)); const row = " " + theme.fg(color, ` ${icon} `) + theme.fg("dim", " │ ") + theme.fg("accent", nameLabel) + theme.fg("dim", " │ ") + theme.fg("muted", ifaceLabel) + theme.fg("dim", " │ ") + theme.fg("success", durLabel) + theme.fg("dim", " │ ") + theme.fg("accent", toolLabel) + theme.fg("dim", " │ ") + theme.fg("muted", taskPreview); rows.push(row); } const visible = rows.slice(offset); for (const row of visible) { lines.push(truncateToWidth(row, width)); } return lines; } // ── View 3: Interfaces ───────────────────────────────────────────── function renderInterfacesView(width: number, theme: any, offset: number): string[] { const lines: string[] = []; lines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Agents by Interface")), width, )); lines.push(""); const ifaceLabels: Record = { team: "🏢 Team (dispatch_agent)", subagent: "🤖 Subagent (subagent_create/continue)", chain: "🔗 Chain (run_chain)", }; const allLines: string[] = []; for (const iface of ["team", "subagent", "chain"] as AgentInterface[]) { const activeForIface = Array.from(activeAgents.values()).filter(a => a.iface === iface); const historyForIface = history.filter(r => r.iface === iface); const totalCount = stats.ifaceCounts[iface] || 0; allLines.push(truncateToWidth( " " + theme.fg("accent", theme.bold(ifaceLabels[iface])) + theme.fg("dim", ` — ${activeForIface.length} active, ${totalCount} completed`), width, )); allLines.push(truncateToWidth(" " + theme.fg("dim", "─".repeat(Math.min(60, width - 6))), width)); // Active if (activeForIface.length > 0) { for (const agent of activeForIface) { allLines.push(truncateToWidth( " " + theme.fg("accent", "● ") + theme.fg("accent", agent.name) + theme.fg("dim", " — ") + theme.fg("success", fmtDuration(agent.elapsed)) + theme.fg("dim", " │ 🔧") + theme.fg("muted", `${agent.toolCount}`) + theme.fg("dim", " │ ") + theme.fg("muted", truncate(agent.task, 40)), width, )); } } // Recent completed (last 5) const recent = historyForIface.slice(-5).reverse(); if (recent.length > 0) { for (const run of recent) { const icon = run.status === "done" ? "✓" : "✗"; const color = run.status === "done" ? "success" : "error"; allLines.push(truncateToWidth( " " + theme.fg(color, `${icon} `) + theme.fg("muted", run.name) + theme.fg("dim", " — ") + theme.fg("muted", fmtDuration(run.duration)) + theme.fg("dim", " │ ") + theme.fg("muted", truncate(run.task, 40)), width, )); } } if (activeForIface.length === 0 && recent.length === 0) { allLines.push(truncateToWidth(" " + theme.fg("dim", "No activity recorded."), width)); } allLines.push(""); } const visible = allLines.slice(offset); lines.push(...visible); return lines; } // ── View 4: Stats ────────────────────────────────────────────────── function renderStatsView(width: number, theme: any, offset: number): string[] { const lines: string[] = []; lines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Aggregate Statistics")), width, )); lines.push(""); const avgDur = stats.totalRuns > 0 ? fmtDuration(Math.round(stats.totalDuration / stats.totalRuns)) : "—"; const successRate = stats.totalRuns > 0 ? `${Math.round((stats.totalSuccess / stats.totalRuns) * 100)}%` : "—"; const allLines: string[] = []; // Summary cards allLines.push(truncateToWidth( " " + theme.fg("muted", "Total Runs: ") + theme.fg("accent", `${stats.totalRuns}`) + theme.fg("dim", " │ ") + theme.fg("muted", "Success: ") + theme.fg("success", `${stats.totalSuccess}`) + theme.fg("dim", " │ ") + theme.fg("muted", "Errors: ") + theme.fg(stats.totalError > 0 ? "error" : "muted", `${stats.totalError}`) + theme.fg("dim", " │ ") + theme.fg("muted", "Success Rate: ") + theme.fg("success", successRate), width, )); allLines.push(truncateToWidth( " " + theme.fg("muted", "Total Duration: ") + theme.fg("success", fmtDuration(stats.totalDuration)) + theme.fg("dim", " │ ") + theme.fg("muted", "Avg Duration: ") + theme.fg("accent", avgDur), width, )); allLines.push(""); // Interface breakdown allLines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Interface Breakdown")), width, )); allLines.push(""); const ifaceTotal = Math.max(1, stats.ifaceCounts.team + stats.ifaceCounts.subagent + stats.ifaceCounts.chain); const barWidth = Math.min(30, Math.floor(width * 0.3)); for (const [iface, label] of [["team", "Team "], ["subagent", "Subagent "], ["chain", "Chain "]] as [AgentInterface, string][]) { const count = stats.ifaceCounts[iface] || 0; const ratio = count / ifaceTotal; const filled = Math.round(ratio * barWidth); const bar = "█".repeat(filled) + "░".repeat(barWidth - filled); allLines.push(truncateToWidth( " " + theme.fg("accent", label) + " " + theme.fg("success", bar) + " " + theme.fg("muted", `${count}`) + theme.fg("dim", ` (${Math.round(ratio * 100)}%)`), width, )); } allLines.push(""); // Most-used agents bar chart allLines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Most-Used Agents")), width, )); allLines.push(""); const agentEntries = Object.entries(stats.agentRunCounts).sort((a, b) => b[1] - a[1]); if (agentEntries.length === 0) { allLines.push(truncateToWidth(" " + theme.fg("dim", "No agent runs recorded yet."), width)); } else { const maxCount = agentEntries[0][1]; for (const [name, count] of agentEntries.slice(0, 15)) { const ratio = maxCount > 0 ? count / maxCount : 0; const filled = Math.round(ratio * barWidth); const bar = "█".repeat(filled) + "░".repeat(barWidth - filled); allLines.push(truncateToWidth( " " + theme.fg("accent", name.padEnd(16)) + " " + theme.fg("success", bar) + " " + theme.fg("muted", `${count}`), width, )); } } allLines.push(""); // Per-agent average durations if (history.length > 0) { allLines.push(truncateToWidth( " " + theme.fg("accent", theme.bold("Average Duration by Agent")), width, )); allLines.push(""); const durByAgent: Record = {}; for (const run of history) { if (!durByAgent[run.name]) durByAgent[run.name] = []; durByAgent[run.name].push(run.duration); } const durEntries = Object.entries(durByAgent).sort((a, b) => { const avgA = a[1].reduce((s, v) => s + v, 0) / a[1].length; const avgB = b[1].reduce((s, v) => s + v, 0) / b[1].length; return avgB - avgA; }); for (const [name, durations] of durEntries) { const avg = durations.reduce((s, v) => s + v, 0) / durations.length; const min = Math.min(...durations); const max = Math.max(...durations); allLines.push(truncateToWidth( " " + theme.fg("accent", name.padEnd(16)) + theme.fg("dim", " avg: ") + theme.fg("success", fmtDuration(Math.round(avg)).padEnd(8)) + theme.fg("dim", " min: ") + theme.fg("muted", fmtDuration(min).padEnd(8)) + theme.fg("dim", " max: ") + theme.fg("muted", fmtDuration(max).padEnd(8)) + theme.fg("dim", " runs: ") + theme.fg("muted", `${durations.length}`), width, )); } } const visible = allLines.slice(offset); lines.push(...visible); return lines; } // ── Commands ─────────────────────────────────────────────────────── pi.registerCommand("dashboard", { description: "Open Agent Dashboard overlay. Args: clear", handler: async (args, ctx) => { widgetCtx = ctx; const arg = (args || "").trim().toLowerCase(); if (arg === "clear") { stopTick(); clearState(); startTick(); ctx.ui.notify("📊 Dashboard: All data cleared.", "info"); updateWidget(); return; } await openOverlay(ctx); }, }); // ── Event Handlers ───────────────────────────────────────────────── pi.on("session_start", async (_event, ctx) => { applyExtensionDefaults(import.meta.url, ctx); stopTick(); widgetCtx = ctx; clearState(); startTick(); updateWidget(); }); pi.on("before_agent_start", async (_event, ctx) => { widgetCtx = ctx; return undefined; }); pi.on("agent_end", async (_event, ctx) => { widgetCtx = ctx; updateWidget(); }); pi.on("tool_call", async (event, _ctx) => { try { const toolName = event.toolName; if (!TRACKED_TOOLS.has(toolName)) return undefined; const input = event.input; const now = Date.now(); const callId = event.toolCallId; lastActivityTs = now; if (toolName === "dispatch_agent") { const agentName = (input.agent as string) || "unknown"; const task = (input.task as string) || ""; const id = `team:${agentName}:${shortId()}`; const tracked: TrackedAgent = { id, name: agentName, iface: "team", status: "running", task, startedAt: now, elapsed: 0, toolCount: 0, lastText: "", turnCount: 1, teamName: agentName, }; activeAgents.set(id, tracked); pendingCalls.set(callId, { agentId: id, ts: now }); } else if (toolName === "subagent_create") { const task = (input.task as string) || ""; const id = `sub:create:${shortId()}`; const tracked: TrackedAgent = { id, name: "Subagent", iface: "subagent", status: "running", task, startedAt: now, elapsed: 0, toolCount: 0, lastText: "", turnCount: 1, }; activeAgents.set(id, tracked); pendingCalls.set(callId, { agentId: id, ts: now }); } else if (toolName === "subagent_continue") { // Always create a new tracking entry using the widget's ID from input const subId = input.id; const prompt = (input.prompt as string) || ""; const id = `sub:cont:${subId}:${shortId()}`; const tracked: TrackedAgent = { id, name: `Subagent #${subId}`, iface: "subagent", status: "running", task: prompt, startedAt: now, elapsed: 0, toolCount: 0, lastText: "", turnCount: 1, }; activeAgents.set(id, tracked); pendingCalls.set(callId, { agentId: id, ts: now }); } else if (toolName === "run_chain") { const task = (input.task as string) || ""; const id = `chain:${shortId()}`; const tracked: TrackedAgent = { id, name: "chain", iface: "chain", status: "running", task, startedAt: now, elapsed: 0, toolCount: 0, lastText: "", turnCount: 1, chainName: "pipeline", }; activeAgents.set(id, tracked); pendingCalls.set(callId, { agentId: id, ts: now }); } // Ensure tick is running when we have active agents startTick(); updateWidget(); } catch {} return undefined; }); pi.on("tool_execution_end", async (event) => { try { const toolName = event.toolName; if (!TRACKED_TOOLS.has(toolName)) return; const now = Date.now(); const callId = event.toolCallId; lastActivityTs = now; const pending = pendingCalls.get(callId); if (pending) { pendingCalls.delete(callId); const agent = activeAgents.get(pending.agentId); if (agent) { agent.status = event.isError ? "error" : "done"; agent.endedAt = now; agent.elapsed = now - agent.startedAt; // Extract result preview if available try { const result = event.result; if (result?.content) { for (const block of result.content) { if (block.type === "text" && block.text) { agent.lastText = block.text.slice(0, 200); break; } } } } catch {} // Move to history addToHistory(agent); activeAgents.delete(pending.agentId); } } updateWidget(); } catch {} }); }