Commit Graph

19 Commits

Author SHA1 Message Date
Levi Neuwirth b758c2e76f
Merge githubsucks/main into panel-pointer-replay
Merged rather than rebased, by decision: the lane's 12 commits include
10 framing revisions that all touch the same 800-1000 line doc regions,
so a rebase meant twelve rounds of large-block conflict resolution ---
the operation that produced a committed diff3 marker on the last lane.
One pass instead, with all 12 commits preserved.

Resolutions:

- src/daemon.rs --- took main's structure whole, both inbound arms with
  the latch gated on the dispatcher's answer, and threaded replay's
  `mods` through both call sites. `mods` is newly BOUND in the mapped
  arm, which SS5b left in `..`; the mapped family carries the same
  modifiers, so leaving it would have given a v25 session the inverted
  Shift behaviour that parent 48 R-a fixed for v24.
- pmacs-gpu/src/main.rs --- additive throughout: both new struct fields
  (`gesture_last_content_cell`, `last_pointer_generation`), both resets
  at each site, and both test blocks.
- src/editor.rs --- auto-merged; the merged dispatcher keeps SS5b's
  `#[must_use]`, its four rejection paths and its `-> bool`, plus
  replay's `&mut self`, `mods`, chrome/mode-line handling and terminal
  gesture application.
- docs/active-work.md --- the active replay lane above main's corrected
  #239/#240/#242 headers.
- docs/bottom-panel-framing.md --- 5a then 5b. The paragraph arguing
  the v25->v26 bump should be "recorded as required rather than made"
  is marked superseded: SS5b made it and merged as #242.

Workspace compiles clean, all targets, no warnings.

THE MERGE SURFACES A SEMANTIC COLLISION THE FRAMING MUST RULE ON, and
it is not resolved here. The two branches give the dispatcher's bool
different meanings: for SS5b `true` means the gesture was ACCEPTED, and
it drives the accepted-gesture latch; for replay `true` means the event
was CONSUMED HERE, including chrome swallows. So a press on the band's
mode line now returns true and ARMS the latch --- a gesture that never
began in content, which is the defect class SS5b's review round four
found and fixed. Recorded, not patched, because which rows own the
answer is a framing question and the next revision owes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 19:51:50 +02:00
Levi Neuwirth bf4ffe0995
fix(panel): a second press ends the gesture it replaces
Found by reading the arming path back after the G5 checkpoint, not by a
failing row. `arm_accepted_gesture` overwrote an already-armed latch, so
a dropped `Up` --- one lost to an outbox that closed under a stall ---
was followed by the next press silently discarding the first gesture's
record, without counting it as a cancellation.

Inert on this base, where records are only counted. Once
`panel-pointer-replay` attaches a child release to each record, the
discarded one leaves a button held down with nothing left to release
it, and the arming code that decides this is this slice's.

Mutation: restore the plain overwrite -> the new row alone.

Also records three CI-red observations from the slice-completion gate,
which is the first entry on this lane with a MEASURED confound instead
of the standing uncontrolled one. Three wall-clock-deadline rows red in
one run --- criterion_1 by 0.12%, a PTY lifecycle race, and a 5s child
-exit deadline --- all green in isolation, the last in 0.15s against
its 5s deadline. `uptime` during the run went 14.02 -> 28.35, from an
unrelated turso test suite on the same machine with one binary at 693%
CPU. Not a controlled experiment, but the same evidence U9's synthetic
-load control was meant to produce, and it points at load.

Two process traps are recorded with them, because both were made here.
The Bash tool caps a command at ten minutes and SIGTERMs it, which the
gate reports as `FAILED (exit 143)` on whatever stage was running and
which reads exactly like a real failure. And `pkill -f <pattern>` kills
the invoking shell when the pattern appears in its own command line, so
the intended target survives while the operator believes it died --- and
here `pkill -f "cargo test"` would have destroyed an unrelated
project's build. Identify by PID.

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; the four §5b G5 rows. The full protocol gate follows on
a quieter machine; the run described above is not evidence for this tree
and is recorded as an observation only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 8c5ca10698
feat(panel): G5a's cancellation trigger, and the latch it needs
G5a is the one G5 row this slice owns: the mapping key advancing must
raise cancellation at the advance, not reactively when a later event is
refused. Reactive cancellation loses a race --- if the successor frame
reaches the frontend before the physical `Up`, the producer clears its
latch and the cancelling event never arrives.

Cancelling requires something to cancel, so the accepted-gesture latch
lands with it: `AcceptedPanelGesture` (button, coord, buffer, and
whether the press reached a child) in a per-frontend slot on
`SemanticRenderState`, armed from the daemon's accepted inbound arms
after every gate has passed.

Writing that code decides three things the framing's deferred rows later
assert about, so they are pinned here under SUBSTRATE names rather than
under G5c/G5d/G5g/G5p. Those IDs stay on `panel-pointer-replay` per
SS5b's split table --- each asserts something about a synthetic release
or a real drag continuation that does not exist on this base, and
claiming an ID in two branches is the merge hazard `active-work.md`
already records surviving a clean merge once.

Two consequences are recorded rather than fixed:

  - Cancellations are COUNTED, not queued. The record queue is what
    replay drains to deliver each release; landing it here would grow
    one entry per cancelled drag with nothing ever draining it. A
    saturating count is bounded and still separates a consume from a
    cancellation.
  - The other G5b transitions --- panel epoch, buffer replacement,
    same-size geometry, detach --- leave the latch armed on this base.
    Each strands a live gesture whose release can never be accepted.
    That is inert while nothing consumes the latch, and becomes a defect
    exactly when replay supplies effects, in the branch that owns the
    row. `Absent` is wired anyway, because `publish_absent_panel`
    clears input authority two lines later; leaving it out would be an
    inconsistency inside one function rather than a clean deferral.

Five mutations, each biting only its named row:

  - drop the advance trigger (reactive cancellation) -> G5a alone
  - arm on every accepted pointer event -> the arming substrate alone
  - an ordinary `Up` no longer consumes -> the arming substrate alone
  - a consume counts as a cancellation -> the arming substrate alone
  - one global latch via a shared slot -> the ownership substrate alone

Also repairs a fourth rustdoc split on this branch. Inserting
`AcceptedPanelGesture` at what read as a blank gap adopted
`SemanticRenderState`'s doc comment AND its
`#[allow(clippy::struct_excessive_bools)]`, silently un-suppressing a
lint on the struct that needed it. Same mechanism all four times; the
ledger now records the check as "look UP from the insertion point".

Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1956 passed); the two
`bottom_panel_stage2b_*` acceptance suites (39 passed); `git diff
--check`. The full eleven-stage `--protocol` gate is reserved for slice
completion per the standing procedure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 775a046ce4
docs(framing): fold the mapping-generation closure audit into revision 16
Close the producer, receiver, protocol-family, and gesture-lifecycle
cross-products in one pass. Separate route witnesses from replay effects,
freeze both old boundaries and new variant fields, and record the bounded
review method for the remaining chain.
2026-08-20 17:55:21 +02:00
Levi Neuwirth 137c7fc736
docs(framing): SS5b revision 16 --- the producer rule was self-defeating, and R7 recurs on a docs-only diff
Answers review of 15. Framing only. Three items reverse a rule 15
introduced, and one retracts a mutation that was not a defect.

**THE PRODUCER RULE CONTRADICTED PROACTIVE CANCELLATION.** The daemon
cancels BEFORE emitting the replacement frame, so the frontend needs
only to clear its local latch when that frame arrives, and send
nothing. Revision 15 asked it to emit a cancellation tail or retain the
latch: the tail is redundant --- the daemon would receive a release for
a gesture it has already settled, which is the duplicate release the
latch exists to prevent --- and RETAINING IS ACTIVELY HARMFUL, because
it manufactures a `Drag` under the NEW generation with no accepted
`Down`. That is the exact orphan the section exists to prevent,
produced by the rule meant to prevent it.

Ordering is what makes the simple rule safe: cancel, then emit. The
frame's arrival IS the cancellation signal; no second channel is
needed. Witnessed as `Down` -> key advances -> replacement frame ->
motion and physical `Up` produce no new drag and no duplicate release.

**THE LATCH HAD ONE TRIGGER AND NEEDED FIVE.** Cancellation runs on
every loss of gesture authority: generation advance, `Absent`, panel or
buffer identity change, geometry-epoch change EVEN AT AN UNCHANGED CELL
TOTAL, and detach. And an ordinary accepted `Up` must clear the latch,
or a later invalidation finds a gesture it believes live and
synthesises a duplicate release for a button already up --- the replay
lane's D1/D2 orphan race, arriving from the daemon's side.

**G9b's MUTATION WAS A VALID IMPLEMENTATION, NOT A DEFECT.** Keying the
dedupe by `(mapping_generation, coord)` preserves same-generation
suppression and naturally admits the first motion under a new
generation. Requiring it to fail would have forbidden a correct design.
Replaced with two real defects: compare only the cell and never key or
reset by generation (the first post-change motion is eaten), and reset
on every same-generation repaint (pixel-rate traffic returns).

**"PROJECTED CELL IDENTITY" CONTRADICTED THE STYLING CONTROL** in the
same section. The wire `Cell` derives `PartialEq` over `glyph`, STYLE
and `attachment` (`pmacs-protocol/src/cell.rs:153`), so an identity
keyed on cell equality moves on a pure recolour --- while the stable
controls rule style out. Terminal identity is now glyph and row
TOPOLOGY plus the view anchor, excluding face, style and cursor, with a
same-glyph/different-style control: the row that catches an
implementation reaching for `Cell` equality because it is right there.

P2s: zero-generation rows added in BOTH directions as independent legs
(a valid `PresentMapped` with generation zero must be rejected
atomically; a zero-generation `PanelPointerMapped` must be refused);
G7 split into outbound mapped-frame and inbound mapped-pointer legs,
since its old mutation only withheld the frame; G2's grid rows/columns
and fold-map-content/`fold_projection`-policy composites split; and
SS20 now names journey steps 5 and 8 while stating neither grade
changes --- an auditor scanning for grade movement alone would
otherwise conclude this slice touches no journey.

**AND R7 RECURRED, ON A DIFF THAT IS ENTIRELY DOCUMENTATION.** The
first `--protocol` run of this tree failed the `gpu` step on
`managed_retry_survives_transients_and_uses_the_successful_stream`,
with all three required fragments verified from the durable log
(`20260815T072601Z`). Recorded as R7's FIFTH occurrence.

It carries the strongest tree exclusion the row has had: occurrences 1
and 4 argued "unrelated lane", while this branch cannot be related at
all --- no Rust, no wire surface, no `pmacs-gpu` file. The line moved
to `attach.rs:1728` from `:1680`, which the row already treats as
occurrence-specific rather than a fragment. Isolated rerun green, and
the full gate green on the re-run (271/271 in the `gpu` step) --- which
per this file's rerun rule establishes INTERMITTENCE ONLY, though here
there is no tree change to exonerate.

What five occurrences across three flavors and five unrelated lanes now
support is that the failure is NOT LANE-CORRELATED. That is evidence
about where the cause is not. The retirement condition is unchanged.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T073556Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth fa980ef1ba
docs(framing): SS5b revision 15 --- appended means LAST, and cancellation had a race
Answers review of 14. Framing only. Four of the six reverse something
14 asserted, and a green protocol gate would not have caught any of
them.

**"BESIDE `Present`/`PanelPointer`" WAS POSITIONALLY DANGEROUS.**
"Beside" reads as adjacent, and adjacent insertion shifts every
discriminant below it --- the exact hazard the appended-only rule
exists for. Appended means LAST: `PresentMapped` after `Absent`,
`PanelPointerMapped` after `TextInput`, with a diagram so the next
reader cannot re-derive it wrongly. Field order is stated exactly,
`mapping_generation` is a `u64`, ZERO IS INVALID --- it is what a
default-constructed or half-initialised sender produces, so accepting
it would let a peer opt out of the check by sending nothing --- and the
gate reads `PANEL_MAPPING_MIN_VERSION = 25` rather than a literal.

**BLANKET REFUSAL STOPPED THE WHEEL AFTER ONE TICK.** The first
effective document wheel changes `view_top`, which advances the key, so
the next already-queued tick carries the old generation and is refused:
the panel scrolls once and goes dead until the frontend observes the
new frame. Local terminal scrollback has the same shape.

The discriminator is whether the gesture USES its coordinate.
Coordinate-free gestures --- the document wheel, non-reporting terminal
scrollback --- cannot be mis-aimed by a stale mapping and are EXEMPT.
A child-reported wheel is the opposite case: SGR carries row and
column, so a stale one aims an application action at a cell the user
never pointed at, and it keeps the check. Two-tick witnesses added,
because without them a blanket-refusal implementation passes every
single-event row in the matrix.

**CANCELLATION WAS REACTIVE AND LOSES A RACE.** If the replacement
mapped frame reaches the frontend before the physical `Up`, the
producer resets `pointer_held` and SUPPRESSES THE VERY EVENT that would
have cancelled --- so the daemon is never told, the selection stays
armed, and the child keeps holding its button. It is now PROACTIVE,
triggered by the authoritative key advancing while a gesture is
accepted, and the producer must emit a cancellation tail or retain the
latch rather than clearing first.

That needs state 14 assumed and never specified: an ACCEPTED-GESTURE
LATCH recording whether the `Down` was accepted, whether it reached the
child, and the coordinate, button and encoding a release must match.
Two rules fall out and are ruled here --- a stale `Up` with no accepted
`Down` is INERT, and cancellation NEVER reclaims a controller another
frontend has since taken, because a stale gesture must not steal a live
one's terminal.

**THE EXISTING SCREEN GENERATION CANNOT BE THE TERMINAL KEY.**
`Screen::changed()` bumps from 39 call sites including `SetStyle`,
`Bell`, the tab-stop operations, cursor-only motion and `SetTitle`.
None of those change what a coordinate denotes, so keying on it would
cancel a drag every time the child recoloured a character. A dedicated
terminal mapping revision is defined over projected cell identity,
retained-row identity and the per-view scroll anchor --- with those
five events as explicit STABLE CONTROLS, so a reader who later reaches
for the convenient counter fails a test instead of shipping a cancelled
drag.

**G5'S EFFECTS ARE NOT PROVABLE ON THIS BRANCH**, and 14 claimed them.
`gesture_last_content_cell` and the document/terminal replay exist only
on `panel-pointer-replay` (`pmacs-gpu/src/main.rs:2143` there); the
same struct here is at `:2124` with no such field. The obligations are
split in a table. G5a --- that the key advancing RAISES cancellation
--- stays here on purpose: the trigger is this slice's rule, and moving
the whole row out would leave the proactive ruling with no witness in
the slice that introduces it.

P2s: mutation legs split (wrap vs gutter, terminal content vs
scrollback, G5a-c, G8a/b, G9a/b); G7 given a positive-path mutation;
G11 expanded --- exhaustion must publish `Absent`, clear input
authority, cancel any accepted gesture and LATCH, or a stale panel
stays painted and permanently inert; the v26 correction finished at the
gate and old-peer cells (`:573`); and the SS20 impact statement added
--- hardens an existing panel island, no journey grade changes, no
config, no background work.

**And the pin correction is mine to make: it EXISTS**, at
`src/protocol.rs:1975`, in the ROOT crate's test module rather than
under `pmacs-protocol/` or `tests/` --- which is exactly where I
searched. `message.rs:524` was right and the doubt was wrong; the
contrary claim is removed from both records.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260814T180105Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth ca816a1917
docs(framing): the panel cell-mapping generation (v25) --- SS5b revision 14
Own branch, own slice, protocol-bearing, runs alone. Framing only; no
implementation. Blocks `panel-pointer-replay`, which blocks GUI arc 1b.

Answers review of revision 13. Every item below reverses or completes
something 13 got wrong.

**GATING IS REFUSAL, NOT FALLBACK.** Revision 13 said a bare
`PanelPointer` from a new peer would be "handled under the old
semantics". That is a BYPASS: it leaves the exact hole this slice
exists to close, reachable by omitting a field. A >= v25 session
sending the legacy event is REFUSED before mutation, and a >= v25
frontend REJECTS a legacy `Present` rather than painting a band it
cannot safely hit-test. Only negotiated <= v24 keeps legacy semantics;
`Absent` stays common to both families.

**ONE AUTHORITATIVE PER-FRONTEND KEY**, used by projection AND inbound
validation, advanced after any mapping mutation and BEFORE the next
inbound pointer is handled --- whether or not anything has rendered.
Comparing against the last EMITTED frame recreates the hole, because a
mutation not yet painted has still changed the inverse mapping.

**STALE TAILS TERMINATE; THEY DO NOT VANISH.** A blanket drop breaks
liveness: a refused `Up` leaves an empty document selection armed with
a stale anchor, and leaves a reporting terminal child HOLDING A BUTTON
FOREVER. Cancellation is now a ruled outcome --- producer latch reset,
daemon selection and click-chain cleanup, and the child's release
delivered at the last coordinate known good. A cancelled gesture is
explicitly not a replayed one: the release is for liveness, and no
selection or scroll effect is applied from the stale event. Stale
BEGINNINGS may still simply drop.

**THE DOMAIN WAS INCOMPLETE.** `view_left` is added, because 1b makes
horizontal scrolling real. "Cursor movement is stable" is now
CONDITIONAL: a cursor move that triggers vertical or horizontal follow
changes `view_top` or `view_left` and therefore does change the
mapping. Terminal panels are ruled explicitly --- their coordinates are
decided by the SCREEN, so output and scrollback movement change the
generation while their buffer revision does not.

**SS5b HAD NO ACCEPTANCE MATRIX.** G1-G11 now cover the foreign edit
before render, every changing and stable domain entry ROW BY ROW, a
selection repaint that must preserve the generation and let a drag
continue, mid-gesture cancellation, v24 and v25 positive controls with
both wrong-family refusals, identical cells across a generation change
still emitting, atomic retention of frame and generation on an invalid
frame, and fail-closed exhaustion. The per-entry enumeration is
deliberate: one aggregate row cannot show WHICH input moved the key,
and a key ignoring `view_left` passes every vertical-only row.

**MAPPED MOTION KEEPS ITS COALESCING TAGS.** A new variant falling
through to the lossless default would put pixel-rate `Move`/`Drag` on a
bounded queue.

**PINS ACCUMULATE.** Revision 13 said the pin "moves", which would
delete coverage of the shape it protects. `PanelPointer` is retained;
exact `TextInput` bytes are added as the previous-final
`FrontendEvent`; the complete nested `PanelFrame(Absent)` bytes are
added as the previous-final `PanelFramePayload`. Recorded honestly: I
could find NO exact-bytes pin for `PanelPointer` anywhere in the tree,
though `pmacs-protocol/src/message.rs:524` says one is in the tests.
Either my search missed it or the doc overclaims; this slice resolves
it either way, since it must add exact pins regardless.

**AND THIS SLICE OWNS THE VERSION CORRECTION.**
`docs/gui-stage1-input-framing.md` now says 1e's `OpenTarget` is
**v26**, with the reason stated at the top. An expected rebase conflict
on `gui-stage1b-pointer-scroll` is not grounds for leaving the
canonical document false --- which is what I argued last round, and it
was wrong. `ADVERTISED_PROTOCOL_VERSION` stays pinned at 20.

Gates: all ELEVEN green under `env -u TMPDIR`, with `--protocol`
(`build-crdt`, `sweep-crdt`), log 20260814T162843Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 7e107bd137
docs(framing): Q#BP-R3 overruled --- the lane blocks on a cell-mapping generation
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
2026-08-14 18:02:24 +02:00
Levi Neuwirth 3dff79a0ec
fix(panel): D2 was broken on the real sequence, and Q#BP-R3 is ruled
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
2026-08-14 17:50:54 +02:00
Levi Neuwirth 515a4515be
docs(framing): mark bottom-panel revision 12 approved
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
2026-08-14 16:55:24 +02:00
Levi Neuwirth 78e5bec2d4
docs(framing): bottom-panel revision 12 --- the passive precondition, and two assertions that are not interchangeable
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
2026-08-14 16:49:54 +02:00
Levi Neuwirth 112fcf593f
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 16:41:02 +02:00
Levi Neuwirth ec127fc8fa
docs(framing): bottom-panel revision 10 --- the ruling was on the wrong side of the seam
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
2026-08-14 16:28:18 +02:00
Levi Neuwirth 7d6b6da366
docs(framing): bottom-panel revision 9 --- Q#BP-R2 overruled, and R-c2 stops fighting a tested contract
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
2026-08-14 14:39:47 +02:00
Levi Neuwirth 4833a73dfe
docs(framing): bottom-panel revision 8 --- target x origin, and a negative row standing alone
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
2026-08-14 14:29:23 +02:00
Levi Neuwirth d9a23f30eb
docs(framing): bottom-panel revision 7 --- per-kind mode line, geometry identity, and three half-fixes
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
2026-08-14 14:13:37 +02:00
Levi Neuwirth c861d24e91
docs(framing): bottom-panel revision 6 --- four replay edges, and Q#BP-R1 ruled
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
2026-08-14 13:57:37 +02:00
Levi Neuwirth d97dcdfc46
docs(framing): acceptance 48 re-measured --- it is HALF implemented
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
2026-08-14 13:40:24 +02:00
Levi Neuwirth c27f75a1b2 docs: bottom panel framing (revision 4, amended)
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.
2026-07-24 13:00:46 -04:00