Merge pull request #156 from levineuwirth/docs-bottom-panel-landed

docs: record bottom-panel Stage 1 (#155) landing
This commit is contained in:
Levi Neuwirth 2026-07-25 22:00:02 +00:00 committed by GitHub
commit d152120ff8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 167 additions and 154 deletions

View File

@ -517,153 +517,31 @@ If it does not, stop and repair the remote/fetch configuration.
**isolated-config workspace sweep 3,177 across 92 suites, zero failures**; **isolated-config workspace sweep 3,177 across 92 suites, zero failures**;
`git diff --check` clean. Gates were run against the committed tree. `git diff --check` clean. Gates were run against the committed tree.
## Bottom-panel lane (window placement + side windows) — Stage 1 IN REVIEW ## Bottom-panel lane (Arc 7) — Stage 1 MERGED; Stage 2 (GPU band) is next
- Portable branch: `githubsucks/bottom-panel`, worktree Stage 1 is on `main`; nothing in this arc is in flight. Stage 2 has **no
`../pmacs-bottom-panel`, based on `githubsucks/main` @ `ddaa80d`. branch and no framing yet** — the approved parent framing
- Approved framing: `docs/bottom-panel-framing.md` revision 4, committed `docs/bottom-panel-framing.md` (rev 4) is what it re-scouts against.
as the branch's first commit (`c27f75a`).
- **Stage 1 implemented; no wire change (protocol stays v20).** What - Stage 1 merged as **#155** (`main` @ `e745068`, 2026-07-24, after two
landed on the branch: review rounds). No protocol change. Durable substrate facts live in
- `src/window.rs`: `WindowParams` (`side` / `fixed_rows` / `dedicated` `docs/agent-handoff.md` §1; the two round lessons are in §5.
+ implementation-owned `quit_action` and `origin_document`), `Side`, - Retained, carrying nothing unmerged: branch `bottom-panel` and worktree
a depth-bounded `QuitAction`, `MIN_WINDOW_OUTER_ROWS = 2`, `../pmacs-bottom-panel`.
`Layout::compute(area, fixed)`, the `subtree_min_rows` / - **Stage 2 obligations, already named by the framing** — the starting
`interactive_min_rows` recursions, `boundary_below`, and the three point for its own framing doc: `InstanceMessage::PanelFrame` plus
new `FrontendView` fields (`panel_capable`, `frame_geometry`, `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`
`panel_hidden`). at the next available protocol version, gated in both directions and
- `src/editor_core.rs`: `primary_document_window`, the non-side target each extended enum byte-pinned on its own previous final variant;
rule, `display_buffer` + the Q#BP3 placement policy, `quit_window`, extracting `paint_frame`'s per-window body *together with* the
`reconcile_panel_layout_core`, `resize_boundary`, per-frontend active-window auto-scroll preparation; routing every consumer in the
`JumpEntry`s, and the shared `resolve_target_buffer` seam that the framing's §1.3 census of 23 transitive active-context reads through
#148 initial-target bootstrap now routes through as well. `primary_document_window`; the focus-chrome surface matrix (Q#BP14b);
- `src/editor.rs`: the reconciliation transaction, geometry and Q#BP17's fold-projection parameter plus the stale invariant comment
declaration, the side-window `dispatch_idle_for` gate, the divider at `src/window.rs`. Stage 3 is the adopter default flip.
paint, and the divider drag. - **Folding Stage 3 and this arc's Stage 2 both touch the semantic
- `src/lua_bindings/window_panel.rs`: the whole `pmacs.window` panel projection.** Whichever is framed second re-scouts the other's landed
surface plus the shared adopter-placement helpers; state.
`builtin/runtime/window.lua` owns `window.panel-height` /
`window.min-height` and the resize commands.
- Adopters: `listview.open`, `compile.run`, `pmacs.terminal.open` all
take `display = "current" | "panel"` (Stage 1 default `"current"`);
LSP/compile visits route through `display_file`.
- **Review round 1 addressed.** The load-bearing finding: the Q#BP6
side-window split guard (`try_split_active`) had **no production
caller** — `pmacs.window.split_horizontal` / `split_vertical`, and so
`C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a
focused panel made the root wrapper's final child a split rather than
`Leaf(side)`, which both `Layout::compute`'s fixed pass and
`document_subtree` key on. It survived the first round because the
acceptance test called the core method **directly**; it now goes
through the real Lua binding. This is the folding-arc round-2 lesson
repeating exactly: *after wiring a guard into a production hook, pin it
through the real path — a direct-call test misses the wiring.*
Also fixed: the armed divider drag was not scoped to its arming
frontend (it could cancel and swallow a peer's mouse events); a
recompile carries no `display` and duplicated a panel-placed
`*compilation*` into the document window; and
`paint_mode_line_graphemes` had lost its doc block to an insertion.
Five bite-verified fixes (three via `scripts/bite`, two by manual
revert since their tests share `src/daemon.rs` with the production
code).
- Two Stage-2 hazard pins now exist in `src/daemon.rs`, closing the gap
the review named: a fresh attach while `LOCAL` is focused in a panel
inherits `LOCAL`'s **document** buffer, and an initial-target bootstrap
whose `after-load` hook creates and selects a panel still reasserts
into a document window.
- **Review round 2 addressed.** The load-bearing finding: **Q#BP7 item 1
— "growth reaching the live tail re-arms follow" — was never
implemented.** `at_bottom` is the instantaneous geometric readout
`scroll_offset == 0`, which a still-anchored view satisfies whenever it
is momentarily tall enough to reach the tail, so the round-1 assertion
could not see the gap: the next rows the child printed pushed the
anchored view back into history. `src/terminal/view.rs` now has
`rearm_follow_on_growth`, reached by one shared `declare_view_size`
helper from every size-declaring path (`snapshot_for_view`,
`record_view_size`, `view_status_for_size`) so grid and semantic
declarations cannot disagree.
Also fixed: the PTY fixtures emitted LF-only output, which staircases
until every row clips to blanks — so the anchor assertions compared
`""` with `""` and could not fail (now CRLF, each guarded by
`assert!(!top_before.is_empty())`); acc33's contrast case asserted
nothing; `start_run` let `already_in_panel` override an **explicit**
`display = "current"`, which is the documented opt-out from the Stage 3
flip (now gated on omission); and `window_drag` was a daemon-global
slot that a peer's mode-line press could clear.
- Durable test lessons from this round, both the same class:
1. **A geometric readout is not a state predicate.** `at_bottom` says
"the viewport currently reaches the tail", not "this view follows
the tail". Pinning follow requires feeding MORE output and asserting
the view moved (acc32b uses a filesystem gate between two bursts).
2. **A PTY in the default mode does not translate LF to CRLF.** An
`echo`-driven fixture staircases rightward and clips to blanks past
the viewport width, so any text equality over it is vacuously true.
Emit `\r\n`, and guard text comparisons with a non-empty assertion
the way the daemon pin guards on `!panel_hidden`.
- **Round-2 self-review caught a regression the round-2 commit
introduced**, in the change it labelled "minor": routing
`pmacs.window.buffer()`'s **no-argument** arm through the fid-scoped
`selected_window` validator made it **fallible**, and
`acting_frontend` can name a frontend with **no registered view** (a
bare `dispatch_key` from an unattached peer does exactly that). The
runtime calls that function on ordinary edits from `killring`,
`syntax`, `autosave`, `pair`, `indent` and `comment` **without
`pcall`**, so the raise never surfaced as an error — it silently
dropped the operation. `kill_ring_acceptance` went 30/30 → 25/5
(`frontend_detached_drops_per_frontend_state`: "B has kill state").
The no-arg arm is back on ambient `active_buffer_id()` and documented
as deliberately infallible; the explicit-window arm keeps its Q#BP11
validation. New **acc19c** pins it through the real path (a
`buffer.after-edit` subscriber during a viewless peer's `dispatch_key`)
and bites against the regressing commit.
Generalizes: **a "uniformity" cleanup that changes a function's
fallibility is not minor** — check every caller's error discipline
first, and remember that an ambient resolver's fallback IS its
contract.
- Verification on this branch: `cargo fmt --check` clean; strict
workspace Clippy clean; 1,817 default + 1,994 CRDT library tests;
`bottom_panel_stage1_acceptance` 46/46; kill ring 30 default + 30 CRDT;
vterm Stage 1 9 default + 10 CRDT; M4 121; required GPU 152;
compile 67; vterm Stage 2 4 / Stage 3 5 (7 CRDT); folding Stage 2 48;
statusline 7; listview 6;
**isolated-config workspace sweep 3,130 passed across 89 suites, zero
failures**; `git diff --check` clean.
- **Run the sweep with an isolated `XDG_CONFIG_HOME`.** The real
`~/.config/pmacs/init.lua` on this desktop calls
`pmacs.packages.install_local(...)`, so every editor the sweep builds
races on one shared install root; a losing race sets a status message
that leaks into the mode line and breaks
`folding_stage2_acceptance::unfolded_frame_is_identical_to_the_pre_folding_baseline`,
which compares whole painted frames. Standalone it is 48/48. This
generalizes the known `compile_mode_acceptance` real-config trap:
any suite that paints the status area inherits it.
- **A latent pre-existing `main` bug surfaced while gating and is NOT
this branch's**: `buffer::tests::proptests::rope_matches_crdt_projection_after_arbitrary_edits`
fails on `main` @ `352bf0b` with `ops = [Insert(0,"a"),
Insert(0,"aaa"), Replace(0,1,"a"), Undo]` — undo of a textually-null
`Replace` returns a no-op edit result still carrying `crdt_op =
Some`, violating the suite's own shape invariant. `src/buffer.rs` is
byte-identical here, and the seed was deliberately **not** committed
(it would make an unrelated failure deterministically red on this
PR). Needs its own lane.
- Durable test lesson from this round: `TerminalViewStatus.scroll_offset`
is documented as the retained rows between **this viewport** and the
live tail, so it necessarily tracks the viewport height. Asserting it
constant across a panel height change is either vacuous or wrong —
the invariant Q#BP7 actually states is that the **anchor** is frozen,
which the acceptance now pins by comparing the first visible row's
text, plus `at_bottom` for the follow re-arm.
- `compile_mode_acceptance` needs `--test-threads=1` locally; it is
67/67 there. Under default parallelism it fails roughly 1 run in 3,
with a *different* test each time (acc14/acc25a, then acc24) —
**verified pre-existing** by swapping in `githubsucks/main`'s
`builtin/runtime/compile.lua` and reproducing the same rate. The
`pmacs-gpu` bin tests have historically gone red under a loaded sweep
(wgpu device contention). Rerun isolated before treating either as a
regression.
- Stage 2 (the GPU panel band, next available protocol version) has its
own re-framing obligation before implementation; Stage 3 is the default
placement flip.
## Folding lane (Arc 6) — Stages 1 and 2 MERGED; Stage 3 (GPU) is next ## Folding lane (Arc 6) — Stages 1 and 2 MERGED; Stage 3 (GPU) is next
@ -721,6 +599,34 @@ git worktree add --track \
## Closed since the last snapshot ## Closed since the last snapshot
- **Bottom panel Stage 1 — MERGED as #155** (`main` @ `e745068`,
2026-07-24, after two review rounds). Window placement, window
parameters, TUI side windows, the divider, and the adopter `display`
opt-in, with no protocol change. Both rounds found the same class of
defect and are worth keeping:
- **Round 1**: the Q#BP6 side-window split guard had *no production
caller* — `C-x 2` still reached plain `split_active` — and survived
because the acceptance test called the core method directly.
- **Round 2**: Q#BP7's terminal growth re-arm had *never been
implemented*, and the assertion meant to pin it (`at_bottom`) is a
geometric readout that a still-anchored view satisfies; the anchor
assertions beside it compared `""` with `""` because the PTY fixture
emitted LF-only output.
- **Post-round-2 self-review**, caught by CI going red on all four Test
jobs: resolving `pmacs.window.buffer()`'s no-argument arm through the
acting frontend made a total function partial, and six runtime modules
silently dropped operations (`kill_ring_acceptance` 30/30 → 25/5).
Fixed in `9110f9f` before merge.
- Gating fact found on the way: **the workspace sweep must run with an
isolated `XDG_CONFIG_HOME`**, because the real user `init.lua`
installs a local package and the losing race leaks a status message
into painted-frame comparisons. There is also a latent pre-existing
`main` bug in the buffer CRDT undo path, unrelated to this arc.
- `compile_mode_acceptance` is load-sensitive under default
parallelism (~1 run in 3, a different test each time); verified
pre-existing by swapping in `main`'s `compile.lua`. It is 67/67 at
`--test-threads=1`.
- **GPU initial target — MERGED as #148** (`main` @ `0dd16a5`, 2026-07-24, - **GPU initial target — MERGED as #148** (`main` @ `0dd16a5`, 2026-07-24,
after two review rounds). `pmacs --gpu [--socket …] FILE` opens a target after two review rounds). `pmacs --gpu [--socket …] FILE` opens a target
before the GPU window appears. Protocol bumped 19 → 20: a semantic-session before the GPU window appears. Protocol bumped 19 → 20: a semantic-session

View File

@ -75,14 +75,77 @@ commands, read `docs/active-work.md` immediately after this file.
`pmacs.path.canonicalize`. Its branch state, substrate facts, and `pmacs.path.canonicalize`. Its branch state, substrate facts, and
verification live in `docs/active-work.md`; this section absorbs them verification live in `docs/active-work.md`; this section absorbs them
when it merges. when it merges.
- **GPU initial target LANDED — #148** - Protocol **v20** (`SUPPORTED=[6..=20]`; v16 = `ThemeFacts`, v17 =
(`docs/gpu-initial-target-framing.md` rev 3; merge `0dd16a5`; two review
rounds). `pmacs --gpu [--socket NAME|PATH] FILE` transports exact Unix path
refresh #150, folding Stage 2 #149, ledger refresh #147, web grammars #146,
LaTeX Stage 1 #144 / inline-math framing #145, and folding Stage 1 #142),
protocol **v20** (`SUPPORTED=[6..=20]`; v16 = `ThemeFacts`, v17 =
`FontFacts`, v18 = `StatuslineSegments`, v19 = terminal frames/events, v20 = `FontFacts`, v18 = `StatuslineSegments`, v19 = terminal frames/events, v20 =
the GPU initial-target semantic bootstrap family). the GPU initial-target semantic bootstrap family).
- **Bottom panel Stage 1 (window placement + TUI side windows) LANDED —
#155** (`docs/bottom-panel-framing.md` rev 4; merge `e745068`; two review
rounds). **No protocol change (still v20).** Arc 7's substrate: pmacs now
has Emacs's `display-buffer` + window parameters, and a buffer can be
displayed in a fixed-height window pinned to the bottom of the frame that
feature code targets **by policy** instead of by stealing the selected
window.
- `src/window.rs`: `WindowParams { side, fixed_rows, dedicated }` plus
implementation-owned `quit_action` / `origin_document` (Lua reads them,
`set_params` refuses them); `MIN_WINDOW_OUTER_ROWS = 2`;
`Layout::compute(area, fixed)` subtracts fixed children before dividing
the remainder by weight, preserving last-flexible-takes-the-remainder, so
a tree with no fixed leaves computes byte-identically to before.
- **`Layout::compute` has TWO production callers**, and both must feed the
same shared `panel_fixed_rows` map: `window_placements` and
`src/overlay_paint.rs`'s peer-presence pass, which derives its own
text-area `Rect` and never routes through the first. Leaving the second
on unfixed geometry paints every peer cursor at the row it would occupy
with no panel open.
- **The minimum is recursive** (`subtree_min_rows`: horizontal splits sum,
vertical splits max). "Two rows at the root" does not give each nested
leaf two rows. `interactive_min_rows` is the same recursion over the
user's `window.min-height` preference, and applies to drag/keyboard
resize ONLY — the layout pass and frame-resize reconciliation use the
structural floor, so changing a preference can never invalidate an
existing layout.
- **Hiding a panel is a durable state transition, not a per-frame effect**
(`EditorState::reconcile_panel_layout`): it moves focus out and releases
the terminal controller, because the terminal resize path merely returns
on zero content without releasing. It runs after attach/resize/display/
close and defensively before input dispatch, terminal sync, and paint.
- `FrontendView` gains `panel_capable`, `frame_geometry`
(`None` = **unknown**, never the GPU attach request's permanent 24×80
placeholder), and derived `panel_hidden` — each spelled explicitly at
every construction site, preserving folding's non-`Default` discipline.
- `EditorCore::primary_document_window` is the Q#BP14 projection seam;
`display_buffer` is Phase 1 of the display transaction (exact target →
side affinity → ordinary reuse, with option-valued height/dedication);
the Lua layer owns Phase 2 (activate → hook → reconcile → revalidate →
final-focus matrix).
- **Optimistic input is gated per WINDOW, not per buffer**:
`dispatch_idle_for` returns `false` whenever the acting frontend's active
window is a side window. Marking the panel's BUFFER round-trip would be
wrong — `round_trip_buffers` is global by `BufferId`, so it would disable
optimistic apply for another frontend editing that buffer as its document.
- Jump entries are per frontend and carry their origin `WindowId`; a stale
**side** origin is SKIPPED, because degrading it to an active-window
switch is exactly the duplicate-panel corruption the arc removes.
- `pmacs.window.display / display_file / quit / panel / params /
set_params / resize / display_target`, plus `builtin/runtime/window.lua`
(`window.panel-height`, `window.min-height`, `C-x ^` / `C-x C-^`).
Adopters take `display = "current" | "panel"`; **Stage 1 default is
`"current"`** and Stage 3 flips it.
- The divider is the upper subtree's existing mode-line row — no row added
or consumed, `ui.divider` restyles every exposed segment of one boundary,
and drag state is `HashMap<FrontendId, _>` so frontends cannot steal each
other's gestures.
- `open_initial_target` now shares one `resolve_target_buffer` +
exact-window install seam with `display_file`, and reasserts into a
document window after hooks (a startup hook can now create a panel).
- Final gates: 1,817 default + 1,994 CRDT library tests; the new
`bottom_panel_stage1_acceptance` 46; kill ring 30; compile 67; M4 121;
required GPU 152; initial-target 14 CRDT; all three vterm suites; folding
Stage 2 48. All 12 CI checks green at merge.
- **Stage 2 (the GPU panel band) needs its own re-framing** before
implementation and takes the next available protocol version; the
framing's §1.3 census of 23 transitive active-context reads is its map.
Stage 3 is the adopter default flip.
- **GPU initial target LANDED — #148** - **GPU initial target LANDED — #148**
(`docs/gpu-initial-target-framing.md` rev 3; merge `0dd16a5`; two review (`docs/gpu-initial-target-framing.md` rev 3; merge `0dd16a5`; two review
rounds). `pmacs --gpu [--socket NAME|PATH] FILE` transports exact Unix path rounds). `pmacs --gpu [--socket NAME|PATH] FILE` transports exact Unix path
@ -580,8 +643,14 @@ commands, read `docs/active-work.md` immediately after this file.
(GPU) is next**, unframed. (GPU) is next**, unframed.
- **Web grammars HTML+CSS LANDED — #146**, and **LaTeX Stage 1 — #144** - **Web grammars HTML+CSS LANDED — #146**, and **LaTeX Stage 1 — #144**
with its inline-math parent framing **#145**. with its inline-math parent framing **#145**.
- Remaining ranked arcs: 6 folding Stage 3, 7 DAP, 8 GPU splits, plus - **Arc 7 (bottom panel) Stage 1 LANDED — #155** — window placement,
the `.ipynb` arc (its JSON-grammar prerequisite shipped in #123). window parameters, TUI side windows, the divider, and the adopter
`display` opt-in. **Stage 2 (the GPU band) is next and needs its own
re-framing**; Stage 3 is the default flip. DAP was parked awaiting
exactly this arc's Stage 1 and can now re-baseline its touch census.
- Remaining ranked arcs: 6 folding Stage 3, 7 bottom-panel Stages 23,
DAP, 8 GPU splits, plus the `.ipynb` arc (its JSON-grammar
prerequisite shipped in #123).
## 2. How we work (the part that must not drift) ## 2. How we work (the part that must not drift)
@ -848,6 +917,44 @@ final variant — its own round-trip cannot detect a discriminant shift.
before any `FrontendEvent` touches render/size/editor state. Generalizes: before any `FrontendEvent` touches render/size/editor state. Generalizes:
when a new failure path can leave a handle installed without its owning when a new failure path can leave a handle installed without its owning
session, dropping a handle is not the same as tearing down the connection. session, dropping a handle is not the same as tearing down the connection.
- **A guard with no production caller passes every direct-call test.**
#155 round 1: `EditorCore::try_split_active` implemented the side-window
split refusal, but `pmacs.window.split_horizontal` / `split_vertical` — and
therefore `C-x 2` / `C-x 3` — still called plain `split_active`. The
acceptance test called the core method directly, so reverting the guard
entirely would have left every test green. Same shape as folding #142
round 2. Assert through the outermost user-reachable seam
(`try_exec(&s, "pmacs.window.split_horizontal()")`), then falsify by
revert. When the test shares a file with the code it pins, `scripts/bite`
cannot swap it — break the production line by hand and `git checkout --`.
- **A geometric readout is not a state predicate.**
`TerminalViewStatus::at_bottom` is defined as `scroll_offset == 0` — "the
viewport currently reaches the tail", not "this view follows the tail". A
still-anchored view satisfies it whenever it happens to be tall enough, so
asserting it could not detect that Q#BP7's growth re-arm had never been
implemented (#155 round 2): the next rows the child printed pushed the
anchored view back into history. Pinning *following* requires advancing the
world — feed more child output through a filesystem gate — and asserting the
view came along. Related: `scroll_offset` is viewport-relative, so
"unchanged across a height change" is vacuous or wrong; the invariant is
the frozen ANCHOR.
- **A PTY in the default mode does not translate LF to CRLF.** An
`echo`-driven test fixture staircases rightward, and past the viewport
width every row clips to blanks — so `assert_eq!(top_before, top_after)`
compares `"" == ""` and passes for any regression (#155 round 2). Emit
`printf '...\r\n'`, and guard text comparisons with
`assert!(!observed.is_empty())` the same way the panel daemon pin guards on
`!panel_hidden`.
- **Widening an ambient resolver into a scoped one can make a total function
partial.** #155 round 2 resolved both arms of `pmacs.window.buffer()`
through the acting frontend "for uniformity". `acting_frontend` follows the
interactive origin, which can name a frontend with no registered view (a
bare `dispatch_key` from an unattached peer), so the no-argument arm began
raising instead of answering. No runtime caller `pcall`s it, so killring,
syntax, autosave, pair, indent and comment silently dropped operations —
`kill_ring_acceptance` went 30/30 to 25/5 on every CI platform. The ambient
resolver's fallback is what makes it *total*; keep it, and document that as
deliberate. Uniformity is not free when the paths have different totality.
- **An upgrade decision must be tracked independently of the outcome that - **An upgrade decision must be tracked independently of the outcome that
triggered it.** #148 published a target's fresh `BufferSnapshot` to triggered it.** #148 published a target's fresh `BufferSnapshot` to
existing grid replicas only when the buffer was `newly_loaded || existing grid replicas only when the buffer was `newly_loaded ||