# Bottom panel Stage 2 — the GPU panel band (framing) **Revision 1 — pre-implementation. Ground truth: canonical `main` @ `5aa9044`, protocol v20, 2026-07-25.** Stage 1 (#155, merge `e745068`) gave pmacs window placement, window parameters, TUI side windows, the divider, and the adopter `display` opt-in. It deliberately set `FrontendView::panel_capable = false` for every semantic session, so a GPU frontend silently falls back to the non-side target. **Stage 2 flips that bit and earns the right to.** This document is the re-framing `docs/bottom-panel-framing.md` (rev 4) §2 requires before Stage 2 is implemented. It does **not** restate the parent's decisions. It records what the re-scout against current `main` found: which anchors moved, which parent claims survived, which did not, and the four questions the parent framing cannot answer without a decision from the user. Read the parent framing's Q#BP8, Q#BP9, Q#BP14b, Q#BP15, Q#BP15a, Q#BP16, and Q#BP17 alongside this. Those decisions stand except where §4 below revises them. ## 0. Why the re-scout was required The parent framing's Stage 2 sections were written against `main` @ `0dd16a5` and last re-scouted at `47581f4`. Since then eleven PRs have merged: #149/#150 (folding Stage 2), #152–#155 (through bottom-panel Stage 1), #158–#166 (inline math, minimap, Lean 4 Stages 1–2, COHERENCE.md, find-file, the dired framing and Stage 1, the GPU terminal input fix). Every source anchor Stage 2 depends on has moved. Two things did **not** change, and both are load-bearing: - **Protocol is still v20.** `PROTOCOL_VERSION` is `20` (`pmacs-protocol/src/message.rs:1568`); no intervening PR bumped it. Q#BP9's conditional resolves: **Stage 2 is v21**. - **Both byte pins are still the final variants.** `InstanceMessage::InitialTargetResult` is last in its enum (`message.rs:577` within the enum at `:569`), and `FrontendEvent::TerminalPointer` is last in its own. Q#BP9's append-plus-pin instruction applies verbatim, with no re-derivation. ## 1. Anchor re-scout Every line reference Stage 2 inherits, re-verified. "Claim" is the parent framing's assertion about that site; "verdict" is what the code at `5aa9044` actually says. | Parent anchor | Now at | Claim | Verdict | | --- | --- | --- | --- | | `src/editor.rs:2833` `paint_frame` returns cursor separately | `src/editor.rs:3171` | cells alone lose the caret | **Holds.** Signature still returns `Option` | | `src/editor.rs:2883-2935` cursor-visible prep | `src/editor.rs:3249+` | extract with the per-window body | **Holds**, but see §3.1 — Stage 1 inserted work *above* it | | `src/editor.rs:2937-3040` per-window paint body | after `:3260` | origin-agnostic `Viewport<'a>`, extractable | **Holds** | | `src/editor_core.rs:566` `fold_map_for_window` | `src/editor_core.rs:734` | gates on the **active** frontend | **Holds** — `:738` is `if !self.fold_projection_active()` | | `src/window.rs:339` stale invariant comment | `src/window.rs:562` | "a semantic session never enters `paint_frame`" | **Holds, still stale.** Updating it remains a Stage 2 obligation | | `src/statusline.rs:634` indirect `view.active` read | `src/statusline.rs:629`, `:644`, `:675` | one read to close | **Revised: three sites**, not one | | `src/daemon.rs:3122-3130` grid-only `Mouse` | `src/daemon.rs:3123` | `Mouse` is contractually the grid path | **Holds** | | `pmacs-gpu/src/attach.rs:420-429`, `:573-577` | `pmacs-gpu/src/attach.rs:577` | permanent `24×80` placeholder | **Holds**, single site now | Nothing in the parent's mechanical model was falsified by the re-scout. The decisions in §4 come from what Stage 1 *added*, not from anything Stage 2 got wrong. ## 2. What Stage 1 already built for Stage 2 More than the parent framing anticipated, which shrinks Stage 2 and changes one of its wire contracts. - **`DeclaredFrameGeometry { geometry_epoch: u64, total: CellSize }` exists** (`src/window.rs:522-528`), stored as `FrontendView::frame_geometry: Option<_>` (`:589`) with `None` meaning **unknown** — exactly Q#BP15a's "unknown is first-class". - **The declaration path exists.** `EditorState::sync_frame_geometry` (`src/editor.rs:877-882`) calls `EditorCore::declare_frame_geometry` then `reconcile_panel_layout`. Two daemon sites already drive it, both gated on `panel_capable_for` (`src/daemon.rs:1882-1883` at attach, `:1972-1973` on resize). - **`paint_frame` itself declares geometry** (`src/editor.rs:3187`), before the statusline fan-out and before the long mutable core borrow, with a comment naming Q#BP2b/Q#BP15a. - **`StatuslineEvaluationTarget`** (`src/statusline.rs:212-226`) is already a two-variant enum — `Grid { frontend_id }` and `Semantic { frontend_id, declared_buffer }`. Q#BP8's "generalize the fan-out" is an added variant, not a refactor of a concrete type. - **`primary_document_window`** exists (`src/editor_core.rs:2830`). ## 3. What the re-scout found ### 3.1 The geometry epoch is allocated daemon-side; the wire contract says frontend-side This is the one genuine conflict between landed Stage 1 and framed Stage 2, and it needs a decision before implementation. `declare_frame_geometry` (`src/editor_core.rs:3155-3172`) **allocates the epoch itself**: ```rust let next = view .frame_geometry .map_or(1, |geometry| geometry.geometry_epoch.saturating_add(1)); ``` Q#BP15a specifies the opposite: `FrontendEvent::FrontendCellGeometry { frontend_id, geometry_epoch, total }` carries a **frontend-owned** declaration id, and `PanelResizeRows` / `PanelPointer` / every `Present` echo it. Under the landed code the daemon would have to either ignore the wire epoch (breaking the echo contract the GPU validates against) or overwrite its own allocator for panel-capable semantic frontends only (two allocation regimes for one field). Two further details of the landed allocator matter: - **It dedups on value.** The function returns early when `total` is unchanged, so the epoch advances only on an actual size change. For a grid frontend that is correct — cells are the unit, and an unchanged grid means an old `PanelFrame` is still valid under the new metrics. For a frontend that *owns* its epoch, the daemon cannot dedup by value without discarding a declaration the frontend already considers current. - **`saturating_add` is neither wrapping nor fail-closed.** Q#BP15a requires exhaustion to "fail closed rather than wrap". Saturation pins the epoch at `u64::MAX`, after which two different geometries share one id — the exact staleness confusion the epoch exists to prevent. Unreachable in practice; wrong as a contract, and free to fix. **Q#BP2S1 (new, needs a decision).** Three candidate resolutions: 1. **Frontend-owned, as framed.** `FrontendCellGeometry` carries the epoch; the daemon stores it verbatim for semantic panel-capable frontends and rejects a lower-or-equal epoch carrying different data. Grid/LOCAL keep the local allocator, which never collides because those frontends never send the event. Cost: one field, two provenances, documented. 2. **Daemon-owned, GPU echoes.** `FrontendCellGeometry` carries only `total`; the daemon allocates and the GPU learns its current epoch from the next `PanelFrame`. Simpler invariant, but it reintroduces a first-open ordering problem — the GPU must send `PanelResizeRows` and `PanelPointer` carrying an epoch it has not been told yet, so the first gesture after a resize is unvalidatable and must be dropped. 3. **Frontend-owned everywhere.** Grid/LOCAL synthesize an epoch at their existing declaration sites and the allocator moves out of `EditorCore` entirely. Most uniform; largest Stage 1 churn, and it touches code #155 just stabilized. **Recommendation: option 1.** It preserves the parent framing's validation chain intact, and the "two provenances" cost is one doc comment on a field that already carries three. ### 3.2 The §1.3 census is essentially unrouted The parent framing's §1.3 lists 23 transitive active-context reads that must route through `primary_document_window` before a panel can hold focus without corrupting the document mirror. Stage 1 created the seam but routed almost nothing through it: `primary_document_window` has **three** references in `src/`, one of which is its own definition and one a doc-comment link. The single production caller is `src/daemon.rs:1639`. For scale, `src/*.rs` still contains ~80 non-test direct `.active` reads (excluding `active_frontend`, setters, and predicates), on top of the `active_window*` / `active_buffer*` helper family at `src/editor_core.rs:663-967`. This is not a defect in Stage 1 — with `panel_capable = false` for every semantic session, no semantic frontend can hold a side window, so the unrouted reads are unreachable from the GPU. It does mean **the census is the bulk of Stage 2's work**, not a tidy-up at the end, and the stage plan in §5 sequences it first. ### 3.3 The statusline read is three sites, not one Q#BP8 says closing the indirect `view.active` read at `src/statusline.rs:634` falls out of the target generalization. There are three: `:629` and `:675` compute `active: window_id == view.active`, and `:644` does `.get(&view.active)`. They are the same concern, but a fix that closes one and leaves two is a live risk, and the acceptance criterion should name all three. ### 3.4 Scout obligations still open Stated plainly rather than papered over. These were not re-verified in this pass and must be before the doc leaves revision 1: - `pmacs-protocol/src/terminal.rs`'s validator internals, which Q#BP15 asks to factor into a shared parameterized wire-cell-grid validator (the `MAX_TERMINAL_ROWS/COLS = 512` split). - `pmacs-gpu/src/attach.rs`'s bounded outbox policy and its existing tail-coalescing classes, which Q#BP15a asks to extend with two new classes and Q#BP16 with two more. - The GPU-side band renderer and where it clips against the status band — Q#BP15a's pixel formula is stated but its inputs (`status_band_height_px`, `TEXT_TOP_px`, `code_line_height_px`, `resolved_monospace_advance_px`) were not located in this pass. - Whether folding Stage 3 lands first. Both stages touch the semantic projection, and the ledger's standing rule is that whichever is framed second re-scouts the other's landed state. ## 4. Revisions to the parent framing Only these. Everything else in Q#BP8/9/14b/15/15a/16/17 stands. - **Q#BP9 resolves to v21.** No reservation was taken; none was needed. - **Q#BP15a's epoch ownership is reopened as Q#BP2S1** (§3.1). - **Q#BP8's statusline criterion names three sites** (§3.3). - **Q#BP17's stale comment is at `src/window.rs:562`**, and its text is now embedded in a longer `fold_projection` doc block that also explains the Stage 2/Stage 3 split — the edit is a paragraph rewrite, not a one-line correction. ## 5. What ships, in order Sequenced so each step is independently gateable and the census — the riskiest part — lands before anything depends on it. 1. **Route the census.** Every §1.3 read through `primary_document_window`, with `panel_capable` still `false`. No wire change, no behavior change for any existing frontend; pure seam adoption, falsifiable by revert. 2. **Extract the per-window painter.** Lift `paint_frame`'s per-window body plus the active-window cursor-visible preparation into a function taking the fold map as a **parameter** (Q#BP17), leaving `sync_frame_geometry` and the statusline fan-out where Stage 1 put them. Grid rendering must be byte-identical. 3. **Protocol v21.** Append `InstanceMessage::PanelFrame` and `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`, each with a byte pin on the current final variant. Factor the shared cell-grid validator. Gated both directions. 4. **Daemon-side panel projection.** `PanelFrame` production, presentation epochs, `Absent` authority, the third statusline target, the focus-chrome pass (Q#BP14b). 5. **GPU band.** Geometry declaration, band paint, divider chrome, document clip, pointer transport, and the `panel_capable = true` flip for semantic sessions — the flip is last, and it is what makes the whole stage observable. Steps 1 and 2 are reviewable without any protocol change and could ship as a separate PR if the user prefers a smaller first review. **That is one of the questions in §8.** ## 6. Coherence impact (per `COHERENCE.md` §20) - **Journey steps touched:** none directly. Stage 2 does not add a journey step; it removes a frontend-dependent *hole* in one. Today a user who runs `pmacs --gpu` and triggers compile, grep, references, or a terminal gets the non-side fallback — the panel silently becomes a stolen window. Every journey step that ends in an output surface behaves differently on GPU than on TUI, and Stage 2 is what closes that. - **Interaction islands added: none, and this is a reduction.** §6 grades islands "weak, and growing by one island per modal feature". The panel is the opposite move: `display = "panel"` is one adopted policy across listview, compile, and terminal, and Stage 2 extends the existing policy to a second frontend rather than adding a parallel GPU-only surface. The focus-chrome routing table (Q#BP14b) deliberately reuses the existing `SearchPrompt` / `MenuPrompt` / `CompletionPopup` messages instead of minting panel-specific ones. - **Config registry adoption:** inherited, not extended. Stage 1's `window.panel-height` and `window.min-height` already live in the registry; Stage 2 adds no new user-facing option. If the GPU needs a band-specific preference, it enters the registry — no new configuration mechanism. - **Background-work attribution:** unchanged. Stage 2 introduces no worker, task, or process. It does, however, make §9's "✓ mechanics / ✗ visibility" gap materially cheaper to close on the GPU: a terminal PTY appearing in no user-visible activity view (§9) is partly a *placement* problem, and after Stage 2 both frontends have a place to put one. - **Section this serves:** `COHERENCE.md` §14, which already records the panel primitive as landed for Stage 1 and names "Stage 2 (GPU band) pending its own framing" as the open item. This is that doc. ## 7. Acceptance criteria (draft) Numbered for review; each must be falsifiable by revert, per the standing lesson that a guard with no production caller passes every direct-call test. 1. Every §1.3 census read resolves through `primary_document_window`; asserted at the outermost user-reachable seam, not by direct call. 2. All three `src/statusline.rs` active reads route through the new frontend-layout target. 3. Grid `paint_frame` output is byte-identical across the extraction. 4. A semantic frontend with `fold_projection = false` painting a panel over a folded buffer shows **every** source line (Q#BP17), and the panel path never calls `fold_map_for_window`. 5. v21 round-trips; v20 peers negotiate without the panel events; each extended enum's previous final variant is byte-pinned. 6. `Absent` is emitted on both close and hide, and clears input authority before any later event validates. 7. A `PanelPointer` failing any of Q#BP16's six checks mutates no view, controller, selection, menu, or PTY. 8. A geometry epoch change makes an older `PanelFrame` non-painted and non-hit-testable until a matching `Present` arrives. 9. A panel wider than 512 columns is legal; a panel exceeding the shared wire budget fails closed to `Absent`, not to a partial frame. 10. Panel focus does not disturb the document mirror (`BufferSnapshot`, `CursorByte`, `Viewport`). 11. Native popups clear on document→panel focus change and panel popups clear on panel→document, per Q#BP14b's ordering. ## 8. Questions for the user 1. **Q#BP2S1 epoch ownership** (§3.1) — recommendation is option 1, frontend-owned with the daemon storing verbatim. Confirm or pick another. 2. **One PR or two?** Steps 1–2 (census + extraction) are protocol-free and independently valuable; steps 3–5 are the wire and the band. Splitting gives two small reviews instead of one very large one, at the cost of a second round-trip. 3. **Ordering against folding Stage 3.** Both touch the semantic projection. Framing bottom-panel Stage 2 first means folding Stage 3 re-scouts against a landed band. Confirm that order. 4. **Scout obligations in §3.4** — should revision 2 close all four before implementation, or is the GPU-side pixel formula (the largest of them) allowed to be settled during implementation? ## 9. Gates The standing suite, plus `bottom_panel_stage1_acceptance` and a new `bottom_panel_stage2_acceptance`, the three vterm suites (the panel hosts terminals), folding Stage 2's 48 (shared projection), and `PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`. Protocol round-trip and byte-pin tests ride step 3.