From 112fcf593f31866acfdeda7ae961ded4e50b9105 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 14 Aug 2026 16:41:02 +0200 Subject: [PATCH] docs(framing): bottom-panel revision 11 --- consume the chrome wheel before ACTIVATION Answers review of revision 10. Still framing only; no implementation. **"CONSUME BEFORE `apply_terminal_gesture`" WAS NOT EARLY ENOUGH.** `activates` is `!matches!(kind, Move)` for a terminal panel (`src/editor.rs:2695`), which INCLUDES the wheel, and focus plus `active_frontend` are written at `:2699` --- ahead of any replay decision. A consume check below that block leaves the wheel CHANGING FOCUS while scrolling nothing and claiming no controller: exactly the half-state AC48's activate-then-claim rule exists to prevent. The panel steals focus and does not move. A terminal-chrome wheel is therefore not a terminal gesture at all, and the dispatcher must treat it that way. The order is now stated: 1. authenticate and validate (the existing ladder) 2. resolve the side window and its buffer kind 3. terminal + chrome wheel -> CONSUME IMMEDIATELY, before `focus_window`, `active_frontend`, any controller claim, any command-chain mutation, and the shared terminal path 4. otherwise, the existing activation and replay rules Step 3 is implementable where it belongs: `is_terminal` is resolved from `buffer_id` at `:2687`, before the `core` borrow and before the activation block, so the kind is already in hand. **The witness gains focus and controller assertions, and they are load-bearing.** The document->terminal replacement row now asserts the focused window and terminal controller identity are unchanged, on top of no child bytes, no scrollback and no document movement. Two mutations bite different halves: - terminal branch calls `apply_terminal_gesture` -> the chrome coordinate fails the reporting bounds check, falls into the local branch, and the row catches the accidental local scrollback - the consume check sits BELOW activation -> nothing scrolls, so every movement assertion still passes and ONLY the focus and controller assertions catch it That second mutation is the reason the new assertions exist. Without them the row stays green against an implementation that steals focus on every chrome wheel over a terminal, because the half-state is invisible to any assertion about movement --- nothing moves either way. Gates: all nine green under `env -u TMPDIR`, log 20260814T143558Z. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 12 +++++- docs/bottom-panel-framing.md | 73 +++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index fc95bb4..980d262 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -281,7 +281,7 @@ from #171 and #215 — the correction the 1b lane missed, honoured here. **`githubsucks/panel-pointer-replay` is the authoritative tip** (the ref, not a SHA). Recover with `git fetch githubsucks && git checkout panel-pointer-replay`. -- **No PR yet. Checkpoint: framing revision 10 (§5a) AWAITING APPROVAL; +- **No PR yet. Checkpoint: framing revision 11 (§5a) AWAITING APPROVAL; NO IMPLEMENTATION WRITTEN.** Commit one was the ground-truth re-measurement; 6 added the four replay edges; **7 answers review of 6; **8 answers review of 7** — R-c is target × gesture-ORIGIN @@ -310,6 +310,16 @@ from #171 and #215 — the correction the 1b lane missed, honoured here. the side window and decides: document → `scroll_window`, terminal → consume. Witness: one frontend across a document→terminal replacement. + **Revision 11** fixes the ORDERING: the terminal-chrome wheel is + consumed **before activation**, not merely before + `apply_terminal_gesture`. `activates` is `!matches!(kind, Move)` for + a terminal (`src/editor.rs:2695`), so the wheel already writes focus + and `active_frontend` at `:2699` ahead of any replay decision — a + consume check below that would change focus while scrolling nothing + and claiming no controller. Four-step order, consumption at step 3; + the witness now asserts **focus and controller identity unchanged**, + which is what catches the consume-below-activation mutation, since + that mutation moves nothing. - **Why this lane exists.** `PanelPointer` **replays nothing**: `dispatch_semantic_panel_pointer` (`src/editor.rs:2674`) validates, focuses, returns. A panel wheel is dead on both axes and so is every diff --git a/docs/bottom-panel-framing.md b/docs/bottom-panel-framing.md index 3c45220..cbb8c68 100644 --- a/docs/bottom-panel-framing.md +++ b/docs/bottom-panel-framing.md @@ -7,8 +7,25 @@ protocol v20, 2026-07-24. Amended by the pre-implementation dependency verification in §0.6: the folding dependency is cleared, and one geometry caller-census error is corrected.** -**Revision 10 — 2026-08-14, AWAITING APPROVAL.** Answers review of 9, -which put a correct ruling **on the wrong side of the seam**. +**Revision 11 — 2026-08-14, AWAITING APPROVAL.** Answers review of 10. + +**The terminal-chrome wheel must be consumed BEFORE ACTIVATION**, not +merely before `apply_terminal_gesture`. `activates` is +`!matches!(kind, Move)` for a terminal panel +(`src/editor.rs:2695`), so the wheel already activates: focus and +`active_frontend` are written at `:2699` ahead of any replay decision. +A consume check below that block would leave the wheel **changing focus +while scrolling nothing and claiming no controller** — the precise +half-state AC48's activate-then-claim rule exists to prevent. §5a now +states the four-step order, with consumption at step 3, and the +document→terminal witness asserts **focus and controller identity +unchanged** alongside no bytes, no scrollback and no document +movement — assertions that are load-bearing, because the +consume-below-activation mutation moves nothing and is invisible +without them. + +**Previously, revision 10 — SUPERSEDED.** Answered review of 9, which +put a correct ruling **on the wrong side of the seam**. **The GPU cannot know whether a panel holds a terminal.** `PanelFrame` carries `buffer_id`, both epochs, `size`, `cells`, `cursor` and @@ -2194,7 +2211,7 @@ wheel is the sole divergence, and it is now decided receiver-side. | chrome `Down`/`Drag` | reserved — drop | drop, matching the TUI | | crossing `Drag` (normalized) | process when it arrives | process at the content coordinate | | crossing `Up` (normalized) | terminate the gesture | **replay at the last valid CONTENT coordinate** — never the chrome row | -| chrome wheel | **process through `scroll_window`** | **CONSUME** — no child bytes, no local scrollback, no document fallthrough (Q#BP-R2) | +| chrome wheel | **process through `scroll_window`** | **CONSUME, before activation** — no focus change, no controller claim, no child bytes, no local scrollback, no document fallthrough (Q#BP-R2) | **The chrome wheel carries a chrome coordinate over the wire, and that is fine**: it is a valid frame cell, the daemon's coord validation @@ -2244,19 +2261,57 @@ the panel holds a terminal (see the seam note above). The producer claims the chrome wheel and sends it for **every** panel; the daemon resolves the side window and consumes it when the buffer is a terminal. +##### And it is consumed BEFORE ACTIVATION, not merely before replay + +**"Consume before `apply_terminal_gesture`" is not early enough.** +Every non-`Move` terminal panel gesture activates the side window +first, and `activates` is computed as `!matches!(kind, Move)` +(`src/editor.rs:2695`–`:2698`), which **includes the wheel**. Focus and +`active_frontend` are written at `:2699`–`:2701`, *before* any replay +decision. + +A consume check placed after that block leaves the exact half-state +AC48's activate-then-claim rule exists to prevent: **the wheel changes +FOCUS while scrolling nothing and claiming no controller.** The panel +steals focus and does not move. + +**A terminal-chrome wheel is not a terminal gesture at all**, and the +dispatcher must treat it that way. The order is: + +1. Authenticate and validate the panel event (the existing ladder). +2. Resolve the side window and determine its **buffer kind**. +3. **If terminal + chrome wheel: consume IMMEDIATELY** — before + `focus_window`, before `active_frontend`, before any controller + claim, before any command-chain mutation, and before the shared + terminal path. +4. Otherwise, the existing activation and replay rules. + +Step 3 is implementable where it needs to be: `is_terminal` is resolved +from `buffer_id` **before** the activation block, so the kind is +already known at that point. + **Witness — one frontend across a document→terminal replacement**, so the two outcomes are separated by nothing but the buffer kind: 1. panel shows a **document**; wheel over chrome → **the panel scrolls**. 2. the panel is replaced by a **terminal**; wheel over chrome → - **nothing changes** — no child bytes, no scrollback movement, no - document scroll. + **nothing changes at all** — no child bytes, no scrollback movement, + no document scroll, **and the focused window and terminal controller + identity are unchanged**. -*Mutation: let the terminal branch call `apply_terminal_gesture` — the -chrome coordinate fails its reporting bounds check, falls into the -local branch, and the row catches the **accidental local scrollback** -that revision 8's clamp would have shipped deliberately.* +Two mutations, biting different halves: + +| mutation | must fail | +|---|---| +| the terminal branch calls `apply_terminal_gesture` | the chrome coordinate fails its reporting bounds check, drops into the local branch, and the row catches the **accidental local scrollback** revision 8's clamp would have shipped deliberately | +| the consume check sits **below** the activation block | nothing scrolls, so bytes/scrollback/document all still pass — **only the focus and controller assertions catch it** | + +**The second mutation is why the focus and controller assertions are +load-bearing.** Without them the row is green against an +implementation that steals focus on every chrome wheel over a +terminal — the half-state is invisible to any assertion about +movement, because nothing moves either way. Doing it in one frontend across a replacement is what makes it a control rather than two unrelated observations: the geometry, the