Records the overrule and frames the slice (SS5b). No implementation;
the lane is PAUSED behind it.
**ALL THREE OF MY BOUNDS WERE WRONG**, and each is recorded because
each is why the trade looked cheap:
"self-inflicted" another frontend, or a background process, can
edit the same buffer --- the mapping moves with
`view_top` untouched and the clicking frontend
did nothing
"bounded by 3 rows" unbounded: ticks, paging, folds, edits and
reloads accumulate before the new frame is seen
"one round trip" until the frontend PRESENTS the replacement
frame; a slow or backed-up frontend widens the
window arbitrarily
With the narrowness gone there is nothing left to trade on.
**AND THE FIX IS A GENERATION, NOT A TOKEN.** A per-frame token would
invalidate a gesture on every repaint --- the exact mistake
`panel_epoch` exists to avoid, and it would break drags outright.
`mapping_generation` identifies the INVERSE MAPPING: it moves on
viewport, folds, wrap/gutter geometry and buffer content, and holds
across focus, styling, cursor motion and selection-only changes. The
stability half is load-bearing rather than an optimisation --- a drag
provokes selection repaints on every motion, and a generation that
moved with them would cancel the drag after one step.
SS5b frames the slice: appended variants (`PresentMapped`,
`PanelPointerMapped`) rather than widened structs, because postcard
encodes positionally and a shipped variant's field list is frozen;
bilateral gating, with neither side assuming the other's support and a
bare `PanelPointer` from a new-enough peer NOT silently upgraded; and a
drop-before-mutation check at the same rung of the ladder as the epoch
gates, one level finer. Dropping mid-drag is correct: if the mapping
changed, the cells being dragged across no longer mean what they meant.
This is where the hole always was --- `buffer_id` catches replacement,
`panel_epoch` catches close/reopen, `geometry_epoch` catches a
declaration race, and nothing catches "the text under that cell
changed".
Consequence: SS5b takes the next protocol version and GUI arc 1e's
`OpenTarget` moves to the one after. That edit is OWED BY THE 1b
BRANCH, which already revises `docs/gui-stage1-input-framing.md`
heavily; making it here would collide at the rebase 1b is scheduled
for, so it is recorded as required rather than made.
Gates: all nine green under `env -u TMPDIR`, log 20260814T155745Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two of review's findings: the framing blocker, and a live defect in the
commit before this one. The remaining six are queued.
**I REPORTED M-D2 AS BITING AND IT DID NOT.** `presented()` filters on
`frame.geometry_epoch == self.panel.geometry_epoch`, and a geometry
change advances that field FIRST --- so by the time the matching frame
arrives, `presented()` already answers `None` and my
`is_some_and` predicate skipped the reset entirely. The shipped D2 did
nothing on the production sequence.
The witness could not see it because it invented a higher-epoch frame
without driving `next_geometry_declaration`, leaving
`self.panel.geometry_epoch` untouched so `presented()` still matched.
A test that skips the step which breaks the code cannot fail on it.
The predicate now compares against the RETAINED frame
(`self.panel.frame`), which survives the epoch advance, and the witness
drives `GeometryTrigger::Metrics` for real --- asserting along the way
that `presented()` IS `None` in that window, so the trap is pinned
rather than merely avoided. Restoring the `presented()` predicate now
fails the row.
**M-D3 WAS ALSO UNCONSTRAINED**, for a smaller reason: arming clears
`last_pointer_cell`, and the leg only armed, so the field was already
`None` before the replacement and deleting its reset changed nothing.
The arm helper now seeds the baseline with one accepted motion --- what
a real gesture would have produced --- and after replacement the row
requires `panel_motion_is_new` at that same cell to return true.
Deleting only that line now fails.
**Q#BP-R3 IS RULED: current-state hit semantics, narrowly, with the
token named as follow-up.** `PanelPointer` carries epochs and a cell
but nothing identifying the frame CONTENT the user saw, and
`panel_epoch` is stable across ordinary frames by design. So a document
wheel moves `view_top` daemon-side, and a click sent before the new
frame lands is inverted through the NEW `view_top` --- selecting a row
the user never saw, with every validation passing.
Closing it properly needs a per-frame token on `PanelFrame` echoed by
`PanelPointer`: a WIRE CHANGE, and this lane is non-protocol-bearing
with 1b blocked behind it. A daemon-only mitigation was considered and
does not work --- inverting against the last EMITTED frame still cannot
tell which frame the user SAW, and the failing window is identical.
So the lane accepts current-state semantics and says so: the window is
narrow and self-inflicted (the same frontend must move the view and
then click within one round trip), the magnitude is bounded by
`SCROLL_LINES`, and the TUI is structurally unaffected. The token is
recorded as a named follow-up for the next protocol-bearing slice, so
it is inherited rather than rediscovered. Overrule stated explicitly:
the trade is a narrow same-frontend mis-hit now, against serializing
this lane and 1b behind a v25 wire change.
Gates: all nine green under `env -u TMPDIR`, log 20260814T154611Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
SS5a is the acceptance-48 ground truth and its rulings are settled:
Q#BP-R1 (a single click selects a listview row only), Q#BP-R2 (a
terminal-chrome wheel is consumed daemon-side, before activation), the
R-a..R-d replay edges, and the witness matrices A1-A5, B1-B6, D1-D4.
Also records what the eight rounds cost, because the pattern repeated:
every round found a witness or a rule that would have PASSED against a
broken implementation. A `#[cfg(test)]` fixture cited as a production
handler. Activation ordering mistaken for a safety proof. A mode-line
rule that was per-row when the tree's is per-kind, and document-only at
that. A producer rule the frontend had no information to obey. A
consume check that would have left focus stolen. And twice, an
assertion added without a state in which it could fail.
No round found a design disagreement. The rulings from 8 and 9 survived
intact; what changed each time was WHERE a rule lived or WHAT A WITNESS
COULD SEE.
Implementation begins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of revision 11. Still framing only; no implementation.
**"FOCUS UNCHANGED" IS VACUOUS IF THE PANEL IS ALREADY FOCUSED.** With
the terminal side window active, the below-activation mutation calls
`focus_window` on the window that is ALREADY active --- nothing
changes, and the row stays green against the exact bug it exists to
catch. I added the assertion in revision 11 and did not give it a state
in which it could fail.
Leg 2 now starts passive, and says so as assertions rather than setup:
the primary document window is active, the terminal side window is
distinct from it and passive, and the terminal controller identity is
captured as a baseline. Asserted rather than arranged, because each is
the kind of detail a later edit changes silently, and each failure mode
is a witness that passes while proving nothing.
**AND THE TWO ASSERTIONS ARE NOT INTERCHANGEABLE**, which revision 11's
prose blurred by naming them together as though either would do:
consume check below activation
-> `focus_window` runs, the panel takes focus, and NOTHING
scrolls, so every movement assertion still passes
-> caught by FOCUS, and focus alone
terminal branch calls `apply_terminal_gesture`
-> `claims_control` is `!matches!(kind, Move)` (`:3555`), so a
wheel CLAIMS THE PANEL'S CONTROLLER at `:3571`, before any
local handling at `:3575`; the chrome coordinate then fails
the reporting bounds check and drops into the local branch,
which may also move scrollback
-> caught by CONTROLLER IDENTITY, plus scrollback when it moves
Activation alone claims nothing --- `focus_window` and
`claim_terminal_controller` are separate acts --- so a row asserting
only controller identity misses the ordering bug entirely, and one
asserting only focus misses a replay that quietly claims the terminal.
Gates: all nine green under `env -u TMPDIR`, log 20260814T144513Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of revision 9. Still framing only; no implementation.
Q#BP-R2's outcome is unchanged; its ENFORCEMENT POINT moves.
**THE GPU CANNOT KNOW WHETHER A PANEL HOLDS A TERMINAL.** `PanelFrame`
carries `buffer_id`, both epochs, `size`, `cells`, `cursor` and
`focused` --- and no target-kind discriminator
(`pmacs-protocol/src/panel.rs:73`). The panel is an OPAQUE CELL
PROJECTION to the frontend. `state.terminal`
(`pmacs-gpu/src/main.rs:1964`) cannot answer either: it describes the
primary full-window terminal, not the side-window.
So revision 9's producer rule --- "terminal chrome wheel: do not send"
--- was unimplementable without a new wire field, and this lane is
explicitly non-protocol-bearing. I wrote a rule the frontend has no
information to obey.
The correction is structural, not cosmetic. **Every producer rule is
now target-blind**, and the table is split by SEAM SIDE rather than by
target:
producer claims the chrome wheel and sends `PanelPointer` with its
valid frame coordinate, for EVERY panel
daemon after validation and resolving the side window ---
document panel -> `scroll_window`;
terminal panel -> CONSUME, producing no child bytes, no
local scrollback, and no document fallthrough
The other producer rules collapse safely because both targets want the
same behaviour there: a chrome press is reserved by documents and
dropped by terminals --- both drop --- and a crossing `Up` must arrive
for either, to terminate a selection or deliver a release. The wheel
was the sole divergence, which is exactly why it was the rule that
could not survive on the producer side.
The chrome coordinate does cross the wire now, and that is fine: it is
a valid frame cell, coord validation accepts it, `scroll_window` is a
window-level move that never reads it, and the terminal branch never
forwards it to a child. Nothing is fabricated and nothing reaches an
application --- which was the whole objection to clamping.
**Witness: ONE frontend across a document->terminal replacement.**
Document chrome wheel scrolls the panel; after replacement the terminal
chrome wheel changes nothing. Doing it in one frontend across a
replacement is what makes it a control rather than two unrelated
observations --- geometry, pointer position and producer path are
identical, and only the target differs. Mutation: let the terminal
branch call `apply_terminal_gesture`, and 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 on purpose.
Gates: all nine green under `env -u TMPDIR`, log 20260814T142336Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of revision 8. Still framing only; no implementation.
Both items reverse a decision revision 8 made.
**Q#BP-R2 IS OVERRULED: a terminal-chrome wheel is CONSUMED, not
clamped.** I argued clamping on consistency grounds and missed that SGR
wheel input is COORDINATE-BEARING: `encode_mouse` takes a `coord` and
writes `coord.col + 1` / `coord.row + 1` into the emitted sequence
(`src/terminal/input.rs:102`, `:146`) --- which A4, added one revision
earlier, pins exactly. Clamping therefore fabricates a hit on the
terminal's final content row, and an application that routes wheel
input by position acts on a cell the user never pointed at. The
consistency I was buying is cosmetic; the price is a synthetic
coordinate handed to a program as real.
The asymmetry with `Up` is the part worth keeping in the record: `Up`
must be normalized because an unterminated gesture HANGS --- the daemon
holds a button down forever --- while a wheel tick is self-contained
and dropping one strands nothing. Liveness is what earns
normalization, and the wheel has no liveness obligation. Consumed, and
never fallen through to the document; TUI parity restored.
**R-c2 WOULD HAVE BROKEN A TESTED GUARANTEE.** Revision 8 said to
retain the `Down` cell in `last_pointer_cell`. That field is cleared on
press deliberately, and a live test says why: "the first drag after a
press must reach the daemon even at the cell the press landed on"
(`pmacs-gpu/src/main.rs:19841`). Storing `Down` there makes the press's
own cell the dedupe baseline and suppresses exactly that `Drag`.
Ruled: a separate `gesture_last_content_cell`. The two fields have
different jobs and conflating them was the error ---
`last_pointer_cell` answers "is this motion worth sending?", the new
one answers "where did this gesture last legitimately point?" ---
different lifetimes, different resets. Written on arm and on each
accepted content motion; reset on release and on BOTH identity changes;
never consulted by `panel_motion_is_new`. A second mutation guards the
separation itself: point the dedupe at the new field and the existing
first-`Drag`-after-press assertion must fail.
The alternative --- ruling `Down` the dedupe baseline and retiring the
guarantee --- is recorded as considered and rejected. It would need a
mutation showing the first same-cell `Drag` is redundant, and it is not
obviously so, since that event is what establishes the daemon's drag
state.
**And the crossing table follows the choice.** Revision 8 promised a
content-originated `Drag` over chrome would be sent. It should not be:
once normalized, its coordinate is usually the one already reported and
the ordinary dedupe suppresses it, correctly --- promising delivery
would oblige the producer to defeat its own dedupe for a daemon state
that is identical either way. `Up` is the load-bearing crossing event
and the only one promised unconditionally.
Gates: all nine green under `env -u TMPDIR`, log 20260814T123502Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of revision 7. Still framing only; no implementation.
**R-c NEEDED TARGET x GESTURE-ORIGIN, NOT KIND ALONE.** The per-kind
rule I took from the TUI is DOCUMENT-ONLY: for a terminal window the
TUI rejects every kind on the mode line before any per-kind match
(`src/editor.rs:3273`) and hands `dispatch_terminal_mouse` a
content-sized viewport (`:3272`). Terminals never see a chrome
coordinate.
And passing one is not merely untidy, it is wrong twice:
`apply_terminal_gesture`'s reporting branch is bounds-checked on
`coord.row < screen_size.rows` (`:3560`), and a chrome row equals
`screen_size.rows`, so the check FAILS and the gesture falls into the
LOCAL branch. A reporting child therefore receives a `Down` and then no
`Up`, and a chrome wheel silently becomes local scrollback instead of
behaving like a content wheel. The table is now split by target, and a
content-originated gesture terminates at its LAST VALID CONTENT
COORDINATE.
**Q#BP-R2 is ruled: a chrome wheel over a terminal panel CLAMPS.** This
diverges from the TUI, which drops it, and is flagged as the one
product call inside R-c. The panel's mode line is the band's own
chrome, one row, and a document panel already scrolls on a chrome
wheel; dropping only for terminals would make the same pixel behave
differently depending on which buffer happens to be shown. Overrule if
TUI parity is preferred --- nothing else in R-c depends on it.
**R-c2 --- THE PRODUCER NEVER REMEMBERS THE `Down` CELL.**
`set_panel_pointer_held` clears `last_pointer_cell`
(`pmacs-gpu/src/main.rs:7250`) and only motion refills it, while
`panel_release_cell` falls back to exactly that field. Today the
fallback is rarely reached because chrome IS a `PanelCell`; the moment
R-c stops that, a `Down` in content followed immediately by a release
over chrome --- no intervening motion --- has nothing to fall back to.
Retain the `Down` cell at arm time. For a reporting terminal the row
asserts the exact child release BYTES, because a latch that clears
while the child never hears the release is the failure being guarded.
**A1 WAS A NEGATIVE ROW STANDING ALONE.** It asserts bytes that must
not appear, so disabling child reporting entirely satisfies it --- as
does never reaching `apply_terminal_gesture`. The whole edge matrix
could have stayed green against a replay that never reports anything.
A3-A5 add the positive half on the SAME terminal: exact SGR sequences
for `Down`/`Drag`/`Up`, exact wheel bytes when reporting, and the
non-reporting wheel's local scrollback effect.
**Four witness seams tightened.** B2 was checking the wrong thing:
`active_region()` returns `None` "if no region is set OR IT IS EMPTY"
(`src/editor_core.rs:4684`), so it answers None both before and after
the `Up` and cannot see the residue --- which is an active-but-EMPTY
selection whose stale anchor would capture the next shift-motion. It
reads the owning window's raw `selection` now. B4 pins the exact
selected word rather than that click state "reads a multi-click". B6
runs its visit sentinel through single AND double click, since the
ruling forbids both. D4 uses a CHANGED frame with unchanged epochs ---
the focus repaint the `Down` itself causes --- because a byte-identical
duplicate returns early ("A duplicate does no work", `:6918`) and would
never reach the reset code it is meant to constrain.
Gates: all nine green under `env -u TMPDIR`, log 20260814T122442Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of revision 6. Still framing only; no implementation.
Three of the five are corrections to rules 6 introduced.
**R-c's "the last row is inert" WAS WRONG.** The TUI precedent I cited
is per KIND, not per row: `inner_rows` guards `Down(Left)` (:3303),
`Drag(Left)` (:3331) and `Down(Right)` (:3348), and deliberately does
NOT guard `Up(Left)` (:3339) or the wheel (:3358/:3362). A blanket
inert rule breaks two things at once --- a wheel over the mode line
stops scrolling, and a gesture begun in content and released over the
mode line NEVER TERMINATES.
And a receiver-only rule cannot work at all here, because the producer
arms first: `PanelCell` spans the whole frame, so a mode-line `Down`
runs `set_panel_pointer_held(true)` locally
(`pmacs-gpu/src/main.rs:2878`) before the daemon sees anything.
Dragging into content then emits an orphan `Drag` the daemon cannot
tell from a real one. R-c now carries a per-kind producer/receiver
table, both crossings as witnesses --- mode-line->content must emit no
Drag, content->mode-line must terminate --- and rules the wheel
explicitly.
**R-d COVERED PANEL IDENTITY AND MISSED GEOMETRY IDENTITY.** The two
epochs move independently by design: `panel_epoch` is stable across
ordinary frames while `geometry_epoch` moves on any new declaration
"including a font or scale change that leaves `CellSize` identical"
(`pmacs-protocol/src/panel.rs:61`). `next_geometry_declaration`
(`pmacs-gpu/src/main.rs:6847`) advances it and clears neither pointer
field, so a held gesture resumes UNDER A NEW GRID carrying epochs that
are current and valid --- acceptance 49 rejects stale epochs and this
one is not stale.
R-d also constrained only one of the two latch fields. Clearing
`pointer_held` alone kills the orphan drag but leaves
`last_pointer_cell`, and `panel_motion_is_new` (`:7238`) then
suppresses the successor's first same-cell `Move` as a duplicate. Four
mutations now, D1-D4, including the NEGATIVE one: an ordinary
same-identity refresh must not cancel a live gesture, or D1/D2 are
satisfiable by resetting on every frame and selection stops working.
**R-a WITNESSED ONLY THE TERMINAL.** `dispatch_pointer`'s `Down` arm
reads `extending = mods.contains(SHIFT)` and either keeps the anchor or
collapses the selection (`src/editor.rs:3673`), so Shift-click in a
document or listview panel is selection extension. Threading modifiers
into `apply_terminal_gesture` alone would have passed the proposed row
while leaving document panels broken --- a witness certifying half a
fix. Two rows now, A1 terminal and A2 document, with separate
mutations: a single "drop mods at the boundary" bites both and proves
the boundary matters, not that each CONSUMER is wired.
**R-b'S ROWS WERE SATISFIABLE BY DOING NOTHING.** "Only A's panel
changes" passes for an implementation that drops the tail entirely. B1
now pins A's anchor at the Down cell's byte and A's cursor at the Drag
cell's byte; B2 proves `Up` collapses an empty click. B4 and B5 carry
Q#BP16's existing multi-click and Context statements, which replay is
where they first become executable. B6 is a listview visit sentinel:
the row is selected and `on_visit` does NOT run, so Q#BP-R1's ruling is
falsifiable rather than merely asserted.
Gates: all nine green under `env -u TMPDIR`, log 20260814T120856Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of revision 5. Still framing only; no implementation.
**Q#BP-R1 IS RULED: a single click SELECTS a listview row only.**
RET/SPC remain the activation path (`listview.lua:610`); no
click-to-visit, no double-click-to-visit. That is acceptance 48's own
wording, and it keeps document navigation from arriving as an
incidental consequence of wiring replay.
**REVISION 5 CONCLUDED THAT ACTIVATION ORDERING MADE REPLAY SAFE. IT
IS NECESSARY BUT NOT SUFFICIENT.** The ordering holds only for gestures
that activate, in a session nothing interleaves with, on a presentation
that does not change mid-gesture, with modifiers intact and the mode
line excluded. Each of those four provisos is an edge:
**R-a --- modifiers are dropped before the shared path.** `PanelPointer`
carries `mods`; the daemon destructures them into `..`
(`src/daemon.rs:2425`) and the dispatcher has no modifier parameter.
This breaks PRECEDENCE, not just fidelity: `apply_terminal_gesture`
gates child reporting on `!shift && ... && modes.mouse_sgr`
(`src/editor.rs:3534`), so Shift is the user's "select locally instead
of talking to the child" override. Zeroed, a Shift-drag over a
reporting terminal panel sends SGR. Row: Shift-drag selects locally and
the child receives NO bytes. Mutation: drop `mods` at the boundary.
**R-b --- `Drag` and `Up` do not activate**, and another frontend's
input can interleave between a Down and its Drag, so a replay reading
ambient active-window state acts on whatever is active then. The
framing now NAMES the mechanism: an explicit side-window cell->byte
adapter, and selection through a window-TARGETED path.
`activate_and_position` (`src/editor.rs:3795`) is both the precedent
and the trap --- its conversion is properly window-scoped, but it calls
`set_active_window_id`. Rows: panel A Down -> frontend B input -> A
Drag/Up changes only A; an orphan Drag/Up on a passive panel leaves the
document mirror byte-identical.
**R-c --- `panel_grid_size` is the FRAME, not the terminal viewport.**
The panel's last row is its mode line: projection derives content as
`rows - 1` (`src/editor.rs:2499`) while `panel_hit_test` reports across
the whole frame (`pmacs-gpu/src/main.rs:7184`), so a `PanelPointer` can
name the mode-line row. Passing the full grid to
`apply_terminal_gesture` would make the mode line a child terminal
cell. Terminal viewport is `rows - 1`; document replay follows the
TUI's existing rule, "Mode-line click: reserved"
(`src/editor.rs:3304`). Rows must distinguish content from chrome or an
off-by-one passes.
**R-d --- replacement leaves the frontend's gesture latch armed.**
`Absent` clears `pointer_held` and `last_pointer_cell`
(`pmacs-gpu/src/main.rs:6909`); `Present`->`Present` does not
(`:6913`). Press on A, A replaced by B, and the latch emits a Drag or
release for B with no B press --- and acceptance 49 CANNOT reject it,
because the event carries B's current epochs. 49 is a staleness gate
and this is not a stale event; it is a well-formed event from a gesture
whose presentation no longer exists. The fix has a precedent in the
same file: the DIVIDER drag latch already carries both epochs and
self-invalidates (`:7288`). The pointer latch never got it.
Three of the four are places where the tree already contains the right
answer and the panel path does not use it, which is worth saying
plainly: this is wiring that has to be done carefully, not mechanism
that has to be invented.
Gates: all nine green under `env -u TMPDIR`, log 20260814T115257Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Ground truth before code, and the lane entry in the same commit, per
the standing correction from #171 and #215 --- the one the 1b lane
missed. Base `githubsucks/main` @ `72da24a`. No implementation yet.
**AC48 IS NOT UNIMPLEMENTED. IT IS HALF IMPLEMENTED**, and nothing had
written the halves down separately, which is how "PanelPointer drives
listview rows, selection, terminal reporting and focus" could sit next
to a dispatcher that does none of the first three.
Landed already: click-to-focus and the terminal activation rule
(`src/editor.rs:2701`); the focused-only auto-scroll clamp with a
passive panel's `view_top` preserved (`:2569`, which has cited parent
48 all along); and the coalescing rules --- `Move`/`Drag` tails
coalesce while press/release/context/wheel stay lossless and ordered
(`pmacs-gpu/src/attach.rs:374`).
Missing: listview row selection, panel selection, terminal mouse
reporting, wheel replay.
**AND THE REPLAY IS MOSTLY WIRING.** `apply_terminal_gesture`
(`src/editor.rs:3525`) is already "the one terminal pointer path,
shared by both frontend kinds" and already drives child mouse
reporting, selection and scrollback; a panel terminal needs the SAME
CALL, with `side_window_for` + `TerminalViewKey` + the
`panel_grid_size` the dispatcher already fetches. `scroll_window`
(`:3845`) is window-scoped including its cursor carry, and documents
that a wheel "names the pane under the pointer and does NOT activate
it". This is why a wheel-only bridge would be the wrong shape: the
shared path takes every kind at once, and the wheel-only version would
have to grow a second one later.
**The scoping hazard, and why no ruling is needed.** `set_cursor_byte`,
`begin_selection` and `clear_selection` are ACTIVE-WINDOW scoped
(`src/editor_core.rs:1216`, `:4691`), so replaying naively from the
panel path would move the DOCUMENT's point --- exactly what AC48's
"without disturbing the document mirror" forbids. The existing
activation rule already resolves it: activation runs before replay in
the same dispatch, and the gestures that need the active-window API are
precisely the ones that activate. The single gesture that does not --- a
document panel's wheel --- needs only the window-scoped
`scroll_window`.
**One genuine question, opened not answered: Q#BP-R1.** Does a listview
row VISIT on click, or only select? `listview.lua` binds visiting to
RET/SPC (`:610`) and there is no pointer precedent anywhere in the
file. Single-click-visits matches a file tree; click-selects,
double-click-visits matches an editor list; AC48 says "row selection"
and nothing more. This lane implements SELECTION and does not invent
activation, because the difference is whether a click navigates the
user's document.
Gates: all nine green under `env -u TMPDIR`, log 20260814T113155Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Adds the approved framing for the bottom-panel arc: a buffer displayed
in a fixed-height window pinned to the bottom of the frame, targeted by
policy rather than by stealing the selected window, plus the missing
display-buffer/window-parameter concept underneath it.
Revision 4 follows three review rounds, an integration review, and a
landed-state audit against GPU initial target (#148, protocol v20) and
folding Stage 2 (#149).
Amended before branching by the pre-implementation dependency
verification recorded in section 0.6:
- the folding dependency is cleared and re-verified against canonical
main at ddaa80d (nothing in flight, folding Stage 2 acceptance 48/48
green, every borrowed anchor reproducing, and folding's only window.rs
edit confined to one 22-line hunk that leaves the layout functions
pre-folding code);
- R5-B1: Layout::compute has TWO production callers, not one. The
second, the peer-presence overlay pass in src/overlay_paint.rs, builds
its own text-area rect from active_layout() and never routes through
window_placements, so the planned compute(area, fixed) signature change
would otherwise leave every peer cursor painted at its no-panel row.
Corrected in section 1.1 and Q#BP2, pinned by acceptance 1, and the
fixed map is now specified to come from one shared helper rather than
being assembled per call site.
Stage 1 (window placement + TUI side windows) changes no wire shape.