# Agent inbox

Everything your agents are waiting on in one queue: approvals, questions and failed runs, with bulk approve and reject, a kind filter and full keyboard control.

Tier: pro. Type: block. Category: blocks. Tags: agent, approval, run, keyboard, motion.

## Install

```bash
npx shadcn@latest add @pixilui-pro/agent-inbox
```

One-time setup: add the registry to `components.json`:

```json
{
  "registries": {
    "@pixilui": "https://pixilui.com/r/{name}.json",
    "@pixilui-pro": {
      "url": "https://pixilui.com/r/pro/{name}.json",
      "headers": { "Authorization": "Bearer ${PIXILUI_TOKEN}" }
    }
  }
}
```

Pro items need a licence. Create a token on https://pixilui.com/account and set `PIXILUI_TOKEN` in `.env.local`; the CLI sends it as a bearer header.

Files: `components/pixilui/agent-inbox/agent-inbox.tsx`, `components/pixilui/agent-inbox/data.ts`, `components/pixilui/agent-inbox/parts/agent-inbox-detail.tsx`, `components/pixilui/agent-inbox/parts/agent-inbox-row.tsx`. npm: `motion`.

## Preview

Live preview: https://pixilui.com/components/agent-inbox

Knobs:

- `filter`: `all`, `approval`, `question`, `failed`
- `queue`: `full`, `empty`

Signature: Tick three approvals, press Approve 3, and they fold out of the queue one after another while a quiet receipt takes the bulk bar's place.

Styles: `quiet` (default), `lively`, set with `data-style` on <html>. The item reads the style through the foundation; it needs no prop.

## Code

Pro source is available to licence holders.

## Usage

When to use:

- Several agents or runs can stop and wait for a person, and that person needs one place to clear them.
- An operations view where approvals arrive in batches and are approved ten at a time.
- A home for agent questions that would otherwise sit in chat threads.

When not to use:

- A single approval inside a run: use approval-card where the run is shown.
- A notification feed with nothing to decide: use a plain list.
- Human review of model outputs at scale (labelling): that needs a review queue with its own grading UI.

```tsx
import { useState, type ReactNode } from "react";
import { BarChart3, GitPullRequest, LifeBuoy, Mail, MessageSquare, Receipt } from "lucide-react";
import { AgentInbox, type AgentInboxFilter } from "@/components/pixilui/agent-inbox/agent-inbox";
import { agentInboxSample } from "@/components/pixilui/agent-inbox/data";

export const knobs = {
  filter: ["all", "approval", "question", "failed"],
  queue: ["full", "empty"],
} as const;

const marks: Record<string, ReactNode> = {
  "Billing agent": <Mail strokeWidth={1.5} />,
  "Finance close": <Receipt strokeWidth={1.5} />,
  "Analytics agent": <BarChart3 strokeWidth={1.5} />,
  "Release bot": <GitPullRequest strokeWidth={1.5} />,
  "Support triage": <LifeBuoy strokeWidth={1.5} />,
};

const requests = agentInboxSample.map((request) => ({ ...request, icon: marks[request.agent] ?? <MessageSquare strokeWidth={1.5} /> }));

const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

export default function AgentInboxDemo({ filter = "all", queue = "full" }: KnobValues<typeof knobs>) {
  const [current, setCurrent] = useState<AgentInboxFilter>(filter);
  const [knob, setKnob] = useState(filter);
  if (knob !== filter) {
    setKnob(filter);
    setCurrent(filter);
  }
  return (
    <AgentInbox
      key={queue}
      className="max-w-4xl"
      requests={queue === "empty" ? [] : requests}
      filter={current}
      onFilterChange={setCurrent}
      onApprove={() => wait(900)}
      onReject={() => wait(700)}
      onAnswer={() => wait(800)}
      onRetry={() => wait(900)}
      onDismiss={() => wait(500)}
    />
  );
}
```

## Examples

None.

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `requests` (required) | `AgentInboxRequest[]` |  | The queue, newest first. Settled requests leave the list; remove them from this array whenever you like. |
| `filter` | `AgentInboxFilter` |  | Controlled kind filter. |
| `defaultFilter` | `AgentInboxFilter` | `"all"` | Starting filter when uncontrolled. |
| `onFilterChange` | `(filter: AgentInboxFilter) => void` |  | Runs when the filter changes. |
| `onApprove` | `(ids: string[]) => void \| Promise<void>` |  | Approves one request (from its card) or many (bulk). Return a promise to show pending; throw to keep them waiting. |
| `onReject` | `(ids: string[], note: string) => void \| Promise<void>` |  | Rejects one request with the note from its card, or many with an empty note. |
| `onAnswer` | `(id: string, answer: string) => void \| Promise<void>` |  | Sends the person's answer to a question. |
| `onRetry` | `(id: string) => void \| Promise<void>` |  | Starts a failed run again. |
| `onDismiss` | `(id: string) => void \| Promise<void>` |  | Clears a failed run without retrying. |
| `labels` | `Partial<AgentInboxLabels>` |  | Copy overrides, for translation and house verbs. |

Also accepts every native `<section>` attribute except `title`, and a ref to it.

## Keyboard

- `Tab`: Moves through the bulk actions, the filter, the list (one stop) and the open request.
- `ArrowDown` + `ArrowUp`: Moves the cursor in the list; the open request follows it.
- `Shift` + `ArrowDown`: Extends the selection while moving, for bulk actions.
- `Space`: Ticks or unticks the request under the cursor.
- `Ctrl` + `A`: Ticks every request in the current filter, or clears them all (Cmd on a Mac).
- `Esc`: Clears the selection.
- `Enter`: Moves focus into the open request; on an approval, Enter again approves it.
- `Home` + `End`: Jumps to the first or last request.

## Accessibility

- The list is a multi-select listbox with aria-activedescendant, so focus stays on it while the cursor moves; ticked requests are aria-selected.
- The filter is a group of toggle buttons with aria-pressed and live counts.
- Bulk actions, approvals and answers use aria-busy while pending, never disabled, so focus stays put.
- Every settled request leaves a receipt with a status role; bulk results are announced the same way.
- The open request is a labelled region you can reach with Enter from the list.

## Motion

- Settled requests leave the list by folding their height on the smooth spring; the rows below close the gap.
- The filter's pill slides to the chosen kind on the snappy spring.
- Ticks draw their check on the snappy spring; the bulk bar and its receipt swap in through a 2px blur.
- Approvals fold into their one-line receipt, as approval-card does.
- Reduced motion: rows and receipts fade, the pill jumps.

## Responsive

- Fills its container. The list and the open request sit side by side when there is room (about 600px) and stack below it, list first.
- The bulk bar wraps under the title on narrow screens; long titles truncate in the list and wrap in the open request.

## Performance

- Only the open request renders its full card; the list renders one light row per request.

## Notes for AI

- Map your agent requests to AgentInboxRequest: kind approval, question or failed; title in the agent's words; agent and time already formatted.
- Return promises from onApprove, onReject, onAnswer, onRetry and onDismiss: the inbox shows pending, keeps the request on a thrown error, and removes it when the promise resolves.
- onApprove and onReject take an array of ids: one from a card, many from the bulk bar. Bulk reject sends an empty note.
- Mark irreversible approvals risk: "high"; bulk approve then asks for a second press too.
- Settled requests leave the list on their own; you can also drop them from requests once your store confirms.
- Pass each agent's or tool's mark as icon; the block never imports an icon library.
- One per-style rule: in the lively style (data-style="lively") the empty tick box fills with the track grey, because lively controls carry no outline.

## Related

- [Approval card](https://pixilui.com/components/approval-card/markdown): An agent asks before it acts: what it will do, the rows that matter, the risk, and approve or reject with a note.
- [Run timeline](https://pixilui.com/components/run-timeline/markdown): The steps of one agent run as a vertical timeline: a connector that fills as steps finish, a live timer on the active step, and a waiting step that holds an approval.
- [Tool call row](https://pixilui.com/components/tool-call-row/markdown): One line per tool call: what it did in plain words, the tool's name, a live timer, and a fold-out with the input and output as copyable JSON. ToolCallRowGroup sums several calls into one line.
- [Button](https://pixilui.com/components/button/markdown): A button whose label can change without a jump: the width springs to the new words while they swap in place.
