"use client"; // Per-phase list of topology_run rows shown on the mission phase card. // Extracted from MissionCanvas to keep that file under the 1250-line // budget. Owns the "show/hide activity" toggle for running runs and // mounts PhaseRunStream on demand. import { useEffect, useState } from "react"; import { getRunOutput, type MissionRunSummary, type RunOutput } from "@/lib/api/missions"; import { PhaseRunStream } from "./PhaseRunStream"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) { const [expanded, setExpanded] = useState>(new Set()); if (runs.length === 0) return null; return (
{runs.map((r) => { const isFailed = r.status === "failed"; const isTerminal = ["completed", "failed", "cancelled"].includes(r.status); return (
run · {r.status} {r.id.slice(0, 8)} {isTerminal && r.finished_at && ( {new Date(r.finished_at).toLocaleTimeString()} )} {(r.status === "running" || isTerminal) && ( )}
{r.status === "running" && expanded.has(r.id) && ( )} {isTerminal && expanded.has(r.id) && } {isFailed && r.error && (
{r.error.split("\n")[0].slice(0, 180) || "error"}
                  {r.error}
                
)}
); })}
); } /** First few lines of a turn — enough to recognize it in the timeline, * short enough not to need its own scrollbar. */ function excerpt(text: string, lines = 12): string { const parts = text.split("\n"); if (parts.length <= lines) return text; return `${parts.slice(0, lines).join("\n")}\n…`; } function RunOutputPanel({ runId }: { runId: string }) { const [data, setData] = useState(null); const [err, setErr] = useState(null); useEffect(() => { let alive = true; getRunOutput(runId) .then((d) => { if (alive) setData(d); }) .catch((e) => { if (alive) setErr(String(e)); }); return () => { alive = false; }; }, [runId]); if (err) { return (
{err}
); } if (!data) { return (
loading output…
); } return (
turns: {data.turns} tokens: {data.tokens.toLocaleString()} records: {data.records_count} outputs: {data.outputs.length}
{data.outputs.length === 0 ? ( Run completed but produced no output. The agents may have been unable to reach their working directory or found nothing to act on. ) : ( data.outputs.map((o, i) => { // First turn open by default so the operator sees something // without a click; every subsequent turn collapses so the // panel stays glanceable — click to expand each turn. const firstLine = o.preview .split("\n") .find((l) => l.trim().length > 0) ?.trim() ?? ""; const preview = firstLine.length > 140 ? `${firstLine.slice(0, 140)}…` : firstLine; return (
turn {i + 1} {o.truncated ? ` · ${o.preview.length.toLocaleString()}/${o.full_len.toLocaleString()} chars` : ""} {preview && ( · {preview} )} {/* A SHORT excerpt only — no inner scrollbar. This used to be a 300px scroll box nested inside the 260px run box inside the page scroller, which made long output unreadable. The full document lives in the Output tab's reader. */}
                {excerpt(o.preview)}
              
{o.full_len.toLocaleString()} chars · open the{" "} Output {" "} tab to read this in full
); }) )}
); }