fix(webui): C-Space sets the Playbook mark and motion extends the region - #1298
Merged
Conversation
The web Playbook editor had no mark at all. `C-Space` fell through
`playbookOnKeyDown` — which bound only `C-f` out of the TUI's movement
keys — to the contenteditable underneath, where Chrome took it as a
literal space and typed one into the document. The TUI has had the full
emacs contract for this since the Playbook popup shipped
(`begin_playbook_selection` + `update_playbook_selection_head`), so the
two clients disagreed on a binding the shared keymap implies.
Give the web editor the same contract, built on the browser's own
selection rather than a shadow offset pair: the DOM selection's anchor
*is* the mark, so extending is `Selection.modify("extend", …)` and
nothing has to stay in sync with a DOM the renderer rewrites.
- `C-Space` arms the region at the caret, with `preventDefault` so the
contenteditable never sees the space. Verified against a trusted CDP
key event, not a synthesized one — the default action is the bug.
- While armed, every motion extends from the mark: arrows, Home/End,
`C-f`/`C-b`/`C-n`/`C-p`/`C-a`/`C-e`, and word / line-boundary /
document-boundary motions under each platform's modifier. Alt combos
match on `code` because macOS turns `M-f` into `ƒ`.
- `C-g` and `Escape` cancel, leaving the caret put and the document
untouched. Copy and cut act on the region and then disarm the mark.
- An edit, a pointer press, blur, or mounting another session's Playbook
ends the region; native mouse and shift selection are untouched.
Adds spec 0206 for the cross-client contract, and a web_smoke section
that drives real CDP key events through set → extend → cancel. Confirmed
non-vacuous: disabling the `C-Space` branch fails the new assertions.
edwin-zvs
added a commit
that referenced
this pull request
Aug 21, 2026
0206 was taken by the Playbook mark-and-region spec (#1298) while this branch was open.
edwin-zvs
added a commit
that referenced
this pull request
Aug 21, 2026
* feat(tui): replay the focus sweep when typing resumes after idle Focus acquisition answers "where did my focus go" at the instant it moves. It says nothing to a user coming back to a terminal they left minutes ago, where a fleet of similar panes gives the eye nothing to lock onto. Replay the same sweep on the already-focused pane when a keystroke reaches it after a long silence. The transition animates, not the typing: every key the focused pane consumes refreshes the idle clock, and a key landing while a sweep is already on screen is absorbed, so focus gain and idle resume can never stack. Keys swallowed by a modal, popup, or the prompt strip are not pane input and animate nothing. Threshold: 120s, in a named constant beside the other sweep constants, with a CONSTRUCT_FOCUS_SWEEP_IDLE_MS override for recordings. * chore: temporary PR media for the idle focus sweep * chore: drop temporary PR media * chore(specs): renumber the idle focus-sweep spec to 0207 0206 was taken by the Playbook mark-and-region spec (#1298) while this branch was open.
This was referenced Aug 21, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
C-Spacein the web Playbook editor now sets the mark, and the next caret motion extends the region from it — the binding the TUI has always had.Mark set at "The·|quick", then
M-f×3 →Down→C-e. Status readsmark set; the selection verb menu comes up on its own once the region is non-empty. (full-window shot)Root cause
Not a swallowed keystroke —
C-Spacereached the page fine. The web editor had no mark at all.playbookOnKeyDownbound exactly one of the TUI's movement keys (C-f) and had no notion of a region.C-Spacefell straight through to thecontenteditableunderneath, where Chrome did the only thing it could with it: typed a literal space into the document. So the binding didn't just fail — it corrupted the playbook you were composing.The TUI has had the full contract since the Playbook popup shipped:
begin_playbook_selectionsetsanchor == head == cursor, and every motion arm callsupdate_playbook_selection_head. Two clients, one keymap, one of them missing half the vocabulary.Ruled out along the way, since the brief flagged them:
handleChordKey) doesn't touch it —keyTokenmaps Ctrl+Space toC-space, which is bound to nothing and is not a prefix of theC-x Spacechord, so it returns false and the event continues to the editor's own listener.C-Spaceas the input-source switcher, but that's above the browser and no client can override it. Confirmed the keydown arrives and is prevented (below).Fix
Give the web editor the same contract, built on the browser's own selection instead of a shadow offset pair. The DOM selection's anchor is the mark, so extending is just
Selection.modify("extend", …)— nothing has to stay in sync with a DOM the renderer rewrites underneath us.C-Spacearms the region at the caret, withpreventDefault()so the contenteditable never sees a space.Home/End,C-f/C-b/C-n/C-p/C-a/C-e, plus word / line-boundary / document-boundary motions under each platform's modifier. Alt combos match one.code, because macOS turnsM-fintoƒandM-binto∫ine.key.C-gandEscapecancel — caret stays put, document untouched.C-gpeels the clip menu and Find first, like the TUI's.Adds spec 0206 for the cross-client contract. It's deliberately scoped so it doesn't fight spec 0059 (web parity is by capability, native affordances stay the baseline): it constrains only the mark bindings a client chooses to offer — but requires the whole contract rather than a fragment, since a half-implemented mark is what produced this bug.
Verification
Not eyeballed. Driven in a real Chrome over CDP against a hot-reloaded daemon (
CONSTRUCT_ASSETS_DIR), with trustedInput.dispatchKeyEvent— a synthesizedKeyboardEventnever runs the default action, which is the entire bug.Each motion also checked individually (
C-b,C-p,C-a,C-e,M-f,M-b,Home, arrows), plus no-regression checks: typing replaces the region and disarms, click disarms, mouse drag still selects natively,Escapecancels,copyreturns the region and disarms.One thing worth naming: reversing direction across the mark (
C-ethenC-a) selects the whole line rather than collapsing. That's Chrome's ownSelection.modifybehavior under macOS editing semantics — a plain contenteditable does exactly the same onShift+End→Shift+Home, verified side by side. Matching the platform is the right answer here, so it's left alone.Test
crates/e2e/tests/web_smoke.rsgains a mark section that drives set → extend → cancel with real CDP key events (addedpress_key_chord, sincepress_keycan't carry modifiers), assertingdefaultPrevented, the extended text, the selection menu, and that the document never changes.Confirmed non-vacuous: with the
C-Spacebranch commented out, the new assertions fail —(Also caught that
web_smokewas silently skipping locally on a stalechromiumoxide-runner/SingletonLock— worth knowing when reading a green local run of that file.)cargo test --workspacegreen: 57 targets, 0 failures.Binary
Web-UI only — the change is
crates/daemon/assets/index.html, embedded into the daemon, which lives in theconstructbinary. Nothing else moves.