# Build the "Background run tray" pattern: `RunTray`

## Stack (edit this block to match your project)
- React 18+ with TypeScript: one function component, typed props, no `any`.
- Tailwind CSS, using your design system's tokens if you have them.
- Presentational only: data comes in through props, decisions go out through callbacks. No fetching or global state inside.
- If your codebase already has conventions (naming, shared components, tokens), follow them over this spec.

## What the pattern is for
A persistent list of agent runs with status, so async work is inspectable instead of invisible.

Use it when: Any agent that outlives the session or runs while the user is elsewhere.

## What to build
A persistent panel listing every agent run, running or finished, with its status, age and outcome, so asynchronous work stays inspectable after any toast has gone. A person can filter by status, open a run's detail and act on runs that need them. It renders as a drawer, sidebar section or page.

### Props
- `runs`: Array<{ id: string; title: string; agent?: string; startedAt: Date; endedAt?: Date; status: "queued" | "running" | "needs-input" | "succeeded" | "failed" | "cancelled"; progress?: { done: number; total?: number }; summary?: string; error?: string }>, server-owned run objects.
- `filter`: "all" | "active" | "needs-input" | "failed" | "done", controlled, with `onFilterChange(filter)`.
- `selectedId?`: string and `onSelect(id | null)`: which run's detail is open.
- `onOpenRun(id)`: navigate to the full run log or result.
- `onCancel?(id)` and `onRetry?(id)`: actions for running and failed runs. The component never starts or stops work itself.
- `connection`: "live" | "reconnecting" | "offline", how fresh the statuses are.
- `lastUpdatedAt?`: Date, shown when not live.

### States it must render
- List: newest first, each row showing title, status label with icon, elapsed or finished time, and a one-line outcome ("Tagged 412 tickets", "Failed: rate limited").
- Running row: progress such as "120 of 400" or an indeterminate indicator, with Cancel.
- Needs input: amber row pinned to the top with "Waiting for you" and an Open action.
- Failed row: the error in plain words with Retry and Open log.
- Empty: "No runs yet. Work you start in the background will appear here." Filter-empty says which filter is hiding runs.
- Stale: when `connection` is not live, a banner "Statuses may be out of date, last updated 09:14" over an unchanged list.

### Interactions
- Status filter chips show a count each and call `onFilterChange`.
- Clicking a row calls `onSelect` and expands the summary inline; "Open" calls `onOpenRun`.
- Cancel asks for confirmation inline ("Cancel this run? Work done so far is kept") before `onCancel`.
- A collapsed tray shows a badge with the number of active and needs-input runs, so it is reachable from anywhere.

### Edge cases
- A run finishes while its toast is dismissed or the page was closed: it is still in the list with its terminal state, since the tray reads runs, not notifications.
- A run stuck in running far past its expected time: show "No update for 20 min" rather than a forever spinner.
- Many finished runs: group older ones under "Earlier" and paginate; never drop them silently.
- A run that transitions while selected: keep it selected and update in place without reordering under the pointer.

## Must avoid
A notification-only model where a dismissed toast is the only record the work ever happened.

## Engineering constraint
Model runs as first-class server objects with terminal states; poll or stream status into the tray.

## Accessibility
- The list is a `list` of runs whose accessible names include title and status; announce only terminal transitions and needs-input, not progress ticks.
- The collapsed badge is a button named like "Background runs: 2 running, 1 needs you".

## House rules
- Real `<button>`/`<input>` elements, full keyboard use, visible focus; announce status changes with `aria-live="polite"`; respect `prefers-reduced-motion`.
- All user-facing text comes from props (or a `labels` prop) so it can be translated.
- One consistent accent marks anything the AI made or is doing; amber means "needs a person", red means risky, green means done. Never rely on colour alone.
- Fits a 360 px wide container without horizontal scrolling.

## Acceptance checks
- [ ] Every state above can be rendered by passing props, with no internal fake data.
- [ ] Every interaction and edge case above behaves as described.
- [ ] Nothing in "Must avoid" can happen with this component.

## Deliver
1. The component file with its exported prop types.
2. A short usage example that renders each state.
