docs: bottom-panel Stage 2 framing (revision 1)
The re-framing `docs/bottom-panel-framing.md` rev 4 §2 requires before Stage 2 (the GPU panel band) is implemented. Re-scouted against canonical `main` @ `5aa9044`, protocol v20. It does not restate the parent's decisions; it records what the re-scout found. Every source anchor Stage 2 inherits had moved, but none of the parent's mechanical model was falsified. Two facts held and are load-bearing: protocol is still v20, so Q#BP9 resolves to **v21** with no reservation needed, and both byte pins (`InstanceMessage::InitialTargetResult`, `FrontendEvent::TerminalPointer`) are still their enums' final variants. Four findings: - **Q#BP2S1, new and open.** Stage 1 landed a daemon-side geometry epoch allocator (`declare_frame_geometry`), but Q#BP15a specifies a frontend-owned epoch echoed by every `Present`. The landed allocator also dedups on value and uses `saturating_add`, which is neither wrapping nor the fail-closed the framing asks for. Three resolutions are stated with a recommendation. - **The §1.3 census is essentially unrouted.** Stage 1 built the `primary_document_window` seam but it has one production caller; ~80 direct `.active` reads remain. This is Stage 2's bulk, not its tidy-up, and the stage plan sequences it first. - **The statusline active read is three sites, not one.** - **Four scout obligations are still open** and are named rather than papered over, including the GPU-side pixel formula inputs. Also carries the staged plan, draft acceptance criteria, the coherence impact per `COHERENCE.md` §20 (§14 is the section it serves), and four questions for the user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ccf29e3eb5
commit
c2eeb60dc7
|
|
@ -0,0 +1,326 @@
|
|||
# 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<CellCoord>` |
|
||||
| `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.
|
||||
Loading…
Reference in New Issue