missions: surface run output on terminal phase runs
Adds GET /api/topology-runs/{id}/output — trimmed view of the
runs checkpoint (totals + per-turn output previews, capped at
12 turns × 6kB each). The full checkpoint blob can be hundreds
of KB so it was never viable to send through mission polling.
Phase card run rows now expose a "show output" toggle for any
terminal run (completed/failed/cancelled), rendering turns,
tokens, records count, and per-turn agent text. Running rows
still get the live activity stream from the prior slice.
Diagnostic value: on a mission that "completed" without visible
work, this immediately shows whether the agents produced real
output (workspace missing / instructions vague / etc.) or
whether nothing ran at all.
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
// budget. Owns the "show/hide activity" toggle for running runs and
|
||||
// mounts PhaseRunStream on demand.
|
||||
|
||||
import { useState } from "react";
|
||||
import type { MissionRunSummary } from "@/lib/api/missions";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getRunOutput, type MissionRunSummary, type RunOutput } from "@/lib/api/missions";
|
||||
import { PhaseRunStream } from "./PhaseRunStream";
|
||||
|
||||
const mono =
|
||||
@@ -70,7 +70,7 @@ export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) {
|
||||
{new Date(r.finished_at).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
{r.status === "running" && (
|
||||
{(r.status === "running" || isTerminal) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -84,8 +84,8 @@ export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) {
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
background: "transparent",
|
||||
border: "1px solid rgba(94,200,216,.35)",
|
||||
color: "#5ec8d8",
|
||||
border: `1px solid ${r.status === "running" ? "rgba(94,200,216,.35)" : "rgba(255,255,255,.18)"}`,
|
||||
color: r.status === "running" ? "#5ec8d8" : "#c9c9d0",
|
||||
fontSize: 10,
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
@@ -93,13 +93,18 @@ export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) {
|
||||
fontFamily: mono,
|
||||
}}
|
||||
>
|
||||
{expanded.has(r.id) ? "hide activity" : "show activity"}
|
||||
{expanded.has(r.id)
|
||||
? "hide output"
|
||||
: r.status === "running"
|
||||
? "show activity"
|
||||
: "show output"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{r.status === "running" && expanded.has(r.id) && (
|
||||
<PhaseRunStream runId={r.id} />
|
||||
)}
|
||||
{isTerminal && expanded.has(r.id) && <RunOutputPanel runId={r.id} />}
|
||||
{isFailed && r.error && (
|
||||
<details style={{ marginTop: 4 }}>
|
||||
<summary
|
||||
@@ -135,3 +140,97 @@ export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RunOutputPanel({ runId }: { runId: string }) {
|
||||
const [data, setData] = useState<RunOutput | null>(null);
|
||||
const [err, setErr] = useState<string | null>(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 (
|
||||
<div style={{ marginTop: 6, fontSize: 11, color: "#ff8a7a" }}>{err}</div>
|
||||
);
|
||||
}
|
||||
if (!data) {
|
||||
return (
|
||||
<div style={{ marginTop: 6, fontSize: 11, color: "#6a6a72" }}>
|
||||
loading output…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 6,
|
||||
padding: 8,
|
||||
borderRadius: 5,
|
||||
background: "rgba(0,0,0,.35)",
|
||||
fontFamily: mono,
|
||||
fontSize: 11,
|
||||
color: "#cfcfd5",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 12, color: "#8a8a92", fontSize: 10 }}>
|
||||
<span>turns: {data.turns}</span>
|
||||
<span>tokens: {data.tokens.toLocaleString()}</span>
|
||||
<span>records: {data.records_count}</span>
|
||||
<span>outputs: {data.outputs.length}</span>
|
||||
</div>
|
||||
{data.outputs.length === 0 ? (
|
||||
<span style={{ color: "#6a6a72" }}>
|
||||
Run completed but produced no output. The agents may have been unable
|
||||
to reach their working directory or found nothing to act on.
|
||||
</span>
|
||||
) : (
|
||||
data.outputs.map((o, i) => (
|
||||
<div key={i}>
|
||||
<div
|
||||
style={{
|
||||
color: "#5ec8d8",
|
||||
fontSize: 9,
|
||||
letterSpacing: ".08em",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
turn {i + 1}
|
||||
{o.truncated
|
||||
? ` · showing first ${o.preview.length.toLocaleString()} of ${o.full_len.toLocaleString()} chars`
|
||||
: ""}
|
||||
</div>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 6,
|
||||
borderRadius: 4,
|
||||
background: "rgba(0,0,0,.5)",
|
||||
color: "#e0e0e5",
|
||||
fontSize: 10.5,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: 300,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{o.preview}
|
||||
</pre>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user