diff --git a/docs/designs/ui/compass-ask-free-text/design.md b/docs/designs/ui/compass-ask-free-text/design.md new file mode 100644 index 00000000..dd5bed5d --- /dev/null +++ b/docs/designs/ui/compass-ask-free-text/design.md @@ -0,0 +1,1158 @@ +# Always-explicit ask submission + free-text (`custom_text`) answers (RIG-1330) + +Status: Active +Tracker: RIG-1330 + +Ledger placeholder: the decisions below are marked +`DL-TBD-1`…`DL-TBD-5` — real row ids are allocated when this record's PR +flips `docs/designs/DECISIONS.md`, never by this draft. + +## Problem / Intent + +Three problems, one send model. + +**Free-text questions are unanswerable.** An `AskQuestion` whose `options` list +is empty is a free-text question — the wire says so: "Free-text answer +(\"Other\"); a question with no options is answered by custom_text alone" +(`AskQuestionAnswer.custom_text` in `proto/compass/v1/comms.proto`). The UI +renders such a question as an empty `.ask-options` container with no input +(`AskBlock` in `apps/ui/src/components/ChannelView.tsx` — the only per-question +content is ``), so a free-text question cannot be +answered. This record designs the free-text answer path end to end: domain +shape, store surface, outbound wiring, renderer, and completion accounting. + +**Asks auto-send, and they must not (Matt's ruling).** Today the option click +that completes an ask ships the one unrepeatable `RespondToAsk` with no +confirmation — the tail of `answerAsk` in `apps/ui/src/store.ts`; a +single-question ask sends on its only click. Matt's ruling on this record: **an +ask is never auto-sent — submitting a response is always an explicit user +gesture.** That changes **already-shipped behaviour**, not just the new +free-text path: the completing-click send and the single-click single-question +send are removed, and the submit control becomes the sole, unconditional send +path (D0). The existing gate comment argued only against a *per-click* respond +("don't send early"); it never argued "send without confirmation" — auto-send +was simply where the gate landed once partial sends were ruled out. Meanwhile +the wire is one-shot and unrepeatable (`applyAskAnswer`'s answer-once guard in +`go/internal/store/messages.go` returns `ErrConflict` on a second respond, and +is the sole single-fire mechanism), a first toggle on a multi-select question +can already complete an ask before the user's second pick, and free text widens +that sharp edge from a rare shape to a common one (typed text counting toward +completeness). Every other destructive one-shot in this UI has an explicit +control; the ask now does too. + +**And the shipped UI is a strict subset of the contract (Matt's directive: +full parity).** The wire already carries every presentation axis the native +tool has — `header`, `recommended`, `AskOption.preview` — and free text on +every question; the UI renders none of them and offers the input nowhere. +The contract work is done (the merged derivation record cited in Approach); +the parity gap is entirely in the UI layer, and this record closes it: D1 +widens the domain type, D4 puts the free-text input on every question, D6 +renders the presentation axes. + +Non-goals: proto or server changes, and timeout auto-selection (`timed_out`, +RIG-1310 — the one wire field that stays dropped). One deliberate divergence +from the native tool, and the only one: a single-question single-select ask +auto-submits natively ("A single-select question still submits immediately +when it is the only question", `omp://tools/ask.md` § Modes / Variants); +here it never does — Matt's ruling that an ask is never auto-sent (D0) +removes exactly that behaviour. Do not "fix" this later: the non-parity is +intentional, and this line is its record. + +## Approach + +Eight decisions. The through-lines: **nothing sends but an explicit submit** +(D0); the typed text is a **local pick with exactly the lifecycle of +`chosenOptionIds`** (D1-D5), so it lives in the same place and rides the +same machinery — the local ask copy in the store's message state — rather +than a parallel keyed map; and the UI renders **every wire axis except +`timed_out`** (D1, D6), full parity with the native tool. + +"Explicit" means a deliberate confirmation, **not a mouse**: the intended +flow is answer every question, tab to submit, press Enter. That keyboard path +is a first-class requirement (D5), not an accessibility footnote — it comes +free from using a native `