docs: bottom-panel Stage 2 framing (revision 2)
Closes review round 1 — 2 blocking, 3 high, 3 revision points — and rebases the ground truth onto `main` @ `d152120`. Both blockers were rev 1 asserting something the parent framing already decided otherwise: - **R1-1.** Rev 1 said all 23 census reads route through `primary_document_window`. Q#BP14 routes only the **Projection** class that way; focus/input (#13-#15, #23), focus chrome and surface-routed (#16-#19), and focus/session (#20) keep their own authorities. Rev 1's rule would have broken remote-op validation and application, `DispatchIdle`, presence, focused search/menu/completion routing, and terminal bell ownership. §3.2 restores the four classes as a table and the acceptance asserts each separately — the focus-class assertions are the load-bearing half, since a test that only proves "the document is used" passes with them wrongly rerouted. - **R1-2.** The three `src/statusline.rs` active reads have two dispositions, not one. Only `:644` selects the wrong window; `:629` and `:675` must keep tracking actual focus, because grid contexts need a truthful `active`, revalidation must notice a focus change, and parent acceptance 42 requires a document provider to be able to observe `active = false` while the panel is focused. The three high findings: - Q#BP2S1 resolves to frontend-owned epochs (option 1) — a font or scale transaction can need to invalidate an old `PanelFrame` while the derived `CellSize` is identical, which daemon value dedup cannot detect. Rev 2 adds the four-row transition table, splits grid allocation from semantic acceptance into two APIs rather than one ambiguous method, moves the grid allocator off `saturating_add` to checked-with-fail-closed, and defines the initial epoch and both exhaustion behaviors. Rev 1's "rejects a lower-or-equal epoch carrying different data" was itself wrong: a lower epoch carrying identical data is still stale. - The `panel_capable` flip is narrowed to an authenticated semantic session negotiated at **v21 or later**. Denying a v20 peer the new events is insufficient if the daemon still places its window in a side panel it cannot render — the gate is on placement. - Parent acceptance criteria 37-55 are declared authoritative and mapped to slices 2A/2B, with rev 1's eleven drafts demoted to refinements. The painter-extraction criterion now pins cells, the returned cursor, the focused window's `view_top` mutation, and passive-window state. All four scout obligations are closed (§5), and the pixel formula is treated as contract work, not implementation detail: - The shared/terminal-only validator boundary is named exactly. - Four new outbox tail-coalescing tags beside the existing four. - **`State::mono_advance` is unsafe to adopt**: absent a `FontFacts` probe it samples the document's first shaped glyph, which would make panel columns document-dependent. The declaration uses the existing stable normal-face `probe_mono_advance` instead, and declares zero usable geometry when it returns `None`. - `BASE_DIVIDER_HEIGHT` does not exist. Rev 2 decides its scaling and requires **one** document-bottom accessor routing every consumer (caret, hits, minimap, terminal geometry, clipping, edge scrolling) — a second unrouted seam is precisely the Stage 1 `Layout::compute` two-caller defect. The concrete base value is left open for round 2. Also: the coherence statement now names journey steps 7-10 instead of claiming none, and drops rev 1's overclaim that this advances background-work visibility — a panel gives output a placement but adds no join key to COHERENCE §9's four disjoint activity planes. The ledger's bottom-panel lane is updated from "no branch and no framing yet" to the framing's real state, and carries an explicit correction: that entry was itself the source of rev 1's census mis-statement. Factual corrections: `InitialTargetResult` is at `message.rs:1145`; `primary_document_window` has four references and two production paths (`daemon.rs:1639`, and `daemon.rs:2998` via `primary_document_buffer`, which is census #22); fifteen PRs merged since the parent's last re-scout, not eleven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2eeb60dc7
commit
69f9a1bdf9
|
|
@ -389,28 +389,42 @@ If it does not, stop and repair the remote/fetch configuration.
|
|||
**isolated-config workspace sweep 3,177 across 92 suites, zero failures**;
|
||||
`git diff --check` clean. Gates were run against the committed tree.
|
||||
|
||||
## Bottom-panel lane (Arc 7) — Stage 1 MERGED; Stage 2 (GPU band) is next
|
||||
## Bottom-panel lane (Arc 7) — Stage 1 MERGED; Stage 2 IN FRAMING
|
||||
|
||||
Stage 1 is on `main`; nothing in this arc is in flight. Stage 2 has **no
|
||||
branch and no framing yet** — the approved parent framing
|
||||
`docs/bottom-panel-framing.md` (rev 4) is what it re-scouts against.
|
||||
Stage 1 is on `main`. **Stage 2 is in framing**, no implementation in
|
||||
flight.
|
||||
|
||||
- Stage 1 merged as **#155** (`main` @ `e745068`, 2026-07-24, after two
|
||||
review rounds). No protocol change. Durable substrate facts live in
|
||||
`docs/agent-handoff.md` §1; the two round lessons are in §5.
|
||||
- Landed-docs follow-up merged as **#156** (`main` @ `d152120`,
|
||||
2026-07-25).
|
||||
- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 2**,
|
||||
on branch `githubsucks/bottom-panel-stage2-framing`, worktree
|
||||
`../pmacs-bp-stage2`, based on `githubsucks/main` @ `d152120`. Round 1
|
||||
closed 2 blocking + 3 high findings; awaiting round 2. The approved
|
||||
parent framing `docs/bottom-panel-framing.md` (rev 4) remains
|
||||
authoritative, **including its acceptance criteria 37–55**.
|
||||
- Retained, carrying nothing unmerged: branch `bottom-panel` and worktree
|
||||
`../pmacs-bottom-panel`.
|
||||
- **Stage 2 obligations, already named by the framing** — the starting
|
||||
point for its own framing doc: `InstanceMessage::PanelFrame` plus
|
||||
`FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`
|
||||
at the next available protocol version, gated in both directions and
|
||||
each extended enum byte-pinned on its own previous final variant;
|
||||
extracting `paint_frame`'s per-window body *together with* the
|
||||
active-window auto-scroll preparation; routing every consumer in the
|
||||
framing's §1.3 census of 23 transitive active-context reads through
|
||||
`primary_document_window`; the focus-chrome surface matrix (Q#BP14b);
|
||||
and Q#BP17's fold-projection parameter plus the stale invariant comment
|
||||
at `src/window.rs`. Stage 3 is the adopter default flip.
|
||||
- **Stage 2 ships as two serial slices**, 2A landing before 2B branches:
|
||||
**2A** = classified §1.3 census routing + `paint_frame` per-window
|
||||
painter extraction (with the active-window auto-scroll preparation), no
|
||||
protocol change; **2B** = protocol **v21**
|
||||
(`InstanceMessage::PanelFrame` plus
|
||||
`FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`,
|
||||
gated both directions, each extended enum byte-pinned on its own
|
||||
previous final variant), daemon panel projection, the GPU band, and the
|
||||
negotiated `panel_capable` flip. Stage 3 is the adopter default flip.
|
||||
- **Correction — this entry previously mis-stated the census contract.**
|
||||
It is **not** "route every consumer through `primary_document_window`".
|
||||
Q#BP14 classifies the 23 reads into four classes and routes only the
|
||||
**Projection** class that way; focus/input (#13–#15, #23), focus chrome
|
||||
and surface-routed (#16–#19), and focus/session (#20) keep their own
|
||||
authorities. Rerouting them would break remote-op validation and
|
||||
application, `DispatchIdle`, presence, focused search/menu/completion
|
||||
routing, and terminal bell ownership. The Stage 2 framing carries the
|
||||
full table.
|
||||
- **Folding Stage 3 and this arc's Stage 2 both touch the semantic
|
||||
projection.** Whichever is framed second re-scouts the other's landed
|
||||
state.
|
||||
|
|
|
|||
|
|
@ -1,326 +1,485 @@
|
|||
# Bottom panel Stage 2 — the GPU panel band (framing)
|
||||
|
||||
**Revision 1 — pre-implementation. Ground truth: canonical `main` @
|
||||
`5aa9044`, protocol v20, 2026-07-25.**
|
||||
**Revision 2 — pre-implementation. Ground truth: canonical `main` @
|
||||
`d152120`, 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.**
|
||||
non-side target. **Stage 2 flips that bit, under an exact negotiated
|
||||
rule, 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.
|
||||
parent's decisions or replace its acceptance criteria. It records the
|
||||
re-scout against current `main`, closes the four scout obligations
|
||||
review round 1 required, and fixes what round 1 found wrong.
|
||||
|
||||
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.
|
||||
**Inherited reading, all of which remains authoritative:** parent
|
||||
Q#BP8 (the band), Q#BP9 (protocol), **Q#BP14 (the primary-document
|
||||
projection contract and its census classification)**, **Q#BP14a (panel
|
||||
input gating is per-window)**, Q#BP14b (focus chrome and per-window
|
||||
overlay routing), Q#BP15 (`PanelFrame` lifecycle), Q#BP15a (three
|
||||
geometries), Q#BP16 (pointer transport), Q#BP17 (fold projection), and
|
||||
**parent acceptance criteria 37–55**.
|
||||
|
||||
## 0. Why the re-scout was required
|
||||
## 0. Revision history
|
||||
|
||||
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.
|
||||
### 0.1 Round 1 (rev 1 → rev 2) — 2 blocking, 3 high, 3 revision points, all closed
|
||||
|
||||
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.
|
||||
- **R1-1 (blocker).** Rev 1 said all 23 census reads route through
|
||||
`primary_document_window`. That contradicts Q#BP14, which routes only
|
||||
the **Projection** class (#1–#12, #21–#22) that way and leaves focus,
|
||||
input, chrome, and bell consumers on their own authorities. Rev 1's
|
||||
rule would have broken remote-op validation, `DispatchIdle`,
|
||||
presence, focused search/menu/completion routing, and bell ownership.
|
||||
§3.2 now restores all four classes; §7's criterion pins them
|
||||
separately. The inherited-reading list above gains Q#BP14 and Q#BP14a.
|
||||
- **R1-2 (blocker).** Rev 1 treated the three `src/statusline.rs`
|
||||
active reads as one disposition. Only `:644` selects the wrong
|
||||
window; `:629` and `:675` must keep tracking **actual focus**. §3.3
|
||||
is rewritten and the criterion states the required behavior instead
|
||||
of routing focus away.
|
||||
- **R1-3 (high).** The `panel_capable` flip needed an exact attach
|
||||
rule, not "for semantic sessions". §3.5 states it: **v21-or-later
|
||||
negotiated authenticated semantic session only**.
|
||||
- **R1-4 (high).** Option 1 accepted, but the epoch needed a state
|
||||
machine, split APIs, and a fail-closed allocator. §3.1 now carries
|
||||
the transition table and the API split. Rev 1's phrasing "rejects a
|
||||
lower-or-equal epoch carrying different data" was itself wrong — a
|
||||
lower epoch carrying *identical* data is still stale.
|
||||
- **R1-5 (high).** Rev 1's eleven draft criteria silently omitted
|
||||
parent 37–55. §7 now declares the parent list authoritative, maps it
|
||||
to 2A/2B, and adds only refinements. The painter-extraction criterion
|
||||
pins cursor, `view_top`, and passive-window state, not just cells.
|
||||
- **R1-6.** All four scout obligations are closed in §5.
|
||||
- **R1-7.** The coherence statement understated journey impact and
|
||||
overclaimed on background work. §6 names journey steps 7–10 and
|
||||
narrows the §9 claim.
|
||||
- **R1-8.** Factual corrections in §1 and §3.2.
|
||||
|
||||
## 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 | Verdict |
|
||||
| --- | --- | --- |
|
||||
| `paint_frame` returns cursor separately (`editor.rs:2833`) | `src/editor.rs:3171` | Holds |
|
||||
| Cursor-visible prep (`editor.rs:2883-2935`) | `src/editor.rs:3249+` | Holds; Stage 1 inserted work above it (§2) |
|
||||
| Per-window paint body (`editor.rs:2937-3040`) | after `src/editor.rs:3260` | Holds |
|
||||
| `fold_map_for_window` gates on the **active** frontend (`editor_core.rs:566`) | `src/editor_core.rs:734`, gate at `:738` | Holds |
|
||||
| Stale "semantic session never enters `paint_frame`" (`window.rs:339`) | `src/window.rs:562` | Holds, still stale; now embedded in a longer `fold_projection` doc block, so the edit is a paragraph rewrite |
|
||||
| `Mouse` is contractually the grid path (`daemon.rs:3122-3130`) | `src/daemon.rs:3123` | Holds |
|
||||
| Permanent `24×80` placeholder (`attach.rs:420-429`, `:573-577`) | `pmacs-gpu/src/attach.rs:577`, single site | Holds |
|
||||
| Byte pin `InstanceMessage::InitialTargetResult` | `pmacs-protocol/src/message.rs:1145` | Holds — still the enum's final variant |
|
||||
| Byte pin `FrontendEvent::TerminalPointer` | final variant of its enum | Holds |
|
||||
|
||||
| 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 |
|
||||
**Protocol is still v20** (`pmacs-protocol/src/message.rs:1568`); no
|
||||
intervening PR bumped it. Q#BP9's conditional resolves: **Stage 2 is
|
||||
v21**, no reservation was taken and none was needed.
|
||||
|
||||
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.
|
||||
Fifteen PRs merged between the parent's last re-scout (`47581f4`) and
|
||||
this one: #149, #150, #152–#155, #158–#166. Nothing in the parent's
|
||||
mechanical model was falsified by any of them.
|
||||
|
||||
## 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 }`
|
||||
(`src/window.rs:522-528`), held as
|
||||
`FrontendView::frame_geometry: Option<_>` (`:589`) where `None` means
|
||||
**unknown** — Q#BP15a's "unknown is first-class", already landed.
|
||||
- `EditorState::sync_frame_geometry` (`src/editor.rs:877-882`) →
|
||||
`declare_frame_geometry` + `reconcile_panel_layout`, driven from two
|
||||
daemon sites gated on `panel_capable_for` (`src/daemon.rs:1882-1883`
|
||||
attach, `:1972-1973` resize).
|
||||
- `paint_frame` declares geometry itself (`src/editor.rs:3187`), before
|
||||
the statusline fan-out and before the long mutable core borrow.
|
||||
- `StatuslineEvaluationTarget` (`src/statusline.rs:212-226`) is already
|
||||
a two-variant enum, so Q#BP8's fan-out generalization is an added
|
||||
variant, not a refactor.
|
||||
- `primary_document_window` (`src/editor_core.rs:2830`) and
|
||||
`primary_document_buffer` (`:2845`).
|
||||
|
||||
- **`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. Findings and decisions
|
||||
|
||||
## 3. What the re-scout found
|
||||
### 3.1 Q#BP2S1 — epoch ownership, resolved: frontend-owned, with an exact state machine
|
||||
|
||||
### 3.1 The geometry epoch is allocated daemon-side; the wire contract says frontend-side
|
||||
**Decision: option 1.** The epoch is owned by the frontend for
|
||||
negotiated semantic-panel sessions. The deciding argument is one rev 1
|
||||
missed: **a font or scale transaction can require invalidating an old
|
||||
`PanelFrame` even when the derived `CellSize` is identical.** Daemon
|
||||
value dedup cannot detect that case, because the cell totals it
|
||||
compares are unchanged while the pixels behind them are not.
|
||||
|
||||
This is the one genuine conflict between landed Stage 1 and framed
|
||||
Stage 2, and it needs a decision before implementation.
|
||||
The landed allocator conflicts in three ways
|
||||
(`src/editor_core.rs:3155-3172`): it allocates the id itself, it
|
||||
early-returns when `total` is unchanged (value dedup), and it uses
|
||||
`saturating_add`, which is neither wrapping nor fail-closed — it pins
|
||||
at `u64::MAX`, after which two different geometries share one id.
|
||||
|
||||
`declare_frame_geometry` (`src/editor_core.rs:3155-3172`) **allocates
|
||||
the epoch itself**:
|
||||
**Acceptance rules for a semantic declaration:**
|
||||
|
||||
```rust
|
||||
let next = view
|
||||
.frame_geometry
|
||||
.map_or(1, |geometry| geometry.geometry_epoch.saturating_add(1));
|
||||
```
|
||||
| Incoming declaration | Result |
|
||||
| --- | --- |
|
||||
| epoch **greater** than stored | Accept, store **verbatim**, even if `total` is unchanged |
|
||||
| same epoch, same `total` | Idempotent no-op |
|
||||
| same epoch, **different** `total` | Reject |
|
||||
| **lower** epoch, any `total` | Reject |
|
||||
|
||||
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).
|
||||
The last row is deliberate and corrects rev 1: a lower epoch carrying
|
||||
identical data is still stale and must not be accepted.
|
||||
|
||||
Two further details of the landed allocator matter:
|
||||
**API split.** Two methods, not one method with an optional epoch:
|
||||
|
||||
- **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.
|
||||
- `declare_frame_geometry(fid, total)` — the **grid/LOCAL** allocator.
|
||||
Keeps value dedup (correct there: cells are the unit, and an
|
||||
unchanged grid means an old frame is still valid under unchanged
|
||||
metrics). Changes from `saturating_add` to **checked** allocation
|
||||
with an explicit fail-closed exhaustion arm.
|
||||
- `accept_frame_geometry(fid, geometry_epoch, total) -> bool` — the
|
||||
**semantic** path. No value dedup; applies the table above verbatim;
|
||||
returns whether the declaration was accepted so the caller can drop
|
||||
a stale event before any reconciliation.
|
||||
|
||||
**Q#BP2S1 (new, needs a decision).** Three candidate resolutions:
|
||||
An ambiguous single method with an `Option<u64>` epoch is rejected
|
||||
explicitly: it would let a future caller silently take the wrong regime.
|
||||
|
||||
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.
|
||||
**Initial epoch and exhaustion.** The frontend's first declaration
|
||||
after attach acceptance carries epoch `1`; `0` is reserved as "never
|
||||
declared" and is rejected on the wire. Frontend-side allocation is
|
||||
checked; on exhaustion the frontend stops declaring and **hides its
|
||||
panel** rather than reusing or wrapping an id — it sends no further
|
||||
geometry, so the daemon's last accepted declaration stands and no new
|
||||
`Present` can claim a fresh identity. Daemon-side exhaustion on the
|
||||
grid path fails closed the same way: no new declaration, panel stays
|
||||
at its last valid geometry or hides under Q#BP2b.
|
||||
|
||||
**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 census is classified, and it is mostly unrouted
|
||||
|
||||
### 3.2 The §1.3 census is essentially unrouted
|
||||
**Correction to rev 1.** Q#BP14 routes only the **Projection** class
|
||||
through `primary_document_window`. Rev 1's "all 23 reads" was wrong and
|
||||
would have broken five subsystems. The four classes, restored:
|
||||
|
||||
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`.
|
||||
| Class | Census items | Authority |
|
||||
| --- | --- | --- |
|
||||
| **Projection** | #1–#7, #9, #10, #12, #21, #22 | `primary_document_window` / `primary_document_buffer` |
|
||||
| **Projection + focus** | #8 (document `Pointer`), #11 (full-window `TerminalPointer`) | Align the primary document window **and then activate it** — the one place the two legitimately move together |
|
||||
| **Focus / input** | #13 (remote-op validation), #14 (`dispatch_idle_for`), #15 (presence), #23 (remote-op application) | The frontend's **actually focused** window. Q#BP14a: gating is per-window, never per-buffer |
|
||||
| **Focus chrome / surface-routed** | #16–#19 (search, menu, minibuffer, completion) | Q#BP14b's routing table — the currently owned surface, with authoritative clears for the other |
|
||||
| **Focus / session** | #20 (terminal bell drain) | Per-session counter; the **focused** window chooses which session may drain |
|
||||
|
||||
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`.
|
||||
Rerouting any of the last three classes to the document is a defect,
|
||||
not a simplification: it would break remote-op validation and
|
||||
application, `DispatchIdle`, presence, focused search/menu/completion
|
||||
routing, and bell ownership.
|
||||
|
||||
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.
|
||||
**How much is already routed.** `primary_document_window` has **four**
|
||||
references in `src/` and **two production paths**: directly at
|
||||
`src/daemon.rs:1639` (#148's initial-target bootstrap, Q#BP11b), and
|
||||
through `primary_document_buffer` at `src/daemon.rs:2998`, which is
|
||||
census **#22** and carries a comment naming it. So one census item is
|
||||
routed and the Projection class is otherwise open. For scale, `src/*.rs`
|
||||
still holds ~80 non-test direct `.active` reads on top of the
|
||||
`active_window*` / `active_buffer*` helper family
|
||||
(`src/editor_core.rs:663-967`).
|
||||
|
||||
### 3.3 The statusline read is three sites, not one
|
||||
This is not a Stage 1 defect — with `panel_capable = false` no semantic
|
||||
frontend can hold a side window, so the unrouted Projection reads are
|
||||
unreachable from the GPU. It does mean **classified census routing is
|
||||
the bulk of Stage 2**, which is why it is Stage 2A.
|
||||
|
||||
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.3 The three statusline reads have two dispositions, not one
|
||||
|
||||
### 3.4 Scout obligations still open
|
||||
All three sites are real, but only one is wrong:
|
||||
|
||||
Stated plainly rather than papered over. These were not re-verified in
|
||||
this pass and must be before the doc leaves revision 1:
|
||||
- `src/statusline.rs:644` — `.get(&view.active)` **selects the wrong
|
||||
window** when a panel is focused. This is the Projection read (#12).
|
||||
- `src/statusline.rs:629` and `:675` — `active: window_id ==
|
||||
view.active` **must continue tracking actual focus**. Three reasons:
|
||||
grid contexts need a truthful `active`; post-callback revalidation
|
||||
must notice a focus change; and parent acceptance 42 explicitly
|
||||
requires that a document provider may observe `active = false` while
|
||||
the panel is focused.
|
||||
|
||||
- `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.
|
||||
**The new semantic-layout target** therefore captures the **primary
|
||||
document window plus the visible side window**, marks each context
|
||||
`active` iff its `window_id == view.active`, invokes each provider
|
||||
**exactly once**, and **invalidates the entire evaluation** if a
|
||||
callback mutates layout or focus. Unprojected document splits run no
|
||||
callbacks (Q#BP8). Route the primary-document result to semantic
|
||||
`StatuslineSegments` and the side result to the panel mode line.
|
||||
|
||||
### 3.4 Fold projection
|
||||
|
||||
Unchanged from Q#BP17, with the anchor corrected: the extracted painter
|
||||
takes the map as a **parameter**; the panel path passes `None` when the
|
||||
owning frontend's `fold_projection` is false and must never call
|
||||
`fold_map_for_window`, which gates on the **active** frontend
|
||||
(`src/editor_core.rs:734`, gate at `:738`) — right for command-time
|
||||
reckoning, wrong for painting another frontend's panel. The stale
|
||||
comment is at `src/window.rs:562`.
|
||||
|
||||
### 3.5 The `panel_capable` flip needs a negotiated rule
|
||||
|
||||
Not "true for semantic sessions". Exactly:
|
||||
|
||||
> `panel_capable = true` **only** for an authenticated semantic session
|
||||
> that negotiated **v21 or later**.
|
||||
|
||||
A v6–v20 semantic frontend stays non-panel-capable and takes the
|
||||
existing Stage 1 fallback: the non-side target with **every
|
||||
side-specific parameter discarded**, leaving the document window
|
||||
undedicated (Q#BP2c). "It receives no new events" is insufficient — if
|
||||
the daemon nevertheless places that frontend's window in a side panel
|
||||
it cannot render, the window becomes invisible. The gate is on
|
||||
placement, not only on transport. Parent acceptance 51 pins the mixed
|
||||
session.
|
||||
|
||||
## 4. Revisions to the parent framing
|
||||
|
||||
Only these. Everything else in Q#BP8/9/14b/15/15a/16/17 stands.
|
||||
Only these; everything else 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.
|
||||
- **Q#BP9 resolves to v21.**
|
||||
- **Q#BP15a's epoch ownership is specified** by §3.1's table and API
|
||||
split, replacing the parent's one-line "frontend-owned" statement.
|
||||
- **Q#BP8's statusline criterion splits** per §3.3: one read reroutes,
|
||||
two keep tracking focus.
|
||||
- **Q#BP17's stale comment is at `src/window.rs:562`**, and parent
|
||||
acceptance 52's reference to `:339` should be read against that.
|
||||
|
||||
## 5. What ships, in order
|
||||
## 5. The four scout obligations, closed
|
||||
|
||||
Sequenced so each step is independently gateable and the census — the
|
||||
riskiest part — lands before anything depends on it.
|
||||
### 5.1 The shared cell-grid validator boundary
|
||||
|
||||
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.
|
||||
`TerminalFrame::validate` (`pmacs-protocol/src/terminal.rs:226`)
|
||||
currently interleaves both concerns. The exact split:
|
||||
|
||||
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.**
|
||||
- **Factored into the shared parameterized wire-cell-grid validator:**
|
||||
checked area (the `checked_mul` + `usize::try_from` guard), the
|
||||
`MAX_TERMINAL_VISIBLE_CELLS = 262,144` aggregate cap, cell-count
|
||||
equality against declared area, cursor-in-bounds, and
|
||||
`validate_cells`'s glyph width / continuation topology and aggregate
|
||||
glyph-byte checks.
|
||||
- **Stays terminal-only:** the `MAX_TERMINAL_ROWS/COLS = 512` per-axis
|
||||
caps in `checked_area`, `validate_metadata` for title/signal/crash
|
||||
text, `validate_selection`, and the `at_bottom == (scroll_offset ==
|
||||
0)` coupling.
|
||||
|
||||
`PanelFrame` takes the shared half plus its own presence/epoch rules
|
||||
and does **not** inherit the 512 per-axis cap (Bet B5'), so a 4K
|
||||
small-font panel wider than 512 columns is legal while the shared area
|
||||
budget still binds. Parent acceptance 39 pins exactly this.
|
||||
|
||||
### 5.2 The GPU outbox needs four more tags
|
||||
|
||||
`coalesce_kind` (`pmacs-gpu/src/attach.rs:331`) today returns four
|
||||
tail-only tags: `Viewport` → 0, `Pointer{Drag}` → 1,
|
||||
`TerminalPointer{Move}` → 2, `TerminalPointer{Drag}` → 3. Everything
|
||||
else is `None` = lossless, counting against `OUTBOX_MAX = 8192`.
|
||||
|
||||
Stage 2 adds **four distinct tags**: `FrontendCellGeometry` → 4,
|
||||
`PanelResizeRows` → 5, `PanelPointer{Move}` → 6, `PanelPointer{Drag}`
|
||||
→ 7. Geometry is latest-wins (epochs need only increase, not be
|
||||
consecutive); resize drag is latest-wins over the complete event
|
||||
including its epochs. `PanelPointer` `Down`/`Up`/wheel/context stay
|
||||
lossless and ordered — repeated left `Down`s are what the daemon click
|
||||
state reads as a multi-click, and `Down(Right)` is the context-menu
|
||||
gesture. Tail-only replacement preserves ordering across an
|
||||
intervening event of any other class.
|
||||
|
||||
### 5.3 The pixel formula's inputs — and one trap
|
||||
|
||||
The formula in Q#BP15a is contract-level, not an implementation
|
||||
detail, because its inputs are not all safe to adopt:
|
||||
|
||||
| Input | Source | Note |
|
||||
| --- | --- | --- |
|
||||
| `status_band_height_px` | `FontMetrics::status_band_height` (`pmacs-gpu/src/main.rs:137`) = `BASE_STATUS_BAND_HEIGHT * scale` | Safe |
|
||||
| `TEXT_TOP_px` | `const TEXT_TOP: f32 = 16.0` (`main.rs:352`) | Safe; unscaled today |
|
||||
| `code_line_height_px` | `FontMetrics::code_line_height` (`main.rs:131`) = `BASE_CODE_LINE_HEIGHT * scale` | Safe |
|
||||
| `resolved_monospace_advance_px` | `State::mono_advance` (`main.rs:4899`) | **Unsafe to adopt blindly** |
|
||||
| `divider_height_px` | `BASE_DIVIDER_HEIGHT` | **Does not exist yet** |
|
||||
|
||||
**The `mono_advance` trap.** `State::mono_advance` returns
|
||||
`measured_mono_advance` when a `FontFacts` probe has been applied, but
|
||||
otherwise falls back to **the first shaped glyph of the document
|
||||
buffer** (`main.rs:4903+`). Panel column count would therefore become
|
||||
**document-dependent**: two GPU frontends showing different files could
|
||||
derive different `total.cols` from identical metrics, and the same
|
||||
frontend's panel width could change when the document's first glyph
|
||||
changes.
|
||||
|
||||
**Decision.** The panel geometry declaration uses a **stable normal-face
|
||||
probe**, never the document sample. `probe_mono_advance(font_system,
|
||||
family, metrics)` (`main.rs:323`) already exists and is exactly this: it
|
||||
shapes `ADVANCE_PROBE` in a scratch buffer, independent of document
|
||||
contents, dividing total run width by logical cells so ligature
|
||||
substitution survives. The declaration resolves its advance from that
|
||||
probe for the current family/metrics. If the probe returns `None` (the
|
||||
family shapes no width), the frontend declares **zero usable geometry**
|
||||
under a new epoch — the panel hides — rather than falling back to a
|
||||
document sample.
|
||||
|
||||
**`BASE_DIVIDER_HEIGHT` must be decided before implementation**, because
|
||||
the document-bottom seam it defines is depended on by caret placement,
|
||||
hit testing, the minimap, terminal geometry, clipping, and edge
|
||||
scrolling. Two sub-decisions:
|
||||
|
||||
- **Value and scaling.** It joins the `BASE_*` family and scales as
|
||||
`BASE_DIVIDER_HEIGHT * scale`, matching `status_band_height` — a
|
||||
divider that does not scale with the font would misalign at non-1.0
|
||||
scale. The concrete base value is an open item for review round 2.
|
||||
- **One seam, not several.** Today the document bottom is computed
|
||||
from `status_band_height` at several sites
|
||||
(`main.rs:3175`, `:3185`, `:6601`, `:6607`, `:8491`, and the
|
||||
status-band rect at `:5910`). Stage 2 must introduce **one**
|
||||
document-bottom accessor that subtracts status band **plus** the
|
||||
installed band and divider, and route every one of those sites
|
||||
through it. A second, unrouted seam is the exact shape of Stage 1's
|
||||
`Layout::compute` two-caller defect, where `src/overlay_paint.rs`
|
||||
derived its own rect and painted peer cursors at unfixed rows.
|
||||
|
||||
Note the asymmetry Q#BP15a already requires: `divider_height_px` is
|
||||
subtracted **for sizing purposes even while the panel is absent**, to
|
||||
break the first-open cycle, while the document renderer does not
|
||||
actually lose those pixels until a `Present` panel is painted.
|
||||
|
||||
### 5.4 Ordering against folding Stage 3
|
||||
|
||||
Settled by review round 1: **bottom-panel Stage 2 first, through the
|
||||
landed GPU band.** Folding Stage 3 then re-scouts the extracted
|
||||
painter, the panel projection, clipping, and `fold_projection` behavior
|
||||
exactly once.
|
||||
|
||||
## 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
|
||||
- **Journey steps touched: four, on the GPU frontend — steps 7–10**
|
||||
(find symbol / find file, terminal, build and test, error
|
||||
inspection). Rev 1 said "none directly", which contradicted its own
|
||||
next sentence. Today a GPU user who triggers references, project
|
||||
search, a terminal, compile, or error inspection gets the Stage 1
|
||||
non-side fallback: the output surface steals a document window
|
||||
instead of opening a panel. Every one of those steps therefore
|
||||
behaves differently on GPU than on TUI, and Stage 2 is what closes
|
||||
that.
|
||||
the divergence.
|
||||
- **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
|
||||
Stage 2 extends one already-adopted policy (`display = "panel"`,
|
||||
used by listview, compile, and terminal) to a second frontend rather
|
||||
than minting a GPU-only surface. Q#BP14b deliberately reuses the
|
||||
existing `SearchPrompt` / `MenuPrompt` / `CompletionPopup` messages
|
||||
instead of panel-specific twins.
|
||||
- **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.
|
||||
registry. Stage 2 adds no new user-facing option; if the band needs
|
||||
one, it enters the registry.
|
||||
- **Background-work attribution: unchanged, and this stage does not
|
||||
advance it.** Rev 1 implied Stage 2 helps §9's activity-view gap. It
|
||||
does not. A panel gives output a coherent *placement*; it does not
|
||||
make terminal PTYs, LSP servers, or workers appear in the
|
||||
activity/ownership view §9 describes, and it adds no join key across
|
||||
the four disjoint activity planes. The §9 gap is untouched.
|
||||
- **Section this serves:** `COHERENCE.md` §14, which records the panel
|
||||
primitive as landed for Stage 1 and names "Stage 2 (GPU band)
|
||||
pending its own framing" as the open item.
|
||||
|
||||
## 7. Acceptance criteria (draft)
|
||||
## 7. Acceptance
|
||||
|
||||
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.
|
||||
**Parent criteria 37–55 remain authoritative and are not replaced.**
|
||||
This section maps them to the two slices and adds only refinements.
|
||||
|
||||
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.
|
||||
### 7.1 Stage 2A — classified census routing + painter extraction
|
||||
|
||||
## 8. Questions for the user
|
||||
No protocol change. Parent criteria that apply in full: **42, 43, 44,
|
||||
51 (the `LOCAL`-panel inheritance half), 52**.
|
||||
|
||||
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?
|
||||
Refinements 2A adds:
|
||||
|
||||
## 9. Gates
|
||||
- **A2A-1 (replaces rev 1's criterion 1).** Every **Projection** census
|
||||
item (#1–#7, #9, #10, #12, #21, #22) resolves through
|
||||
`primary_document_window` / `primary_document_buffer`; #8 and #11
|
||||
align **and then activate**; **#13, #14, #15, #23 continue to resolve
|
||||
the actually focused window**; #16–#19 follow Q#BP14b's routing
|
||||
table; #20 keeps its per-session counter with focus choosing the
|
||||
eligible terminal. Each class is asserted separately, at the
|
||||
outermost user-reachable seam, and falsified by revert. A test that
|
||||
only proves "the document is used" would pass with the focus classes
|
||||
wrongly rerouted, so the focus-class assertions are the load-bearing
|
||||
half.
|
||||
- **A2A-2 (replaces rev 1's criterion 2).** `src/statusline.rs:644`
|
||||
resolves the primary document window, while `:629` and `:675`
|
||||
continue to report **actual focus** — pinned by a document provider
|
||||
truthfully observing `active = false` while the panel is focused
|
||||
(parent 42). The semantic-layout target captures primary document +
|
||||
visible side window, invokes each provider exactly once, and
|
||||
invalidates the whole evaluation when a callback mutates layout or
|
||||
focus.
|
||||
- **A2A-3 (replaces rev 1's criterion 3).** The painter extraction
|
||||
preserves, for grid frontends: the painted **cells**, the **returned
|
||||
cursor**, the **focused window's `view_top` mutation** from the
|
||||
auto-scroll clamp, and **passive windows' untouched `view_top` and
|
||||
scroll state**. Byte-identical cells alone would not catch a clamp
|
||||
that silently moved to the wrong window.
|
||||
|
||||
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.
|
||||
### 7.2 Stage 2B — v21 protocol + daemon projection + GPU band
|
||||
|
||||
Parent criteria that apply in full: **37, 38, 39, 40, 41, 45, 46, 47,
|
||||
48, 49, 50, 51, 53, 54, 55**, plus re-assertion of 42/43/44 **through
|
||||
the actual negotiated capability flip** rather than through a
|
||||
test-only panel-capable semantic view.
|
||||
|
||||
Refinements 2B adds:
|
||||
|
||||
- **A2B-1.** The epoch state machine of §3.1 is pinned row by row,
|
||||
including the lower-epoch-identical-data rejection and the
|
||||
same-epoch-different-total rejection. Grid allocation is checked with
|
||||
a fail-closed exhaustion arm; the semantic path performs no value
|
||||
dedup. Epoch `0` is rejected on the wire.
|
||||
- **A2B-2.** A font or scale change that leaves `CellSize` **identical**
|
||||
still produces a new `geometry_epoch`, and the older `PanelFrame`
|
||||
neither paints nor hit-tests until a matching `Present` arrives. This
|
||||
is the case daemon value dedup cannot see and is why option 1 was
|
||||
chosen.
|
||||
- **A2B-3.** Panel columns are derived from the **stable normal-face
|
||||
probe**, not `State::mono_advance`'s document-glyph fallback: two GPU
|
||||
frontends with identical metrics and different documents derive
|
||||
identical `total.cols`, and a probe returning `None` declares zero
|
||||
usable geometry rather than falling back to a document sample.
|
||||
- **A2B-4.** Every document-bottom consumer — caret, hit test, minimap,
|
||||
terminal geometry, clipping, edge scrolling — routes through the one
|
||||
document-bottom accessor. Falsified by introducing a band and
|
||||
asserting each consumer moves; a second unrouted seam is the Stage 1
|
||||
`Layout::compute` defect class.
|
||||
- **A2B-5.** `panel_capable` is true only for a v21+ negotiated
|
||||
authenticated semantic session; a v20 semantic session is never
|
||||
**placed** in a side window, not merely denied the events.
|
||||
|
||||
## 8. Open items for review round 2
|
||||
|
||||
1. The concrete `BASE_DIVIDER_HEIGHT` value (§5.3). Scaling and the
|
||||
single-seam rule are decided; the number is not.
|
||||
2. Whether `TEXT_TOP` should scale. It is an unscaled constant today
|
||||
and the formula consumes it as-is; that is pre-existing behavior
|
||||
Stage 2 inherits rather than fixes, but it is worth a decision
|
||||
before the conversion is pinned by acceptance.
|
||||
|
||||
## 9. Slices, branches, and gates
|
||||
|
||||
Per review round 1: **two serial implementation PRs**, each a named
|
||||
slice under this framing so one-feature/one-branch/one-PR holds. **2A
|
||||
lands before 2B branches** — not stacked.
|
||||
|
||||
- **Stage 2A** — classified census routing + per-window painter
|
||||
extraction. Branch `bottom-panel-stage2a`. No protocol change.
|
||||
- **Stage 2B** — v21 protocol, daemon panel projection, GPU band, and
|
||||
the negotiated `panel_capable` flip. Branch `bottom-panel-stage2b`,
|
||||
cut from `main` after 2A merges. Repeats 2A's relevant census
|
||||
assertions through the real capability flip.
|
||||
|
||||
Gates for both: the standing suite, plus
|
||||
`bottom_panel_stage1_acceptance`, the new
|
||||
`bottom_panel_stage2a_acceptance` / `bottom_panel_stage2b_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 2B. Parent
|
||||
criterion 54's `--headless-probe` run — one real daemon, real PTY, real
|
||||
wgpu, through a panel-hosted terminal — is a 2B gate.
|
||||
|
|
|
|||
Loading…
Reference in New Issue