# Agent Inbox

> One calm list of background agent tasks, grouped into Needs you, Running, and Done, with filter tabs and live counts. A row opens to its last steps and the action its state needs.

- Name: `agent-inbox`
- Status: beta
- Categories: agents, ai, tasks, inbox
- Files: the install writes `agent-inbox.tsx`, and any part files named `agent-inbox-*.tsx`, to your `components` alias, `@/components` by default. Edit them freely. They are yours.
- Other registry items: `@txbrendan/accordion`, `@txbrendan/badge`, `@txbrendan/button`, `@txbrendan/card`, `@txbrendan/motion-timing`, `@txbrendan/skeleton`, `@txbrendan/spinner`, `@txbrendan/status-text`, `@txbrendan/tabs`, `@txbrendan/timeline`. The install adds them.
- Gallery: https://txbrendan.dev/blocks/agent-inbox
- Registry item: https://txbrendan.dev/r/agent-inbox.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the block. It also adds every component it uses:

   ```bash
   npx shadcn@latest add @txbrendan/agent-inbox
   ```

## Use it for

- Show people the work their agents do while they are away, sorted by who has to act next.
- Each task is one row: what it is, where it stands, which model runs it, and how long it has taken.
- Tasks that wait for a yes or no and tasks that stopped both sit under Needs you, where people look first.
- Give a failed task a short reason, like "API key expired", so people can tell whether Retry will help.
- Pass elapsed in seconds. Running and waiting rows count on from there, once a second.
- Without handlers, Approve, Reject, and Retry change the task inside the block. Pass a handler to own the change in your own state.

## Avoid it for

- A queue of more than a few dozen tasks. Use a DataTable with paging and search.
- A task's full log. Link to it with href, and show the log on its own page.
- Tasks that finish in a second or two. Show the result where people asked for it.

## Props

- `tasks`: `{ id: string; title: string; summary?: string; model: string; status: "waiting" | "running" | "done" | "failed"; elapsed?: number; reason?: string; steps?: { label: string; time?: string }[]; href?: string }[]`. Default `[]`. One row per task, in order inside each group. ids must be unique. steps are oldest first, and a row shows the last four. href puts an Open link on running and done tasks. A new list, by value, replaces changes made inside the block.
- `onApprove`: `(task: AgentTask) => void`. Approve on a waiting task. Without it, the task moves to Running.
- `onReject`: `(task: AgentTask) => void`. Reject on a waiting task. Without it, the task fails with "Rejected" as its reason.
- `onRetry`: `(task: AgentTask) => void`. Retry on a failed task. Without it, the task moves to Running and its clock starts over.
- `onOpen`: `(task: AgentTask) => void`. Shows Open on running and done tasks that have no href.
- `loading`: `boolean`. Default `false`. Shows placeholder rows and hides the counts.
- `loadingText`: `string`. Default `"Loading tasks"`. What the status line tells screen readers while loading.
- `emptyText`: `string`. Default `"No agent tasks yet."`. Shown when there are no tasks and nothing is loading.
- `label`: `string`. Default `"Agent tasks"`. Names the filter tabs for screen readers.
- `className`: `string`. Extra classes on the outer div, which holds the container query.

## Accessibility

- The filters are tabs. Arrow keys move between them, and each tab says its count.
- Each row is a button inside a heading, with aria-expanded. Enter or Space opens it to the last steps and the actions.
- Each group is a labeled group in the All tab, so screen readers say Needs you, Running, or Done.
- Status is words with a color, like Waiting for you or Failed: and a reason. Color never carries it alone.
- After Approve, Reject, or Retry, focus moves to the row in its new place. When the open filter hides it, focus goes to the tab.
- One status line outside the lists reads out each change: which task, what happened, and the new counts. The clocks never speak.
- With reduced motion, rows take their new place at once, and the running spinner stays still.

## Examples

### Background tasks

```tsx
<AgentInbox
  tasks={[
    {
      id: "invoices",
      title: "Send March invoices",
      summary: "Drafted 38 invoices. Ready to email them to clients.",
      model: "Opus 5.5",
      status: "waiting",
      elapsed: 412,
      steps: [
        { label: "Drafted 38 invoices", time: "4m ago" },
        { label: "Wants to email 38 clients", time: "1m ago" },
      ],
    },
    { id: "tests", title: "Find the flaky checkout test", model: "Sonnet 5.5", status: "running", elapsed: 221, href: "/runs/tests" },
    { id: "index", title: "Rebuild the search index", model: "Haiku 5.5", status: "failed", reason: "docs server timed out", elapsed: 534 },
  ]}
/>
```

### Your own state owns each change

```tsx
"use client";

function TaskInbox() {
  const { data: tasks } = useTasks();
  return (
    <AgentInbox
      tasks={tasks ?? []}
      loading={tasks === undefined}
      onApprove={(task) => approveTask(task.id)}
      onReject={(task) => rejectTask(task.id)}
      onRetry={(task) => retryTask(task.id)}
      onOpen={(task) => openTask(task.id)}
    />
  );
}
```

### With nothing to show yet

```tsx
<AgentInbox tasks={[]} emptyText="Tasks you hand to an agent show up here." />
```
