# Requests board — task drawer, history, and actions — design

audience: AI coding agents first.
source request: owner 2026-08-15 (verbatim anchors): click a task → drawer with ALL data + history of who took care of it; agents/sessions in the history are CLICKABLE and lead to the agent/session page with full history; a Dispatch button that starts an agent on the cluster immediately ("open a new tab with the new agent launched, in background, not leaving the page"); right-click menu on kanban cards: "Edit, View, Delete, [Stop — if running, stop gracefully] [Kill — if running, kill session and stop work]"; "Open proof" must actually open proof, never a stub. Owner referenced the approved /plans-style drawer mockup (activity story with expandable steps, links out, repeated asks) — that mockup's interaction pattern is the target look.
depends on: live status registry (docs/plans/2026-08-15-live-status-registry.md, factory run in flight) — the `receipt_trail` table and machinery stamps are this drawer's data source. UI slices D1–D3 consume it; do not duplicate its write paths.

## Architecture

Board (existing `apps/web/src/components/requests/RequestsBoard.tsx`, Astryx primitives) gains a detail drawer, a context menu, and an actions API. All actions go through collector HTTP endpoints consumed by the UI; the collector executes them via the same mechanisms the CLI uses (one source of truth — the endpoint calls the same code paths as `od-requests` verbs and the dispatch/session tooling, never a parallel shell-out written just for the UI).

## Slices (each deploys before the next; owner-visible evidence named first)

**D1 — drawer with full history.** Evidence: click any card → right-side drawer (mockup pattern: title, asked-at, "What's happening" summary, Activity story timeline, Links out, Repeated asks). Data: the row + its `receipt_trail` (registry L1) + linked fire/dedup entries. Every trail entry renders owner-language line + timestamp; entries with `meta.worker` render the worker@host as the actor. Empty history renders honestly ("no recorded activity yet"), never fabricated steps.

**D2 — actors are clickable.** Evidence: click a worker/agent in the drawer history → navigates to the session/agent detail view (`/sessions` page anchored to that session, or `/agents` story view if the session carries one) showing that session's full history. Seam: trail `meta` gains `session_id` when the writer knows it (claim path already derives worker labels — extend it to carry the session id). A session no longer on disk renders as "session ended — transcript archived" with whatever the ledger still knows; never a dead link.

**D3 — proof is real.** Evidence: every "Shipped" card's Open proof opens the actual proof. Rule: `proof_url` renders ONLY when the URL is real and reachable at ship time (deployed page, PR URL, artifact path served by the collector); rows without proof show "shipped without recorded proof" (honest gap) instead of a stub link. Backfill: the registry's `landed`/`deployed` stamps populate proof (deployed sha → served page URL; pr kind → PR URL from the delivery receipt).

**D4 — Dispatch button.** Evidence: click Dispatch on an open card → the card flips to in-flight with a live worker label within seconds, WITHOUT leaving the page; the new entry in the drawer history links (per D2) to the running agent's view, and that link opens in a new tab. Mechanics: collector endpoint `POST /requests/:id/dispatch` → launches the standard cluster path: a factory run against the request (its plan-ref doc when present, else the request title as the prompt), detached, on the buildbox path the factory already uses; the run's adw id + session id stamp `claimed` on the row. Guard: button disabled when the row is already claimed by a live worker (worker-lost rows re-enable it). Failure to launch → visible `failed` trail entry on the card, never silent.

**D5 — right-click context menu.** Evidence: right-click a card → menu Edit / View / Delete / Stop (running only) / Kill (running only), each working end-to-end.
- View = open drawer (same as click).
- Edit = title/priority/project editable in the drawer; writes via existing update paths; every edit journals a trail entry.
- Delete = confirm dialog → row archived (state, not hard delete — the journal is append-only truth); card disappears from the board.
- Stop (graceful) = signals the claimed worker's session to finish current step and stand down (the session admission tooling's existing graceful-stop verb; discover in `agent-sessions`/list-sessions tooling). Trail entry "stopped by owner".
- Kill = confirm dialog naming the session → terminates the session scope (the same kill path `list-sessions` uses, pid-verified, NEVER kill anything the ledger cannot prove belongs to that session; human terminal scopes are untouchable). Trail entry "killed by owner".
- Menu renders only actions valid for the row's state; Stop/Kill hidden for non-running rows.

**D6 — per-task routing: priority + account/CLI/model.** (owner 2026-08-15, verbatim: "editing the Pending tasks, i can adjust the priority, and which account/cli/model i would like to launch it with? and when a lane is open it will pick it up accordingly.") Evidence: in the drawer, an open row's priority AND routing are editable — CLI (claude / codex / pi / opencode), model, account label — and a subsequent Dispatch (D4) or any lane pickup launches with exactly those; the drawer history journals both the edit and the launch's actual routing so mismatch is visible. Seams: `requests` row gains `routing` (json: cli, model, account — all optional; empty = dispatcher defaults); `POST /requests/:id/dispatch` and the claim path read it; the board's queue-pickup surface (`od-requests list --json`) exposes it so pickers honor it. Rules: model names render and pass through VERBATIM — never substituted, never defaulted silently when set; a routing the launcher cannot satisfy (unknown model/account) fails the dispatch VISIBLY with the reason, never falls back quietly. Account values are labels the existing account-routing tooling already knows — the UI never stores credentials.

## R-slices — ALL four recommendations, approved 2026-08-15 ("why not all of them? Incrementally... You should decide the slices."). Order decided by dependency and speed; each deploys before the next; a SECOND factory run executes these after D1–D6.

**R1 — live progress tail (first: owner's pick, smallest dependency — needs only D1 + L1 progress entries).** Evidence: an in-flight card's drawer shows the claiming session's latest progress line updating without reload (poll the trail on the board's existing refresh cadence; no new stream infra). Empty when no progress recorded — honest, never synthesized.

**R2 — deliveries timeline strip.** Evidence: the board's bottom strip lists recent deploys (time, item count) fed by registry `deployed` stamps (needs L2 live); clicking an entry filters the board to rows deployed in that batch. Mockup's bottom bar is the pattern.

**R3 — answer box on blocked cards.** Evidence: a row in a blocked/needs-you state renders the drawer answer box; submitting writes an `answer` trail entry; `od-requests` gains `answer <id>` read verb so the claiming agent (or dispatched factory run) reads it; the card leaves the blocked column when answered. Edge-triggered notification on entering blocked (notifications doctrine — one message per crossing, no polling).

**R4 — "Mark not what I meant".** Evidence: button on shipped/in-flight cards; journals `not-what-i-meant` trail entry, flips the row back to open with the note attached, notifies the claiming worker if live (same edge-triggered path as R3). The re-opened row keeps full history — the drawer shows the original delivery AND the correction.

## Rejected-for-now (kept for the record)

Keyboard navigation (Esc close, arrow focus) — cosmetic, revisit after R4.

## Error handling

Every action endpoint: validate row state server-side (a Stop on an unclaimed row = 409), journal the attempt, fail visible in the drawer. Kill/Delete require confirm dialogs. No action ever fabricates state — the card shows what the machinery confirmed, not what the click hoped.

## Testing

- Store/API: bun tests per endpoint (dispatch guard, stop/kill state validation, archive semantics, proof-url honesty rule).
- UI: vitest + testing-library for drawer render from a fixture trail, context-menu state gating, dispatch button disable logic.
- Live acceptance per slice as named in each D. Playwright probes via e2e-remote only; SSE pages use domcontentloaded + waitForSelector.
- Kill-path test uses a fixture/fake session — NEVER a real session; pid-verified evidence rules from the session ledger apply.

## Architecture decisions

- Actions API lives in the collector (single writer to the store) and reuses CLI/machinery code paths — a UI-only shell-out layer was rejected (second source of truth, drifts).
- Drawer consumes the registry's `receipt_trail` — a separate per-card history store was rejected (duplicate of registry data).
- Delete archives rather than hard-deletes — journal is append-only; hard delete would break dedup and "repeated asks" history.
- Dispatch = factory run (the engine) — a bespoke UI-spawned bare session was rejected (unobservable, skips tracing/gates).
