{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-console","type":"registry:block","title":"Multi-agent Ops Console","description":"Watch a fleet of agents at once — parallel statuses, an inspectable handoff, a waiting approval, a failed child run, and per-run plus fleet cost.","author":"Scrim UI (https://scrimui.dev)","categories":["pattern"],"docs":"https://scrimui.dev/patterns/agent-console","dependencies":[],"registryDependencies":["https://scrimui.dev/r/agent-status.json","https://scrimui.dev/r/agent-handoff.json","https://scrimui.dev/r/agent-run-timeline.json"],"files":[{"path":"src/showcase/patterns/agent-console/agent-console.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AgentStatus, type AgentState } from \"@/components/ui/agent-status\";\nimport { AgentHandoff, type HandoffState } from \"@/components/ui/agent-handoff\";\nimport { AgentRunTimeline, type RunEvent } from \"@/components/ui/agent-run-timeline\";\n\n/**\n * An operations console for several agents running in parallel.\n *\n * What this pattern exists to show:\n *\n * 1. **The roster answers \"who's doing what\" in one glance.** Each agent is\n *    a status card; selecting one swaps the timeline, never the page.\n * 2. **A handoff is a first-class event with a context receipt.** What was\n *    carried across — and what was deliberately not — is inspectable where\n *    the handoff happened.\n * 3. **Approval waits at fleet level.** The console header counts pending\n *    approvals across every agent; the gate itself stays inline in the\n *    owning agent's log.\n * 4. **A failed child run is one card, not an outage.** The Billing agent\n *    failed; Research and Writer carry on. Rerun is per-agent.\n * 5. **Cost is per-run and in aggregate.** The header sums the fleet; each\n *    agent's timeline keeps its own meter.\n *\n * Pro boundary: the Cost Meter and Approval Gate components stay Pro — this\n * pattern composes only free components; cost rides the timeline summary.\n */\n\n/* ------------------------------------------------------------------ */\n/* Mock fleet                                                          */\n/* ------------------------------------------------------------------ */\n\ntype Agent = {\n  id: string;\n  name: string;\n  state: AgentState;\n  action: string;\n  cost: string;\n  tokens: number;\n  events: RunEvent[];\n  handoff?: { to: string; task: string; carried: string[]; withheld: string[]; state: HandoffState };\n};\n\nconst FLEET: Agent[] = [\n  {\n    id: \"research\",\n    name: \"Researcher\",\n    state: \"running\",\n    action: \"Searching vendor contracts…\",\n    cost: \"$0.07\",\n    tokens: 12_300,\n    events: [\n      { id: \"r1\", kind: \"model\", title: \"Scoped the renewal question\", at: \"09:41:02\", status: \"completed\", durationMs: 2400 },\n      { id: \"r2\", kind: \"tool\", title: \"search_docs(\\\"acme renewal terms\\\")\", detail: \"6 passages retrieved\", at: \"09:41:05\", status: \"completed\", durationMs: 1700 },\n      { id: \"r3\", kind: \"tool\", title: \"search_docs(\\\"acme SLA history\\\")\", detail: \"3 passages retrieved\", at: \"09:41:08\", status: \"completed\", durationMs: 1400 },\n      { id: \"r4\", kind: \"tool\", title: \"read_file(\\\"contracts/acme-2026.pdf\\\")\", at: \"09:41:12\", status: \"completed\", durationMs: 820 },\n      { id: \"r5\", kind: \"model\", title: \"Comparing renewal clause against SLA log\", at: \"09:41:15\", status: \"running\" },\n    ],\n  },\n  {\n    id: \"writer\",\n    name: \"Writer\",\n    state: \"waiting\",\n    action: \"Waiting on approval to send\",\n    cost: \"$0.03\",\n    tokens: 5_100,\n    handoff: {\n      to: \"Writer\",\n      task: \"Draft the renewal summary email for the account manager\",\n      carried: [\"Renewal clause excerpt (p.4)\", \"SLA breach count: 2\", \"Account tone: formal\"],\n      withheld: [\"Internal pricing floor\", \"Legal's escalation notes\"],\n      state: \"accepted\",\n    },\n    events: [\n      { id: \"w1\", kind: \"handoff\", title: \"Accepted task from Researcher\", detail: \"Draft the renewal summary email\", at: \"09:40:12\", status: \"completed\" },\n      { id: \"w2\", kind: \"model\", title: \"Drafted email v1\", at: \"09:40:31\", status: \"completed\", durationMs: 5100 },\n      { id: \"w3\", kind: \"model\", title: \"Tightened subject line\", at: \"09:40:39\", status: \"completed\", durationMs: 1300 },\n      { id: \"w4\", kind: \"approval\", title: \"Send email to account manager\", detail: \"External recipient — cannot be unsent\", at: \"09:40:41\", status: \"waiting\" },\n    ],\n  },\n  {\n    id: \"billing\",\n    name: \"Billing\",\n    state: \"failed\",\n    action: \"Child run failed — payment API timeout\",\n    cost: \"$0.01\",\n    tokens: 1_900,\n    events: [\n      { id: \"b1\", kind: \"tool\", title: \"read_file(\\\"invoices/q3.json\\\")\", at: \"09:38:44\", status: \"completed\", durationMs: 510 },\n      { id: \"b2\", kind: \"tool\", title: \"charge_customer(acme, 4820)\", detail: \"Payment API timeout after 5s\", at: \"09:38:50\", status: \"failed\", durationMs: 5000 },\n      { id: \"b3\", kind: \"error\", title: \"Child run aborted\", detail: \"1 of 3 steps completed — no charge was made\", at: \"09:38:50\", status: \"failed\" },\n    ],\n  },\n];\n\n/* ------------------------------------------------------------------ */\n/* Pattern                                                             */\n/* ------------------------------------------------------------------ */\n\nexport function AgentConsolePattern() {\n  const [agents, setAgents] = React.useState<Agent[]>(FLEET);\n  const [selectedId, setSelectedId] = React.useState(\"writer\");\n  const [decisions, setDecisions] = React.useState<Record<string, \"approved\" | \"rejected\">>({});\n\n  const selected = agents.find((a) => a.id === selectedId) ?? agents[0];\n  const pendingApprovals = agents.reduce(\n    (n, a) => n + a.events.filter((e) => e.kind === \"approval\" && e.status === \"waiting\" && !decisions[e.id]).length,\n    0,\n  );\n  const totalTokens = agents.reduce((s, a) => s + a.tokens, 0);\n\n  function decide(agentId: string, eventId: string, approved: boolean) {\n    setDecisions((d) => ({ ...d, [eventId]: approved ? \"approved\" : \"rejected\" }));\n    setAgents((as) =>\n      as.map((a) =>\n        a.id !== agentId\n          ? a\n          : {\n              ...a,\n              state: approved ? \"completed\" : \"failed\",\n              action: approved ? \"Email sent — run complete\" : \"Approval rejected — run ended\",\n              events: a.events.map((e) =>\n                e.id === eventId\n                  ? { ...e, status: approved ? \"completed\" : \"cancelled\", detail: approved ? \"Approved by you — sending now\" : \"Rejected by you\" }\n                  : e,\n              ),\n            },\n      ),\n    );\n  }\n\n  function rerun(agentId: string) {\n    setAgents((as) =>\n      as.map((a) =>\n        a.id !== agentId\n          ? a\n          : {\n              ...a,\n              state: \"running\",\n              action: \"Retrying charge…\",\n              events: [\n                ...a.events,\n                { id: \"b4\", kind: \"tool\", title: \"charge_customer(acme, 4820)\", detail: \"Retry after timeout\", at: \"09:44:01\", status: \"running\", retryOf: \"b2\" },\n              ],\n            },\n      ),\n    );\n    window.setTimeout(() => {\n      setAgents((as) =>\n        as.map((a) =>\n          a.id !== agentId\n            ? a\n            : {\n                ...a,\n                state: \"completed\",\n                action: \"Charge succeeded on retry\",\n                cost: \"$0.02\",\n                tokens: a.tokens + 800,\n                events: a.events.map((e) =>\n                  e.id === \"b4\" ? { ...e, status: \"completed\", durationMs: 1900, detail: \"Succeeded on retry\" } : e,\n                ),\n              },\n        ),\n      );\n    }, 2200);\n  }\n\n  return (\n    <div className=\"flex h-[640px] overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900\">\n      {/* Fleet roster */}\n      <aside className=\"hidden w-64 shrink-0 flex-col gap-2 overflow-y-auto border-r border-zinc-200 p-3 dark:border-zinc-800 md:flex\">\n        <p className=\"text-[13px] font-semibold text-zinc-900 dark:text-zinc-100\">Fleet · {agents.length} agents</p>\n        {agents.map((a) => (\n          <button\n            key={a.id}\n            type=\"button\"\n            onClick={() => setSelectedId(a.id)}\n            aria-current={a.id === selectedId ? \"true\" : undefined}\n            className={`rounded-xl text-left transition-shadow ${\n              a.id === selectedId ? \"ring-2 ring-zinc-900 dark:ring-zinc-100\" : \"hover:ring-1 hover:ring-zinc-300 dark:hover:ring-zinc-700\"\n            }`}\n          >\n            <AgentStatus name={a.name} status={a.state} action={a.action} />\n          </button>\n        ))}\n        <div className=\"mt-auto rounded-xl bg-zinc-50 px-3 py-2 text-[11px] leading-5 text-zinc-500 dark:bg-zinc-800/60 dark:text-zinc-400\">\n          <p className=\"font-medium text-zinc-700 dark:text-zinc-200\">Fleet total</p>\n          <p className=\"tabular-nums\">{totalTokens.toLocaleString()} tokens · $0.11</p>\n          {pendingApprovals > 0 && (\n            <p className=\"font-medium text-amber-600 dark:text-amber-400\">\n              {pendingApprovals} approval{pendingApprovals === 1 ? \"\" : \"s\"} pending\n            </p>\n          )}\n        </div>\n      </aside>\n\n      {/* Selected agent */}\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <div className=\"flex flex-wrap items-center justify-between gap-2 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800\">\n          <div className=\"min-w-0\">\n            <p className=\"text-sm font-semibold text-zinc-900 dark:text-zinc-100\">{selected.name}</p>\n            <p className=\"truncate text-xs text-zinc-500 dark:text-zinc-400\">\n              {selected.tokens.toLocaleString()} tokens · {selected.cost} this run\n            </p>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <select\n              value={selectedId}\n              onChange={(e) => setSelectedId(e.target.value)}\n              aria-label=\"Select agent\"\n              className=\"rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 md:hidden\"\n            >\n              {agents.map((a) => (\n                <option key={a.id} value={a.id}>\n                  {a.name}\n                </option>\n              ))}\n            </select>\n            {selected.state === \"failed\" && (\n              <button\n                type=\"button\"\n                onClick={() => rerun(selected.id)}\n                className=\"rounded-lg bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300\"\n              >\n                Rerun failed step\n              </button>\n            )}\n          </div>\n        </div>\n\n        <div className=\"flex-1 space-y-3 overflow-y-auto px-4 py-3\">\n          {selected.handoff && (\n            <AgentHandoff\n              from=\"Researcher\"\n              to={selected.handoff.to}\n              task={selected.handoff.task}\n              carried={selected.handoff.carried}\n              withheld={selected.handoff.withheld}\n              state={selected.handoff.state}\n              reason=\"Drafting needs a writing specialist, not another search pass.\"\n            />\n          )}\n          <AgentRunTimeline\n            className=\"h-full min-h-[300px]\"\n            events={selected.events}\n            onApprove={(eid) => decide(selected.id, eid, true)}\n            onReject={(eid) => decide(selected.id, eid, false)}\n            summary={{ tokens: selected.tokens, cost: selected.cost, elapsed: \"2m 14s\" }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:block","target":"components/blocks/agent-console.tsx"}]}