Commit Graph

3 Commits

Author SHA1 Message Date
Levi Neuwirth 431d844322 feat(bottom-panel): split the GPU document bottom into three boundaries
Bottom-panel Stage 2B-3, part 2 of 3: the pixel substrate for the band.

`text_area_bottom` was three boundaries wearing one name — its own doc
comment called it "the single source for every bottom-of-text
computation" — and once a band can be installed they must diverge:

  status_band_top          = max(0, height - status_band_height)
  geometry_capacity_bottom = max(0, status_band_top - divider_height)
  document_text_bottom     = max(0, status_band_top - installed_band)

The census is 29 matches: 20 production call sites, 1 definition, 8 test
sites. All 20 were read in their enclosing function and classified
individually — 8 status-owned, 12 document-owned. A blanket rewrite that
subtracted the band from all of them would move the status chrome with
the document and pass an "everything moved" assertion, which is why the
classification is per site and the criterion asserts both directions.

The three easiest to get wrong keep their named symptoms: document
completion placement is document-owned (status-owned would overlap the
band), minibuffer candidate clipping is status-owned (the minibuffer is
global bufferless chrome anchored to the band, and clipping it at the
document boundary would cut it off), and edge scrolling is document-owned
(left on the old bottom it would auto-scroll from inside the panel).

`geometry_capacity_bottom` reserves the divider even while the panel is
absent. That asymmetry is what breaks the first-open cycle: the daemon
sizes a panel from the capacity it was told about, so a capacity that
ignored the divider would grant a first panel that does not fit once the
divider appears beside it. The document loses no pixels until a `Present`
frame is really on screen.

`PanelBandInset` is a newtype, not an `f32`, because three boundaries here
take a pixel height and only one takes this one.

Alongside it, the band's own machinery: `PanelBand` with ONE derivation of
"is a panel on screen" (`presented()` — retained valid frame, matching
geometry epoch, latch clear), the frontend-owned epoch state machine with
its fail-closed exhaustion latch, the `Absent`-is-authoritative receipt
path, `panel_cell_capacity` (no per-axis cap — a panel may legitimately be
wider than a PTY — plus the daemon's virtual status row), the stable
normal-face probe for column count, and the divider strip whose paint rect
IS its hit rect.

`TerminalPaintPlan::build_grid` factors the shared cell planner so a panel
and a terminal cannot disagree about a wide-continuation pair; terminal
selection spans stay outside it rather than being faked as empty inside.

`PANEL_MIN_VERSION` moves into `pmacs-protocol` so the GPU frontend aliases
one definition instead of restating 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 18:30:57 -04:00
Levi Neuwirth 9b364adc26 fix(panel): review round 1 — buffer_id, the transport ratchet, shared bounds
P1 — `PanelPointer` was missing the approved `buffer_id`. Q#BP16 gives it
and `panel_epoch` different jobs and neither subsumes the other:
`buffer_id` catches an A->B buffer replacement, `panel_epoch` catches
close/hide/reopen of the SAME persistent buffer, which a buffer id alone
cannot see. Added in the framing's field order, with a pin asserting each
field independently reaches the wire.

P1 — added parent criterion 39's transport-safety ratchet. It builds the
maximum legal panel payload, asserts the fixture actually spends the whole
aggregate glyph budget (otherwise the ratchet measures something smaller
than the worst case), asserts one byte more is rejected, and pins the
encoded `InstanceMessage::PanelFrame` below `MAX_FRAME_BYTES`. Shaped
`1 x MAX_PANEL_VISIBLE_CELLS` deliberately: no per-axis cap makes that a
legal panel geometry a terminal cannot express, so it is the worst case
the terminal's own ratchet never measured. Bitten by tripling the glyph
budget — 30,342,696 bytes against the 16 MiB cap.

P2 — the shared bounds were duplicated literals. `MAX_TERMINAL_GRAPHEME_BYTES`
now aliases `MAX_WIRE_GRID_GRAPHEME_BYTES`, so the terminal screen's
truncation (`src/terminal/screen.rs:697`, `:777`) and the validator cannot
drift. Two more had the same defect and are aliased too:
`MAX_TERMINAL_VISIBLE_CELLS` and `MAX_TERMINAL_FRAME_GLYPH_BYTES`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 17:31:04 -04:00
Levi Neuwirth 640c5cd0d2 feat(panel): bottom-panel Stage 2B — the v21 protocol layer
Adds the four wire shapes Q#BP9 names, bumps the protocol to v21, and
factors the cell-grid validator so a panel frame shares the terminal's
rules without inheriting its PTY caps.

- `InstanceMessage::PanelFrame(PanelFramePayload)`, appended after
  `InitialTargetResult`; `Absent` is an explicit authoritative state, not
  silence, because the receiver retains its last valid frame.
- `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`,
  appended after `TerminalPointer`. Geometry is valid without a side
  window — gating it on panel presence would deadlock the first open,
  since the daemon needs columns before it can paint a first frame.
- `pmacs-protocol/src/wire_grid.rs` holds the shared rules: checked area,
  visible-cell bound, cell count, cursor bounds, glyph legality,
  wide-continuation topology, the aggregate glyph budget, and the
  attachment rejection. The 512 per-axis caps, metadata, selection spans,
  and the at_bottom/scroll_offset coupling stay terminal-only.
- The attachment rejection is deliberately shared despite its
  terminal-side wording: panels render no attachments either, so sharing
  it fails closed for both.

Both byte pins were falsified by revert: moving `PanelFrame` ahead of
`InitialTargetResult` shifts it 27 -> 28 and fails; moving the three
events ahead of `TerminalPointer` shifts it 12 -> 15 and fails.

The factoring changed no terminal acceptance — all 17 terminal tests pass
unchanged. It did surface a pre-existing coverage gap: those tests pin
the row cap but never the column cap, so widening `max_cols` to u32::MAX
left them green. `a_panel_wider_than_512_columns_is_legal_while_a_terminal_is_not`
now covers that direction.

The daemon projection, the epoch state machine, and the GPU band are
later slices of this stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 16:52:54 -04:00