/* Runs — the inbox: a flat list of every agent across every project on the left, the
* selected agent's checkmark + log + answer/resume flow on the right. Maps the design's
* "Runs" pane to Handler's agent / checkmark / log model. */
"use client";
import { useMemo, useState } from "react";
import { useDashboard } from "@/components/store";
import { Badge, Button, Callout, Stat, StatusBadge, Tabs, Textarea } from "@/components/ui";
import { fmtFull, shortSha, statusTone, timeAgo } from "@/lib/format";
import type { Agent, AgentEvent } from "@/lib/api";
const FILTERS = [
{ value: "all", label: "All" },
{ value: "needs", label: "Needs Input" },
{ value: "working", label: "Working" },
{ value: "done", label: "Done" },
{ value: "crashed", label: "Crashed" },
];
function matches(filter: string, status: string): boolean {
if (filter === "all") return true;
if (filter === "needs") return status === "paused_for_input";
if (filter === "working") return status === "working" || status === "running";
if (filter === "done") return status === "done" || status === "completed";
if (filter === "crashed") return status === "crashed" || status === "blocked";
return true;
}
export function RunsSection() {
const s = useDashboard();
const [filter, setFilter] = useState("all");
const runs = useMemo(() => {
const list = s.agents.filter((a) => matches(filter, a.status));
return [...list].sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
}, [s.agents, filter]);
const selected = s.selectedRun;
const needs = s.agents.filter((a) => a.status === "paused_for_input").length;
const working = s.agents.filter((a) => a.status === "working" || a.status === "running").length;
return (
{runs.length === 0 && No runs match this filter. }
{runs.map((a) => (
s.selectRun(a.project_id, a.name)}
/>
))}
{selected ? : }
);
}
function RunRow({
agent,
selected,
onSelect,
}: {
agent: Agent;
selected: boolean;
onSelect: () => void;
}) {
return (
{agent.project_id}
{timeAgo(agent.created_at)}
{agent.name}
{agent.role ? ` · ${agent.role}` : ""}
);
}
function RunEmpty() {
return (
Select a run to see its checkmark, log, and any open question.
);
}
function RunDetail() {
const s = useDashboard();
const run = s.selectedRun!;
const agent = s.agents.find((a) => a.project_id === run.projectId && a.name === run.name);
const cm = s.checkmark;
const [answer, setAnswer] = useState("");
const [busy, setBusy] = useState(false);
const isPaused = agent?.status === "paused_for_input";
const doAnswer = async (resume: boolean) => {
if (!answer.trim()) return;
setBusy(true);
const ok = await s.submitAnswer(answer.trim(), resume);
setBusy(false);
if (ok) setAnswer("");
};
return (
<>
{run.projectId}
/
{run.name}
{agent?.role && {agent.role} }
s.killAgent(run.projectId, run.name)}>
Kill
s.deleteAgent(run.projectId, run.name)}>
Delete row
{agent?.working_dir ?? "—"} · created {fmtFull(agent?.created_at)}
{/* Checkmark */}
Checkmark
{s.checkmarkMissing &&
No checkpoint recorded yet. }
{cm && !s.checkmarkMissing && (
Status
Where it stopped
{cm.where_it_stopped || "—"}
Open question
{cm.open_question || "—"}
Next steps
{cm.next_steps && cm.next_steps.length > 0 ? (
{cm.next_steps.map((step, i) => (
{step}
))}
) : (
"—"
)}
Tests
{cm.tests_status}
{cm.tested_at ? fmtFull(cm.tested_at) : ""}
Build
{cm.build_status}
{cm.built_at ? fmtFull(cm.built_at) : ""}
Checkpoint at
{fmtFull(cm.checkpoint_at)}
)}
{/* Answer / resume */}
{isPaused && (
Answer this question
{cm?.open_question || "(no question text on the checkmark)"}
doAnswer(false)}>
Answer
doAnswer(true)}>
Answer & Resume
)}
{/* Headless run event stream (empty for legacy tmux agents) */}
{agent?.session_id && (
Run events
{agent.worker_id ? (
on {agent.worker_id}
) : null}
{s.events.length === 0 ? (
No events yet.
) : (
{s.events.map((e) => (
))}
)}
)}
{/* Log */}
Log · newest first
{s.log.length === 0 ? (
No log entries.
) : (
When
Status
Summary
Q / A
Push
CI
{s.log.map((e) => (
{fmtFull(e.created_at)}
{e.summary || "—"}
{e.question && (
Q: {e.question}
)}
{e.answer && (
A: {e.answer}
)}
{!e.question && !e.answer && "—"}
{shortSha(e.push_sha)}
{e.ci_status}
))}
)}
s.pageLog(-1)}>
‹ Newer
offset {s.logOffset}
s.pageLog(1)}>
Older ›
>
);
}
/* One stream-json event, rendered by type: assistant text as prose, tool calls as chips,
* the result as a cost/turns footer, worker notices as callouts, raw lines verbatim. */
function EventLine({ e }: { e: AgentEvent }) {
const p = (e.payload ?? {}) as Record;
const xs = { fontSize: "var(--text-xs)" } as const;
if (e.type === "system") {
return (
▸ session {p.subtype ?? "event"}
{p.session_id ? ` · ${String(p.session_id).slice(0, 8)}` : ""}
{Array.isArray(p.tools) ? ` · ${p.tools.length} tools` : ""}
);
}
if (e.type === "assistant") {
const content = p.message?.content;
const blocks: any[] = Array.isArray(content) ? content : [];
const text = blocks
.filter((b) => b?.type === "text" && b.text)
.map((b) => b.text)
.join("\n");
const tools = blocks.filter((b) => b?.type === "tool_use");
return (
{text && (
{text}
)}
{tools.length > 0 && (
{tools.map((t, i) => (
{t.name}
{t.input ? `: ${oneLine(t.input)}` : ""}
))}
)}
);
}
if (e.type === "result") {
const err = Boolean(p.is_error);
return (
{err ? "run errored" : "run finished"}
{p.num_turns != null ? `${p.num_turns} turns` : ""}
{p.total_cost_usd != null ? ` · $${Number(p.total_cost_usd).toFixed(4)}` : ""}
{typeof p.result === "string" && p.result && (
{p.result}
)}
);
}
if (e.type === "worker") {
return (
{p.notice ?? "runner notice"}
{p.stderr_tail ? (
{p.stderr_tail}
) : null}
);
}
if (e.type === "raw") {
return (
{typeof p.line === "string" ? p.line.trimEnd() : JSON.stringify(p)}
);
}
// user (tool results) and anything future: a quiet one-liner, nothing lost, no noise.
return (
▸ {e.type}
);
}
/* Compact single-line preview of a tool_use input object. */
function oneLine(input: unknown): string {
const s =
typeof input === "string"
? input
: (input as Record)?.command
? String((input as Record).command)
: JSON.stringify(input);
return s.length > 80 ? `${s.slice(0, 77)}…` : s;
}