"use client";
import * as React from "react";
import { MotionConfig } from "motion/react";
import { Play, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import type { GateHandle } from "./approval-gate";
import { NOW, SEED_AGENTS, SEED_RUNS } from "./data";
import { Inspector } from "./inspector";
import { RunList } from "./run-list";
import { RunTimeline, visibleCount } from "./run-timeline";
import { stepTitle } from "./step-card";
import type { Agent, ApprovalRow, Run, Step } from "./types";
import { AgentMark, Button, Kbd, Sheet, SheetHeader } from "./ui";
import { forkRun, initRunState, metrics, useRunPlayer } from "./use-run-player";
export type { Agent, ApprovalRow, Run, RunStatus, Step } from "./types";
export type AgentRunConsoleAppProps = {
/** Agents that can be started from "New run" (each with a scripted `script`). */
agents?: Agent[];
/** Runs shown in the list. A run with status "running" plays on open. */
runs?: Run[];
onStartRun?: (agentId: string, goal: string) => void;
onApprove?: (runId: string, stepIndex: number, edited?: unknown) => void;
onReject?: (runId: string, stepIndex: number, reason: string) => void;
/** Custom result previews for tool steps. */
renderToolResult?: (tool: string, result: unknown) => React.ReactNode;
playbackSpeed?: 1 | 2 | 4;
/** Reference time for "5m ago" labels. */
now?: string;
className?: string;
};
const LG = "(min-width: 1024px)";
export function AgentRunConsoleApp({ agents = SEED_AGENTS, runs: initialRuns = SEED_RUNS, onStartRun, onApprove, onReject, renderToolResult, playbackSpeed = 1, now = NOW, className }: AgentRunConsoleAppProps) {
const [speed, setSpeed] = React.useState<1 | 2 | 4>(playbackSpeed);
const { runs, dispatch } = useRunPlayer(initialRuns, speed);
const [selectedId, setSelectedId] = React.useState<string | null>(initialRuns.find((r) => r.status === "running")?.id ?? initialRuns[0]?.id ?? null);
const [pinned, setPinned] = React.useState<number | null>(null);
const [filter, setFilter] = React.useState("all");
const [mobileView, setMobileView] = React.useState<"list" | "run">("run");
const [sheetOpen, setSheetOpen] = React.useState(false);
const [newOpen, setNewOpen] = React.useState(false);
const [keysOpen, setKeysOpen] = React.useState(false);
const [announce, setAnnounce] = React.useState({ polite: "", assertive: "" });
const [sweep, setSweep] = React.useState<Record<string, number>>({});
const gateRef = React.useRef<GateHandle>(null);
const rootRef = React.useRef<HTMLDivElement>(null);
const current = runs.find((r) => r.run.id === selectedId) ?? null;
const agent = current ? agents.find((a) => a.id === current.run.agentId) : undefined;
const m = current ? metrics(current) : null;
const shown = current ? visibleCount(current) : 0;
const stepIndex = current ? (pinned !== null && pinned < shown ? pinned : shown ? shown - 1 : null) : null;
/* ---------------------------- announcements ---------------------------- */
const prev = React.useRef(new Map<string, { cursor: number; status: string }>());
React.useEffect(() => {
for (const r of runs) {
const p = prev.current.get(r.run.id);
prev.current.set(r.run.id, { cursor: r.cursor, status: r.status });
if (!p || r.replay) continue;
if (p.status !== "succeeded" && r.status === "succeeded") setSweep((s) => ({ ...s, [r.run.id]: (s[r.run.id] ?? 0) + 1 }));
if (r.run.id !== selectedId) continue;
if (p.status !== "waiting" && r.status === "waiting") {
const g = r.run.steps[r.cursor];
setAnnounce((a) => ({ ...a, assertive: `Approval required: ${g?.kind === "approval" ? g.title : ""}` }));
} else if (r.cursor > p.cursor) {
const s = r.run.steps[r.cursor - 1];
if (s) setAnnounce((a) => ({ ...a, polite: `Step ${r.cursor} ${s.kind === "error" ? "failed" : "done"}: ${stepTitle(s)}` }));
}
if (p.status !== "succeeded" && r.status === "succeeded") setAnnounce((a) => ({ ...a, polite: "Run succeeded" }));
}
}, [runs, selectedId]);
/* ------------------------------- actions ------------------------------- */
const nextId = () => String(Math.max(2400, ...runs.map((r) => Number(r.run.id) || 0)) + 1);
const select = (id: string) => {
setSelectedId(id);
setPinned(null);
setMobileView("run");
};
const selectStep = (i: number) => {
setPinned(i);
if (typeof window !== "undefined" && !window.matchMedia(LG).matches) setSheetOpen(true);
};
const approve = (index: number, edited?: ApprovalRow[]) => {
if (!current) return;
dispatch({ type: "approve", id: current.run.id, index, edited });
onApprove?.(current.run.id, index, edited);
setPinned(null);
};
const reject = (index: number, reason: string) => {
if (!current) return;
dispatch({ type: "reject", id: current.run.id, index, reason });
onReject?.(current.run.id, index, reason);
setPinned(null);
};
const startRun = (agentId: string, goal: string) => {
const a = agents.find((x) => x.id === agentId);
if (!a) return;
const steps: Step[] = structuredClone(a.script ?? [{ kind: "message", text: `No script configured for ${a.name}.` }]);
const run: Run = { id: nextId(), agentId, goal, steps, status: "running", startedAt: now, costUsd: 0 };
dispatch({ type: "add", state: initRunState(run) });
onStartRun?.(agentId, goal);
setNewOpen(false);
setFilter((f) => (f === "all" || f === agentId ? f : "all"));
select(run.id);
};
const fork = (index: number, step: Step) => {
if (!current) return;
const s = forkRun(current, index, step, nextId(), now);
dispatch({ type: "add", state: s });
setSheetOpen(false);
select(s.run.id);
};
/* ------------------------------ shortcuts ------------------------------ */
const keyRef = React.useRef<(e: KeyboardEvent) => void>(() => {});
React.useLayoutEffect(() => {
keyRef.current = (e) => {
if (e.metaKey || e.ctrlKey || e.altKey || newOpen || keysOpen) return;
const t = e.target as HTMLElement;
if (t.closest("input,textarea,select,[contenteditable]")) return;
if (!rootRef.current?.contains(t) && t !== document.body) return;
const k = e.key.toLowerCase();
if (k === "?" || (e.shiftKey && e.key === "/")) {
e.preventDefault();
setKeysOpen(true);
return;
}
if (k === "n") {
e.preventDefault();
setNewOpen(true);
return;
}
if (!current) return;
if (k === "j" || k === "k") {
e.preventDefault();
const base = stepIndex ?? 0;
const next = Math.max(0, Math.min(shown - 1, base + (k === "j" ? 1 : -1)));
setPinned(next);
requestAnimationFrame(() => rootRef.current?.querySelectorAll<HTMLElement>('ol[aria-label="Run timeline"] > li')[next]?.scrollIntoView({ block: "nearest" }));
} else if (e.key === " ") {
if (t.closest("button,a")) return;
e.preventDefault();
dispatch({ type: "toggle", id: current.run.id });
} else if (k === "a" && current.status === "waiting") {
e.preventDefault();
gateRef.current?.approve();
} else if (k === "r" && current.status === "waiting") {
e.preventDefault();
gateRef.current?.startReject();
}
};
});
React.useEffect(() => {
const h = (e: KeyboardEvent) => keyRef.current(e);
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, []);
const inspector = current && m && (
<Inspector state={current} index={stepIndex} maxMs={m.maxMs} onFork={fork} renderToolResult={renderToolResult} onClose={sheetOpen ? () => setSheetOpen(false) : undefined} />
);
return (
<MotionConfig reducedMotion="user">
<div ref={rootRef} className={cn("relative isolate flex h-[760px] w-full overflow-hidden bg-background text-foreground antialiased", className)}>
<div inert={sheetOpen || newOpen || keysOpen ? true : undefined} className="flex min-w-0 flex-1">
<aside aria-label="Runs" className={cn("w-full shrink-0 border-r bg-muted/30 md:block md:w-[272px] dark:bg-muted/15", mobileView === "list" ? "block" : "hidden")}>
<RunList runs={runs} agents={agents} selectedId={selectedId} filter={filter} onFilter={setFilter} onSelect={select} onNew={() => setNewOpen(true)} now={now} />
</aside>
<main className={cn("min-w-0 flex-1 md:block", mobileView === "run" ? "block" : "hidden")}>
{current && m ? (
<RunTimeline
key={current.run.id}
state={current}
agent={agent}
metrics={m}
selectedStep={stepIndex}
onSelectStep={selectStep}
onApprove={approve}
onReject={reject}
onRetry={() => dispatch({ type: "retry", id: current.run.id })}
onTogglePlay={() => dispatch({ type: "toggle", id: current.run.id })}
onScrub={(to) => {
dispatch({ type: "scrub", id: current.run.id, to });
setPinned(null);
}}
speed={speed}
onSpeed={setSpeed}
sweepKey={sweep[current.run.id] ?? 0}
gateRef={gateRef}
onBack={() => setMobileView("list")}
onOpenKeys={() => setKeysOpen(true)}
renderToolResult={renderToolResult}
/>
) : (
<div className="grid h-full place-items-center p-8 text-center">
<div>
<span className="mx-auto grid size-14 place-items-center rounded-2xl bg-gradient-to-br from-primary to-primary/60 text-primary-foreground shadow-lg shadow-primary/25">
<Sparkles className="size-6" aria-hidden />
</span>
<h2 className="mt-4 text-base font-semibold">Watch your agents work</h2>
<p className="mx-auto mt-1 max-w-72 text-sm text-muted-foreground">Every plan, tool call and approval, step by step — with cost and latency as it happens.</p>
<Button variant="primary" className="mt-5" onClick={() => setNewOpen(true)}>
<Play className="size-3.5" aria-hidden /> Start your first run
</Button>
</div>
</div>
)}
</main>
<aside aria-label="Step inspector" className="hidden w-[320px] shrink-0 border-l bg-muted/20 lg:block">
{!sheetOpen && inspector}
</aside>
</div>
<Sheet open={sheetOpen} onClose={() => setSheetOpen(false)} label="Step inspector" className="lg:hidden">
{inspector}
</Sheet>
<Sheet open={newOpen} onClose={() => setNewOpen(false)} label="New run" variant="dialog">
{newOpen && <NewRunForm agents={agents} onCancel={() => setNewOpen(false)} onStart={startRun} />}
</Sheet>
<Sheet open={keysOpen} onClose={() => setKeysOpen(false)} label="Keyboard shortcuts" variant="dialog" className="max-w-sm">
<SheetHeader title="Keyboard shortcuts" onClose={() => setKeysOpen(false)} />
<dl className="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5 p-4 text-[13px]">
{(
[
[["J", "K"], "Next / previous step"],
[["Space"], "Play / pause"],
[["A"], "Approve the pending gate"],
[["R"], "Reject with a reason"],
[["N"], "New run"],
[["Esc"], "Close sheets and dialogs"],
[["?"], "Show this list"],
] as const
).map(([keys, label]) => (
<React.Fragment key={label}>
<dt className="flex gap-1">
{keys.map((k) => (
<Kbd key={k} className="h-5 min-w-5 text-[11px]">
{k}
</Kbd>
))}
</dt>
<dd className="text-muted-foreground">{label}</dd>
</React.Fragment>
))}
</dl>
</Sheet>
<div aria-live="polite" className="sr-only">
{announce.polite}
</div>
<div aria-live="assertive" className="sr-only">
{announce.assertive}
</div>
</div>
</MotionConfig>
);
}
function NewRunForm({ agents, onCancel, onStart }: { agents: Agent[]; onCancel: () => void; onStart: (agentId: string, goal: string) => void }) {
const [agentId, setAgentId] = React.useState(agents[0]?.id ?? "");
const [goal, setGoal] = React.useState(agents[0]?.defaultGoal ?? "");
const [tried, setTried] = React.useState(false);
const err = !goal.trim() ? "Describe what the agent should do." : undefined;
return (
<form
noValidate
className="flex min-h-0 flex-col"
onSubmit={(e) => {
e.preventDefault();
setTried(true);
if (!err && agentId) onStart(agentId, goal.trim());
}}
>
<SheetHeader title="New run" onClose={onCancel} />
<div className="min-h-0 space-y-4 overflow-y-auto p-4">
<fieldset>
<legend className="mb-2 text-xs font-medium text-muted-foreground">Agent</legend>
<div role="radiogroup" className="grid gap-2">
{agents.map((a, i) => {
const on = a.id === agentId;
return (
<button
key={a.id}
type="button"
role="radio"
aria-checked={on}
data-autofocus={i === 0 ? true : undefined}
onClick={() => {
setAgentId(a.id);
setGoal(a.defaultGoal ?? "");
}}
className={cn("flex items-start gap-3 rounded-xl border p-3 text-left outline-none transition focus-visible:ring-2 focus-visible:ring-ring", on ? "border-primary/60 bg-primary/[0.05] ring-1 ring-primary/40" : "hover:bg-accent/50")}
>
<AgentMark hue={a.hue} />
<span className="min-w-0 flex-1">
<span className="block text-[13px] font-semibold">{a.name}</span>
<span className="block text-xs text-muted-foreground">{a.description}</span>
<span className="mt-1.5 flex flex-wrap gap-1">
{a.tools.slice(0, 4).map((t) => (
<span key={t} className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
{t}
</span>
))}
{a.tools.length > 4 && <span className="px-1 text-[10px] text-muted-foreground">+{a.tools.length - 4}</span>}
</span>
</span>
</button>
);
})}
</div>
</fieldset>
<div className="grid gap-1.5">
<label htmlFor="arc-goal" className="text-xs font-medium text-muted-foreground">
Goal
</label>
<textarea
id="arc-goal"
rows={3}
value={goal}
onChange={(e) => setGoal(e.target.value)}
aria-invalid={tried && !!err}
aria-describedby={tried && err ? "arc-goal-err" : undefined}
className="w-full resize-none rounded-lg border bg-background px-3 py-2 text-[13px] outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20 aria-invalid:border-rose-500 dark:bg-input/20"
/>
{tried && err && (
<p id="arc-goal-err" className="text-[11px] font-medium text-rose-600 dark:text-rose-400">
{err}
</p>
)}
</div>
</div>
<div className="flex justify-end gap-2 border-t p-3">
<Button onClick={onCancel}>Cancel</Button>
<Button type="submit" variant="primary">
<Play className="size-3.5" aria-hidden /> Run
</Button>
</div>
</form>
);
}
export default AgentRunConsoleApp;