17 file(s), +952 / -15 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +35 | -0 |
| messages/hi.json | Config | +35 | -0 |
| src/app/[locale]/(workspace)/matters/[matterId]/inbox/page.tsx | App route | +59 | -9 |
| src/app/api/internal/inbox/items/[itemId]/resolve/route.ts | App route | +43 | -0 |
| src/app/api/internal/matters/[matterId]/inbox/route.ts | App route | +52 | -0 |
| src/app/api/internal/matters/[matterId]/inbox/summary/route.ts | App route | +32 | -0 |
| src/components/matter-detail/inbox/inbox-fixtures.ts | Component | +95 | -0 |
| src/components/matter-detail/inbox/inbox-item-row.stories.tsx | Component | +68 | -0 |
| src/components/matter-detail/inbox/inbox-item-row.tsx | Component | +142 | -0 |
| src/components/matter-detail/inbox/inbox-list.stories.tsx | Component | +57 | -0 |
| src/components/matter-detail/inbox/inbox-list.tsx | Component | +35 | -0 |
| src/components/matter-detail/inbox/matter-inbox-page.tsx | Component | +129 | -0 |
| src/components/matter-detail/inbox/presentation.ts | Component | +62 | -0 |
| src/components/matters/matter-shell-chrome.tsx | Component | +10 | -4 |
| src/lib/inbox/gateway.ts | Library | +67 | -0 |
| src/lib/kubb-client.ts | Library | +29 | -0 |
| src/lib/surface-registry/route-registry.ts | Library | +2 | -2 |
The matter's Triage tab —
/matters/[matterId]/inbox — is where counsel clears the
matter's HITL queue: the items an agent or pipeline paused on because a
human must decide before work proceeds. This Story turns the placeholder
route into the real surface described by the inbox RFD
(pramaan-functions docs/notes/rfd-inbox-hitl-rail.md §7, row
IB-5) and lights the Worknav badge. It is app-lane only: the
contracts (IB-2) already landed in src/gen via codegen-sync
#2138; this Story consumes them.
item.availableActions (each a {key, label}
the backend declares), so the app never hardcodes the closed action
vocabulary. The first action is the primary button; the rest are outline
buttons. Tapping one POSTs
{action} to the resolve route.
source_span.v1 refs backing the decision. The spans are refs,
not content (the contract carries no text), so v1 shows the evidence
pointers honestly rather than faking a quote.
SegmentedFilter toggles
Needs action (open) vs History (resolved / dismissed /
expired / stale, each with a status pill). History fetches lazily and is
the archive-list doctrine: retrievable, out of the default view.
GET …/inbox/summary. The shell (MatterShellChrome)
reads the summary and passes openTotal down as
inboxCount, so the badge shows on every mode tab and updates
after a resolve.
Every read follows the canonical server-prefetch → hydrate → TanStack path (settings/AGENTS.md), and every write is optimistic.
| Layer | What it does |
|---|---|
page.tsx (RSC) |
Prefetches the open list + the summary into a per-request
QueryClient, dehydrates, wraps the client page in
HydrationBoundary. A list 5xx degrades to an inline
CatalogLoadError retry, not a shell crash. |
matter-inbox-page.tsx (client) |
Reads with useAppListMatterInbox, resolves with
useAppResolveInboxItem. Sorts client-side with the
blocking-first comparator so order holds after cache edits. |
| Optimistic resolve | onMutate snapshots the open-list cache and drops the
resolved row immediately (the row slides out via
AnimatedList); onError restores the snapshot;
onSettled invalidates the list + summary to reconcile with
server truth and refresh the badge. |
| BFF proxy | Three /api/internal/* routes (list, summary, resolve)
plus toBrowserRoute mappings, so the browser only ever
talks to /api/internal. Each route validates and delegates
to inboxGateway, which parses every upstream response
through assertContract (HR-21). |
MatterDetail/InboxItemRow — a blocking needs-review row, an
advisory high-severity privilege row, a resolved history row, and a
mid-resolve (spinner + locked) row.
MatterDetail/InboxList — the open queue (blocking-first),
history, and an interactive ResolveRemovesRow story that replays
the optimistic exit animation (row fades + shrinks, siblings glide up).
inbox-fixtures.ts (Storybook only — not a
live-path shortcut; the shipped page reads the generated hooks).
SegmentedFilter, StatusBadge,
AnimatedList/AnimatedListRow,
PrimaryButton, Button, and
SettingsLoading are all existing shared primitives. Colors are
token-backed (crimson accent, amber High chip via
StatusBadge tone="pending", --cream provenance
panel); no literals. Copy is fully i18n'd under the new
MatterInbox namespace in both en and
hi.
spanRefs carry pointers with no content, so the expand shows
the agent, run, and span count. Rendering the actual span quote is a
later enrichment once a span-resolve read exists.
item_type (the seven registry types + a
graceful unknown fallback). The typed per-type payloads (Blake's
questions, declared-vs-inferred side, etc.) are not yet surfaced — that is
the mirror-render work (IB-6/IB-7), out of scope here.
inbox_item.* ws fan-out (OQ-4) is a
later upgrade that does not change this surface.
matter_id as a string path param (vs the integer other
matter routes use). The wire path is byte-identical, so the hooks receive
String(matterId) and the gateway keeps the number.
pnpm lint — clean (pre-existing avatar warning only).pnpm typecheck — clean.pnpm test — 1024 passed (route-registry freshness + i18n parity included).pnpm build — clean; the Inbox route + three proxy routes build.pnpm gen:routes — surface manifest regenerated (the Inbox surface's capabilities now include resolve an inbox item).Staging note: the acceptance criterion "every wired type resolvable end-to-end on staging" is a runtime check against the live IB-2 backend once seeded inbox items exist for a matter; the app seam is contract-parsed at every trust boundary and verified green in Storybook + the full local suite.