"use client"; // Compact live event tail for a single topology_run — used inline on // the mission phase card when a run is 'running'. Subscribes to // /api/topology-runs/{id}/events (SSE), renders events as they arrive, // closes when 'done' or on unmount. // // Reuses the same stream the mission's LIVE tab consumes; the // difference here is scope (one run) + visual density (fits inside // a phase card, not a full tab). import { useEffect, useRef, useState } from "react"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; interface Event { key: number; ts: string; kind: string; text: string; } export function PhaseRunStream({ runId }: { runId: string }) { const [events, setEvents] = useState([]); const [terminal, setTerminal] = useState(null); const seq = useRef(0); const scrollRef = useRef(null); useEffect(() => { const es = new EventSource(`/api/topology-runs/${runId}/events`); const push = (kind: string, text: string) => { seq.current += 1; setEvents((prev) => [ ...prev, { key: seq.current, ts: new Date().toISOString(), kind, text, }, ].slice(-120), ); }; es.addEventListener("step", (ev) => { try { const data = JSON.parse((ev as MessageEvent).data ?? "{}"); const kind = String(data.kind ?? data.step_type ?? "step"); const text = data.summary ?? data.text ?? data.tool ?? data.node ?? data.reasoning ?? JSON.stringify(data).slice(0, 200); push(kind, String(text)); } catch { push("step", (ev as MessageEvent).data ?? ""); } }); es.addEventListener("done", (ev) => { try { const data = JSON.parse((ev as MessageEvent).data ?? "{}"); const label = data.status === "failed" ? "failed" : "done"; push(label, String(data.error ?? data.final_output ?? data.status ?? "")); setTerminal(label); } catch { push("done", (ev as MessageEvent).data ?? ""); setTerminal("done"); } es.close(); }); es.onerror = () => { // Auto-retry is built into EventSource; nothing to do here. }; return () => { es.close(); }; }, [runId]); useEffect(() => { const el = scrollRef.current; if (!el) return; const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; if (nearBottom) el.scrollTop = el.scrollHeight; }, [events.length]); return (
{events.length === 0 ? ( Waiting for events… (runs typically emit within a few seconds) ) : ( events.map((e) => (
{e.ts.slice(11, 19)} {e.kind} {e.text}
)) )} {terminal && (
stream closed ({terminal})
)}
); }