Read two AskUserQuestion shapes that fall through to the raw mirror - #51
Read two AskUserQuestion shapes that fall through to the raw mirror#51konpyl wants to merge 1 commit into
Conversation
…rror Both reported panes showed a question as a wall of 179-column terminal text with nothing to tap. Different repos, different shapes, one root mistake: a grammar asserting a layout instead of reading it. WRAPPED OPTION LABELS (preview variant) The grammar required its numbered rows to be on consecutive lines. The assumption was written down as fact — // (the left column never wraps its 30-col labels) — and this variant's gutter is only ~30 wide, so Claude Code wraps any longer label onto continuation rows and the numbered rows stop being adjacent. The whole dialog fell to raw. Observed live: option 1's label "Rename + keep old as hidden aliases (Recommended)" spanned three rows. Adjacency is replaced by a contiguity walk, with the LABEL COLUMN as the thing that was missing: a wrapped line hangs under its label, so left of that column is a new option, at it is label text, and right of it is not label text at all. One rule, three cases. Without it, a label ending "…and 3. Backfill later" mints a phantom third button that types a stray digit, and — where the Notes column and preview pane don't coincide — box-drawing gets folded onto a button and dragged into the core signature, making it pointer-DEPENDENT, the one property the race guard needs it not to have. The column is taken only from a line that genuinely parses as an option row: a bare "12." matches the prefix shape but carries no option, and would widen the column that decides what an option is. Also: `chosen` is tracked per row rather than tested against the joined label (a revisited question paints its ✔ mid-string once the label wraps), and the grammar bails past 9 options as the single-select one already did, since a two-key digit is unsendable. MULTISELECT INSIDE A WIZARD A checkbox question that is one step of a multi-question dialog was owned by nobody. wizard.ts refuses it correctly — a wizard digit selects AND advances while a checkbox digit only toggles, so claiming it would mis-drive the dialog — and multi-select only ever handled the single-question form. Two gates encoded that: a stepper of exactly two chips, and an advance row spelled "Submit". multi-select now carries the wizard's chips (parsed by the wizard's own parseStepperLine — the chip grammar stays in one place) and navigates between questions with the wizard's own Left/Right keys, following preview-select, which already solved this same shape. The advance row's label is read rather than assumed: it is "Next" on every step but the last. Hard-coding either word would have broken half the steps of every multi-question dialog. The advance row is located by POSITION — last non-blank line above the rule that separates the menu from the escape row — never by scanning for text. Pane content is model-authored and untrusted, and matching on the word alone let an option DESCRIPTION reading "Next" rename the button to an action it doesn't perform, satisfy the "row absent → fail closed" check on a pane that has no advance row (leaving the macro to spray Down keys hunting for it), and, through pointerAt, forge the pointer so Enter landed on whatever row the terminal's ❯ was really on — which on "Chat about this" aborts the whole tool call. The macro stops at the keystroke instead of predicting what follows. It walks the pointer onto the advance row, re-reads, verifies the pointer is there in that same iteration, then presses Enter — and lets the next poll re-detect whatever appeared, be it the next question or the wizard's review. Predicting another grammar's screen is the assumption that made this brittle to begin with. The single-question review stays anchored where it was; a multi-question dialog reviews once at the end and that screen belongs to the wizard. The race guard now compares the steps themselves. `signature` normalises ☒/☑ → ☐ across the whole chip line — it has to, since the current question's chip flips on the first tick — but that also erases which step you are on, so two questions of one wizard sharing a question text and option labels were byte-identical to the guard, and a tap meant for the first could land on the second. EVIDENCE The wizard-multiselect fixtures are sandbox captures of a real two-question dialog driven end to end, because nothing in the repo described this shape and guessing feeds a keystroke into a live terminal. They settled four things: the chip flips on the FIRST tick (answered means touched, not complete), digits toggle pointer-independently inside a step exactly as they do standalone, the last step reads "Submit", and the footer is not invariant — it gains "ctrl+g to edit in Vim" when the pointer reaches the advance row, which is why the signature already stops before it. The wrapped-label fixture is DERIVED, not captured: its live specimen was real work in a public-repo fixture corpus, so the gutter of claude--wizard-preview-q1 was rewritten and everything from the Notes column rightward carried over. The stepper strip is extracted rather than hand-rolled a third time. multi-select-action.ts carried a literal NUL byte as a map-key separator, which made git classify the file as binary and hide every change to the race guard from review. Same value, written as a source escape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a0e2aee to
b297e3d
Compare
|
Rebased onto Worth confirming the change is still needed rather than just still applying: if (rows.some((r, k) => r.n !== k + 1 || r.index !== rows[0]!.index + k)) return null;— so a wrapped option label still drops the whole dialog to the raw mirror. Also, thank you for |
Two AskUserQuestion shapes currently fall through to the raw mirror. On a phone that means a wall of ~180-column terminal text with nothing to tap and no way to answer — both were hit in normal use today, in different repos.
Both come from the same root mistake: a grammar asserting a layout instead of reading it.
1. A wrapped option label kills the preview dialog
preview-selectrequired its numbered rows to be on consecutive lines. The assumption is written down as fact:That gutter is only ~30 columns, so Claude Code wraps any longer label onto continuation rows and the numbered rows stop being adjacent — the whole dialog bails to raw. Observed live: an option labelled
Rename + keep old as hidden aliases (Recommended)occupied three rows, putting the options on lines 183/186/187.Reproduced by running the real
parseAnsi → splitLines → buildBlockspipeline over the capture, and flipped by the single change of making those rows adjacent — nothing else about the dialog mattered.Adjacency is replaced by a contiguity walk keyed on the label column (the width of the
❯ 1.prefix): left of it is a new option, at it is label text, right of it is not label text at all. That one rule also prevents a label ending…and 3. Backfill laterfrom minting a phantom third button, and stops preview-pane box-drawing being folded onto a button in layouts where theNotes:column and the preview pane don't coincide.2. A multiSelect question inside a multi-question wizard is owned by nobody
wizardrefuses it correctly — a wizard digit selects and advances, a checkbox digit only toggles, so claiming it would mis-drive the dialog — andmulti-selectonly ever handled the single-question form. Two gates encoded that: a stepper of exactly two chips, and an advance row spelledSubmit.multi-selectnow carries the wizard's chips (via the wizard's ownparseStepperLine, so the chip grammar stays in one place) and navigates with the wizard's own Left/Right keys, following the patternpreview-selectalready established for this exact shape. The advance row's label is read, not assumed: it isNexton every step but the last.The row is located by position — last non-blank line above the rule separating the menu from the escape row — never by scanning for the word. Pane text is model-authored; matching on text alone let an option description reading
Nextrename the button to an action it doesn't perform, satisfy the "row absent → fail closed" check on a pane with no advance row, and forge the pointer soEnterlanded on whatever row the terminal's❯was really on (onChat about this, that aborts the whole tool call).The advance macro stops at the keystroke rather than predicting what follows — it verifies the pointer in the same iteration it presses Enter, then lets the next poll re-detect whatever appeared. The single-question review stays where it was; a multi-question dialog reviews once at the end, and that screen belongs to
wizard.Evidence
The
claude--wizard-multiselect-*fixtures are sandbox captures of a real two-question dialog driven end to end, because nothing in the repo described this shape and guessing feeds a keystroke into a live terminal. They settled four things worth knowing:☐ → ☒on the first tick — "answered" means touched, not complete;Submit, earlier onesNext;ctrl+g to edit in Vimwhen the pointer reaches the advance row, which is why the signature already stops before it.claude--wizard-preview-wrapped-label.txtis derived, not captured: its live specimen was real work and this repo is public, so the gutter ofclaude--wizard-preview-q1.txtwas rewritten and everything from theNotes:column rightward carried over untouched. Flagged as derived in the fixtures README.Existing single-question dialogs are unchanged (
steps: null, review phase untouched), and every non-dialog fixture still yields zero detections.Also in here
signaturenormalises☒/☑ → ☐across the whole chip line — it has to, since the current chip flips on the first tick — but that also erases which step you are on, so two questions of one wizard sharing a question text and option labels were byte-identical to the guard.wizard,preview-selectandmulti-selectall render it from the same place instead of three copies.multi-select-action.tscarried a literal NUL byte as a map-key separator, which made git classify the file as binary and hide every change to the race guard fromgit diff. It's now a source escape, plus a.gitattributesrule so a future stray byte can't cost reviewability.Notes for review
prompt-binding-regions.json; the completeness gate and the bridge-side binding test both pass.main: 1271 web tests pass,tscclean.🤖 Generated with Claude Code