Components / Decision Dock

Decision Dock

The formal tier of the two-tier decision system: a quiet strip pinned above the composer that walks the user through everything the system is blocked on, one decision at a time. Consent moments (tool approvals, plan spend gates) and structured questions (setup wizards, data collection) alike. Never for the conversation’s own casual next beat (that is DecisionPills), and never for approving a document (artifact consent happens on the canvas).

import { DecisionDock } from '@layers/ui/decision-dock'

Rendered stock

House rules

  • Two tiers, one vocabulary: DecisionPills is the casual fork (one or two options, tap sends, no confirm); DecisionDock is the formal queue. The threshold is weight, a product-level call: quantity (a batch to walk), verbosity (options that need descriptions or a fact block to read), or consequence (an explicit Confirm). Formalizing every pill makes chat feel like paperwork; burying formal decisions in the stream loses them.
  • It is not a Card: no card surface, no icon gutter. Presence comes from the base background behind a plain 1px border (the Code Block treatment; a one-off piece of UI that needs to feel present). It pins above the composer and is never an entry in the stream. The composer stays live beneath it.
  • A walk split into several small docks passes position={{offset,total}} instead, and the dock renders offset + the item it is on: one reading that advances on every step, inside a dock and across docks. It is a READING, not a scrubber — the chevrons page through this dock's own items and nothing else, so a one-question dock shows the count and no arrows at all. Pairing a walk-wide total with controls that can never reach those numbers is two dead affordances beside a count claiming nine steps. It is the number a person wants (how far through the whole thing, not how far through this card) and it is the only one a one-question dock can honestly show. Pass it only when the total is fixed: a count that grows while someone watches it is worse than no count.
  • The eyebrow is NEVER customized per stage (founder ruling 2026-08-06): every step of a walk renders the same countNoun form — "Question 4 of 7", never "Ads · 4 of 7". There is deliberately no label slot on position: a topic prefix makes one walk read as several surfaces, and the question title below the eyebrow already says what the stage covers.
  • One decision at a time: short formal queues use an honest count up top, with both chevrons always mounted and disabled at the walk bounds. The count eyebrow takes the surface noun via countNoun (Approval on a consent surface, Question in a wizard, Decision by default), and the same noun labels the chevrons when showCount={false}. Suppress the count for long intakes: a nine-question countdown reads as a form, and the count is walk furniture rather than part of the decision. A walk of ONE drops the count and chevrons entirely (the question owns the strip), so the dock scales down to a single quiet decision.
  • One commit model: the dock is a WIZARD. The item set is fixed, any shown count is a stable N of total that never shrinks, chevrons navigate freely both ways to review and change earlier answers, non-final items advance with Next, and NOTHING submits until the final Confirm, which commits the whole answer set at once. A decision is never submitted in the middle of the walk; this holds for approvals exactly as for questions.
  • One layout, and it is not a per-call-site choice (founder ruling 2026-08-03). A layout prop once let a caller render the whole batch at once, and both call sites selected it on count > 1 — the exact condition that makes a walk a walk, so the second question became unreachable. Multi-question decisions walk. A batch that genuinely has to be read at once is a decision-TYPE affordance with its own justification, never a switch on the general ask_user path.
  • Bounded by construction: the dock is capped and only the QUESTION scrolls. The count header and the commit action stay on screen at every viewport height. It hangs in a shrink-0 chat footer, so nothing upstream bounds it — a verbose question with no cap ran the strip off the bottom of the viewport with no scroller anywhere to reach the action.
  • Options are Choice Cards (the FieldLabel radio-card variant). A consent item lists the recommended choice first and preselected, and always includes a way out (Decline, Skip). A data-collection item may have no recommendation: nothing preselects and Confirm waits for a pick.
  • The recommendation is never expressed by preselection alone, and it never breaks height uniformity: in a blurbed question a blurb-less recommended option reads "Recommended" in its description slot; in an all-bare question the marker rides the label line ("Approve (recommended)").
  • Blurbs are uniform per question: within one item, either every option carries a blurb (two-line cards) or none does (one-line cards). Never mix heights in the same question.
  • The fact block (item.rows) is optional context the user reads to decide: a left-aligned label/value grid, never a raw payload dump. Omit it when the option descriptions already carry the decision.
  • Disclosure lines (item.notices) are server-authored sentences the user must read before consenting — a plan that changed while it was being repaired, a parameter the system altered. They render as one warning Alert between the fact block and the options, above the commit action at every viewport height. Pass them through verbatim: the caller never composes or rewords a disclosure, because the whole value of the sentence is that the server wrote it.
  • Committing spins inside the Confirm button (the Button loading prop), never replacement text. A failure shows an honest inline error and the item stays.
  • The final Confirm dissolves the whole walk (the caller removes the dock once its substrate settles). The dock is an affordance, never a record: the record of the decisions is the text or tool outcomes in the stream.
  • onAnswerChange reports the answer set as it changes, for a surface that has to REACT to an answer mid-walk: onboarding reveals the connection sources an answer implies on the canvas beside the dock, question by question. Reporting is not committing. onComplete stays the one commit, and a caller that persisted from here would be storing answers the user has not confirmed and can still walk back and change, which is the whole reason the wizard holds its submit to the end. It reports the effective answers a submit would send, so a preselected recommendation the user walked past counts as answered.