Agent ops
Mission control for background agents: every run in one inbox, every approval in one queue, the selected run's live trace and an activity feed. Approve from anywhere and the run picks up where it stopped.
On this page
The demo runs on a scripted fleet, so it needs no API key. The selected run streams its trace and stops for an edit approval, which approves itself after 4 s if you leave it. Around it, research finishes, a queued run starts and a dependency bump opens a pull request. Approve the Next.js upgrade in Needs you to unblock it. Open /preview/agent-ops-demo to see it full screen.
Installation
The block installs to components/chatcn/agent-ops/agent-ops.tsx together with every component it composes. It is your code from here: add a New task button to the header, swap the trace renderers, drop the activity column.
Usage
Pass your runs as data. Each run is a BackgroundRunData (title, status, context, latest step, progress) plus the task prompt and the agent's parts in the chatcn message contract. Tool parts in approval-requested show up in Needs you on their own, and in the run's trace with Allow and Deny.
"use client"
export default function AgentsPage() {
const { runs, events, approve, deny, cancel, retry, markRead } = useRuns()
return (
<AgentOps
fullHeight
runs={runs}
notifications={events}
onOpenRun={markRead}
onApprove={(approvalId, { scope, runId }) => approve(runId, approvalId, scope)}
onDeny={(approvalId, reason, { runId }) => deny(runId, approvalId, reason)}
onCancelRun={cancel}
onRetryRun={retry}
/>
)
}Keep runs in your own store and update them from your backend over SSE or a websocket. The block never polls and never imports a runtime: a new runs array is the whole update. When a run finishes or needs you, append to notifications; new ones are toasted when the activity column is out of view and the run is not already on screen.
const run: AgentOpsRun = {
id: "run_8f2",
title: "Fix flaky checkout test",
context: "tallyline/web · fix/checkout-flake",
prompt: "The checkout e2e test fails about one run in five on CI.",
status: "needs-input",
steps: { done: 3, total: 5 },
startedAt: 1761060000000,
parts: [
{ type: "reasoning", text: "Reproduce the flake first.", state: "done" },
{
type: "tool-edit_file",
toolCallId: "call_1",
state: "approval-requested",
input: { path: "tests/e2e/checkout.spec.ts", diff },
approval: { id: "ap_1", requestReason: "Test code only." },
},
],
}Components it composes
| Component | Role |
|---|---|
| Run inbox | The run list with filters, unread dots and arrow keys |
| Background run | Run rows, status glyphs and the 2 px progress line under the run header |
| Approval inbox | Needs you: every pending approval across runs, with batch Allow and Deny |
| Run status | Status and elapsed time in the run header |
| Trace | The rail that holds the selected run's steps |
| Reasoning | Thinking blocks inside the trace |
| Tool fallback | Routes each tool part to its renderer; unknown tools fall back |
| Tool bash, Tool file edit, Tool file read, Tool search code, Tool web search, Tool web fetch | The tool calls in the trace, with inline approvals |
| Markdown | What the agent writes between steps |
| Conversation | Stick-to-bottom scrolling for the live trace |
| Agent notification | The activity feed and the toasts |
| Resizable | Columns you can drag, with keyboard resizing |
| Popover | The activity feed behind the bell on narrower layouts |
Responsive
| Block width | Layout |
|---|---|
| 1152 px and up | Three columns: runs, the selected run, then Needs you and Activity |
| 768 to 1151 px | Two columns. Needs you sits above the runs; activity moves behind the bell and toasts |
| Under 768 px | One column. Needs you, then runs; picking a run pushes it with a back button |
The layout follows the block's own width, not the viewport, so it works in a docs column, a split pane and a full page alike. Rows and buttons grow to 44 px on touch screens.
Props
| Prop | Type | Notes |
|---|---|---|
runs | AgentOpsRun[] | Shown in the order you pass. Approvals are derived from their parts |
selectedRunId, defaultSelectedRunId, onSelectedRunChange | string | null | Controlled or uncontrolled. Defaults to the first run that needs you |
onOpenRun | (runId) => void | A run was opened from the list. Mark it read |
onMarkAllRead | () => void | Shows Mark all read while anything is unread |
onApprove | (approvalId, { scope, runId }) => void | From Needs you or from the trace |
onDeny | (approvalId, reason, { runId }) => void | The reason is the note the person typed, if any |
onCancelRun, onRetryRun | (runId) => void | Cancel on active runs, Retry on failed ones |
notifications | AgentOpsNotification[] | Newest last: kind, title, description, runId, createdAt |
toasts | boolean | Toast new notifications while the activity column is hidden. Default true |
title | ReactNode | Header title. Default Agents |
headerActions | ReactNode | Extra controls before the bell |
toolRenderers | ToolRendererMap | Tool name to renderer for the trace |
loading | boolean | Skeleton rows in the run list |
fullHeight | boolean | Fill the viewport. Otherwise give the parent a height |
Keyboard
| Key | Action |
|---|---|
| Arrow up, Arrow down, Home, End | Move between runs in the list |
| Enter, Space | Open the focused run |
| A, D, X | Allow, deny or select the focused row in Needs you |
| Arrow left, Arrow right | Resize the focused column divider. Enter collapses it |
| Escape | Back to the list from a pushed run on phones |
| Alt+T | Focus the toasts |
Accessibility
- The run list, Needs you, the run and Activity are labelled regions with their own headings.
- Run status changes are announced through polite live regions; the header counts are one too.
- The selected run row carries
aria-current, and the bell's label includes the number of new events. - Column dividers are focusable separators with their current size, per the window splitter pattern.
- Status is never color alone: every state has a glyph and a label.
Motion
- Status glyphs swap with a 150 ms blur crossfade, and the latest step line rises 4 px as it changes.
- The progress line under the run header fills with a 280 ms transform.
- New activity slides in 4 px over 280 ms; toasts follow the agent notification stack.
- A pushed run on phones enters from 16 px over 280 ms. Keyboard moves and selection never animate.
- Under reduced motion only the opacity fades remain.