PR: #569: feat(app): the CCB chassis — constant Command+Talk, contextual verbs, focus payload [Story-2283] by @pramaan-board-bot[bot]
View PR →

Story-2283: the CCB chassis (B5a, EPIC-2270)

The section surface is live (app#568) and the backend now assembles a document_selection ContextBundle profile (functions#650). This ticket builds the vehicle that carries counsel's ask to an agent with that context attached.

The idea in one line

The CCB is a vehicle, not a toolbar. The chassis is constant — the same bar, in the same place, with the same two affordances (Command and Talk, D29) — and everything between them changes with what counsel is looking at. The verbs are cargo; the bar is the truck.

Where the pieces live

ConcernFile
The ruling: which verbs a focus resolves to (origin × section kind × signals) src/components/documents/ccb/ccb-presentation.ts (+ 16 unit tests)
What each verb looks like and which lane it fires down src/components/documents/ccb/ccb-verb-catalog.tsx
The chassis (pure, storyable — every state reachable without a socket) src/components/documents/ccb/document-ccb-chassis.tsx
The ask pipeline: context gate → agent src/components/documents/ccb/use-ccb-ask.ts
The in-place answer (first frame → stream → refusal) src/components/documents/ccb/ccb-answer-card.tsx
The focus seam (shell provides, view publishes) src/components/documents/ccb/document-focus-context.tsx
The wire: proxy → gateway (HR-21 assertContract) src/app/api/internal/matters/[matterId]/context/bundles/document-selection/route.ts, src/lib/documents/gateway.ts

1. The chassis is constant, so it lives in the shell

The bar is mounted by document-permalink-shell.tsx — the shared chrome above Original, Insights and the machine copy. That is what makes "constant" structural rather than merely visual: there is no per-view copy of the bar to drift.

But what counsel is pointing at is known one level down, inside whichever view is mounted. So the shell provides a focus context and the view publishes into it — the section selected in the outline, the span dragged on the page, or a gutter mark clicked. A view that has no notion of focus publishes nothing, and the bar honestly falls back to "Whole document": with nothing selected, the document is the scope.

2. The verbs are data, not an if-chain

resolveCcbVerbs(focus) reads three axes and returns at most three verbs (D20's hard ceiling). The chassis maps over the result and dispatches by lane — it contains no knowledge of any individual verb, so adding one is a registry entry, never an edit to the bar.

AxisRule
Content origin Uploaded document text → Explain · Impact · Note. Agent-produced content (the machine's own mark) → Why · Cite. "Why" never appears on uploaded text: there is no reasoning trail behind opposing counsel's words, and offering one would be a lie about where the text came from.
Section kind An opt-in override table. Kind labels are counsel's own words (D26 — the catalog is firm-owned and editable), so no taxonomy is shipped; every kind falls through to the origin default until a firm says otherwise.
Present signals A signal verb exists only while its signal does. A contradiction chip appears when the focused section holds a live contradiction mark, and not otherwise — it is never a permanently-greyed button waiting for one. Signals sort first and survive the ≤3 cap, because a signal the engine actually holds outranks a verb counsel can reach anywhere.

3. The ask: gate first, then agent

The order is the whole point.

  1. The context gate. The focus goes to the document_selection profile. A privileged document is REFUSED and a strategy document EXCLUDED — typed 403s decided at the query layer before a single section is read. We ask for the context before we ask the agent, so a refusal is something counsel is TOLD, not something they infer from a thin answer. A refusal stops the pipeline: no agent turn is fired.
  2. The ask. Cleared by the gate, the prompt goes down Mira's EXISTING converse rail — the same module-scoped connection the Agent Dock is subscribed to.

That second choice is what buys D29's handoff continuity for free: the ask and its answer are already turns on the panel's thread, so Talk / Continue in the panel opens a conversation that CONTINUES this moment instead of restarting it. No new runtime, no broker change, no second socket.

4. Summary-as-first-frame (SS-11, ux-guidelines §19)

The instant counsel asks, the answer card paints the focused section's stored summary (D15) — already in hand from the section layer, so it costs nothing and arrives before the model has produced a token. It is labelled as what it is ("Stored read"), never passed off as the answer, and the shared AgentWorkingLine keeps the wait alive beneath it (motion + ticking seconds + phase). The live stream then replaces it.

An empty box that waits three seconds teaches counsel the surface is slow; a true sentence teaches them it is working. Where there is no stored summary (an unsegmented document), the card goes straight to the wait rather than filling the gap with a guess.

5. The refusal is said out loud

A privileged or strategy-excluded context renders the typed refusal, never a silent empty state. "We would not assemble this" and "we could not assemble this" are different sentences, and counsel is owed the right one — so a 403 with a known code renders as a refusal, and anything else renders as an error with a retry.

Reuse (nothing here was rebuilt)

Live evidence (staging backend)

The context gate is real. A real ask on a staging document (matter 13, HARSHA_VENKATESH_PLAIN.pdf) issued:

POST /api/internal/matters/13/context/bundles/document-selection → 200
request:  {"budget":"M","focus":{"document_id":"be39d48c-…","schema_version":"document_focus.v1","section_ids":[]}}
response: {"schema_version":"context_bundle.v1","profile":"document_selection",
           "focus_axis":{…echoed…},"engine_state":"live","token_estimate":68,
           "version_hash":"5f30c943c72f5858…","sections":[{"kind":"doc_identity",…},
           {"kind":"section_summaries","items":[]}]}

A real context_bundle.v1, through the proxy, past assertContract. section_summaries is empty because that document is not segmented yet — an honest state, not an error.

The refusal is real. With a document marked privileged, the same ask returned 403 pramaan.context.privileged_refused and the CCB rendered "Context withheld — privileged. This document is marked privileged, so it is never assembled into an agent's context. Nothing was sent, and nothing was read." No agent turn was fired. (The privilege flag was reverted afterwards.)

Known gap — the agent leg is blocked on staging, and NOT by this change. The launch leaves the app correctly (matter_id = the matter UUID per Bug-2210) and the broker answers run.accepted, then run.failed with kind=validation_error: "I could not start this run because required runtime information was invalid" (retryable=false).

The same failure reproduces from the Agent Dock's own composer — code this ticket does not touch — so Mira's converse run is currently broken on staging independently of the CCB. Filed separately; broker changes are explicitly out of scope here.

The CCB's behaviour under that failure is itself correct and was verified live: it surfaces the honest error card ("That ask didn't get through. Nothing was sent to the record.") rather than spinning forever — Story-2181 / §19's rule that a stalled run must surface, never masquerade as thinking.

Files changed

119 file(s), +12036 / -668 lines

Path Role + -
messages/en.jsonConfig+46-1
messages/hi.jsonConfig+46-1
openapi/app-api.jsonConfig+1825-529
openapi/permission-map.jsonConfig+9-0
src/app/api/internal/matters/[matterId]/context/bundles/document-selection/route.tsApp route+90-0
src/components/agents/agent-chat-panel.tsxComponent+13-56
src/components/agents/agent-working-line.stories.tsxComponent+49-0
src/components/agents/agent-working-line.tsxComponent+100-0
src/components/documents/ccb/ccb-answer-card.stories.tsxComponent+140-0
src/components/documents/ccb/ccb-answer-card.tsxComponent+236-0
src/components/documents/ccb/ccb-fixtures.tsComponent+67-0
src/components/documents/ccb/ccb-focus-chip.stories.tsxComponent+33-0
src/components/documents/ccb/ccb-focus-chip.tsxComponent+67-0
src/components/documents/ccb/ccb-presentation.test.tsComponent+173-0
src/components/documents/ccb/ccb-presentation.tsComponent+168-0
src/components/documents/ccb/ccb-verb-catalog.tsxComponent+81-0
src/components/documents/ccb/ccb-verb-chip.stories.tsxComponent+61-0
src/components/documents/ccb/ccb-verb-chip.tsxComponent+82-0
src/components/documents/ccb/document-ccb-chassis.stories.tsxComponent+191-0
src/components/documents/ccb/document-ccb-chassis.tsxComponent+160-0
src/components/documents/ccb/document-ccb.tsxComponent+149-0
src/components/documents/ccb/document-focus-context.tsxComponent+84-0
src/components/documents/ccb/use-ccb-ask.tsComponent+177-0
src/components/documents/viewer/document-counsel-bar.tsxComponent+37-73
src/components/documents/viewer/document-original-pane.tsxComponent+79-0
src/components/documents/viewer/document-permalink-shell.tsxComponent+11-1
src/components/documents/viewer/document-reading-surface.tsxComponent+20-1
src/components/documents/viewer/section-mark-popover.tsxComponent+24-1
src/gen/client/contextDocumentSelectionBundle.tsGenerated contract+79-0
src/gen/client/index.tsGenerated contract+1-0
src/gen/hooks/index.tsGenerated contract+4-0
src/gen/hooks/useContextDocumentSelectionBundle.tsGenerated contract+165-0
src/gen/index.tsGenerated contract+104-2
src/gen/schemas/assertion.jsonGenerated contract+146-0
src/gen/schemas/assertionEdge.jsonGenerated contract+80-0
src/gen/schemas/assertionKind.jsonGenerated contract+15-0
src/gen/schemas/buildWatermark.jsonGenerated contract+29-0
src/gen/schemas/bundleSection.jsonGenerated contract+1471-0
src/gen/schemas/bundleSectionKind.jsonGenerated contract+15-0
src/gen/schemas/cardAmount.jsonGenerated contract+105-0
src/gen/schemas/cardAssertion.jsonGenerated contract+95-0
src/gen/schemas/cardDate.jsonGenerated contract+103-0
src/gen/schemas/cardParty.jsonGenerated contract+100-0
src/gen/schemas/chronologyEvent.jsonGenerated contract+107-0
src/gen/schemas/citedValueDate.jsonGenerated contract+90-0
src/gen/schemas/citedValueStr.jsonGenerated contract+90-0
src/gen/schemas/confirmationStatus.jsonGenerated contract+7-0
src/gen/schemas/contextBundle.jsonGenerated contract+1662-0
src/gen/schemas/contradictionItem.jsonGenerated contract+163-0
src/gen/schemas/datePrecision.jsonGenerated contract+7-0
src/gen/schemas/documentCard.jsonGenerated contract+683-0
src/gen/schemas/documentFocus.jsonGenerated contract+48-0
src/gen/schemas/documentIdentityCard.jsonGenerated contract+54-0
src/gen/schemas/documentInsightBrief.jsonGenerated contract+1-1
src/gen/schemas/documentSelectionBundleRequest.jsonGenerated contract+64-0
src/gen/schemas/edgeType.jsonGenerated contract+14-0
src/gen/schemas/evidenceHit.jsonGenerated contract+95-0
src/gen/schemas/matterBriefFrameView.jsonGenerated contract+9-0
src/gen/schemas/matterBriefView.jsonGenerated contract+9-0
src/gen/schemas/matterFrameEditRequest.jsonGenerated contract+14-0
src/gen/schemas/sectionSummaryItem.jsonGenerated contract+56-0
src/gen/types/Assertion.tsGenerated contract+67-0
src/gen/types/AssertionEdge.tsGenerated contract+54-0
src/gen/types/AssertionKind.tsGenerated contract+20-0
src/gen/types/BuildWatermark.tsGenerated contract+17-0
src/gen/types/BundleSection.tsGenerated contract+49-0
src/gen/types/BundleSectionKind.tsGenerated contract+20-0
src/gen/types/CardAmount.tsGenerated contract+39-0
src/gen/types/CardAssertion.tsGenerated contract+29-0
src/gen/types/CardDate.tsGenerated contract+39-0
src/gen/types/CardParty.tsGenerated contract+33-0
src/gen/types/ChronologyEvent.tsGenerated contract+44-0
src/gen/types/CitedValueDate.tsGenerated contract+27-0
src/gen/types/CitedValueStr.tsGenerated contract+27-0
src/gen/types/ConfirmationStatus.tsGenerated contract+13-0
src/gen/types/ContextBundle.tsGenerated contract+72-0
src/gen/types/ContextDocumentSelectionBundle.tsGenerated contract+78-0
src/gen/types/ContradictionItem.tsGenerated contract+24-0
src/gen/types/DatePrecision.tsGenerated contract+13-0
src/gen/types/DocumentCard.tsGenerated contract+71-0
src/gen/types/DocumentFocus.tsGenerated contract+34-0
src/gen/types/DocumentIdentityCard.tsGenerated contract+49-0
src/gen/types/DocumentSelectionBundleRequest.tsGenerated contract+26-0
src/gen/types/EdgeType.tsGenerated contract+19-0
src/gen/types/EvidenceHit.tsGenerated contract+35-0
src/gen/types/MatterBriefFrameView.tsGenerated contract+9-0
src/gen/types/MatterFrameEditRequest.tsGenerated contract+8-0
src/gen/types/SectionSummaryItem.tsGenerated contract+76-0
src/gen/types/index.tsGenerated contract+54-2
src/gen/zod/assertionEdgeSchema.tsGenerated contract+54-0
src/gen/zod/assertionKindSchema.tsGenerated contract+25-0
src/gen/zod/assertionSchema.tsGenerated contract+48-0
src/gen/zod/buildWatermarkSchema.tsGenerated contract+23-0
src/gen/zod/bundleSectionKindSchema.tsGenerated contract+25-0
src/gen/zod/bundleSectionSchema.tsGenerated contract+56-0
src/gen/zod/cardAmountSchema.tsGenerated contract+36-0
src/gen/zod/cardAssertionSchema.tsGenerated contract+30-0
src/gen/zod/cardDateSchema.tsGenerated contract+39-0
src/gen/zod/cardPartySchema.tsGenerated contract+36-0
src/gen/zod/chronologyEventSchema.tsGenerated contract+34-0
src/gen/zod/citedValueDateSchema.tsGenerated contract+23-0
src/gen/zod/citedValueStrSchema.tsGenerated contract+23-0
src/gen/zod/confirmationStatusSchema.tsGenerated contract+17-0
src/gen/zod/contextBundleSchema.tsGenerated contract+63-0
src/gen/zod/contextDocumentSelectionBundleSchema.tsGenerated contract+94-0
src/gen/zod/contradictionItemSchema.tsGenerated contract+34-0
src/gen/zod/datePrecisionSchema.tsGenerated contract+17-0
src/gen/zod/documentCardSchema.tsGenerated contract+85-0
src/gen/zod/documentFocusSchema.tsGenerated contract+40-0
src/gen/zod/documentIdentityCardSchema.tsGenerated contract+45-0
src/gen/zod/documentSelectionBundleRequestSchema.tsGenerated contract+25-0
src/gen/zod/edgeTypeSchema.tsGenerated contract+24-0
src/gen/zod/evidenceHitSchema.tsGenerated contract+35-0
src/gen/zod/index.tsGenerated contract+36-0
src/gen/zod/matterBriefFrameViewSchema.tsGenerated contract+8-0
src/gen/zod/matterFrameEditRequestSchema.tsGenerated contract+7-0
src/gen/zod/sectionSummaryItemSchema.tsGenerated contract+52-0
src/lib/documents/gateway.tsLibrary+41-0
src/lib/kubb-client.tsLibrary+12-0