Skip to content

Land PR #51: the two dialog shapes that fell through to the raw mirror - #62

Merged
AltanS merged 2 commits into
mainfrom
supersede-pr-51
Aug 3, 2026
Merged

Land PR #51: the two dialog shapes that fell through to the raw mirror#62
AltanS merged 2 commits into
mainfrom
supersede-pr-51

Conversation

@AltanS

@AltanS AltanS commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Supersedes #51, which @konpyl closed on their side without it being merged — the defect it fixes is still live on main (the adjacency check in preview-select.ts still drops any preview dialog with a wrapped option label to the raw mirror, and a wizard-hosted multiselect is still owned by no grammar).

This cherry-picks their commit verbatim with authorship preserved (bdf4c26), plus two commits of ours:

  • test(web): the pinned chrome-fixture corpus table landed in 0.21.0, after this PR's base, and requires a row per fixture on disk — the five new dialog fixtures are pinned with computed (not guessed) values; all classify as dialogs (nothing stripped, no statusline, no draft).
  • chore(release): 0.22.0 — minor bump, changelog credits @konpyl on every line.

Verified on top of 0.21.0: root + web tsc clean, 1369 web tests, 479 bridge tests, ctl lifecycle suite all green.

Thanks @konpyl — this was thorough work (sandbox-captured fixtures, the NUL-byte reviewability fix) and it deserved to land.

🤖 Generated with Claude Code

konpyl and others added 2 commits August 3, 2026 09:15
…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>
…rpus table

The pinned-corpus table (0.21.0) requires every claude fixture on disk to have a row, and the
cherry-picked dialog-grammar commit predates it. All five classify as dialogs: no input box at
the tail, so nothing stripped, no statusline, no draft — computed, not guessed.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
@AltanS
AltanS merged commit 8b10244 into main Aug 3, 2026
1 check passed
@AltanS
AltanS deleted the supersede-pr-51 branch August 3, 2026 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants