diff --git a/Cargo.lock b/Cargo.lock index be491fc..489a87d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2595,6 +2595,7 @@ dependencies = [ "postcard", "serde", "thiserror 2.0.18", + "unicode-width", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 166751e..8bfb286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,10 @@ members = [".", "pmacs-protocol", "pmacs-gpu"] serde = { version = "1", features = ["derive"] } postcard = { version = "1", features = ["use-std"] } thiserror = "2" +# Shared between `pmacs` (terminal screen) and `pmacs-protocol` +# (`TerminalFrame::validate`). Pinned here so the producer and the +# validator agree on every glyph's column width. +unicode-width = "0.2" [package] name = "pmacs" @@ -85,7 +89,7 @@ crdt = ["dep:loro", "pmacs-protocol/crdt"] [dependencies] crossterm = "0.28" thiserror = { workspace = true } -unicode-width = "0.2" +unicode-width = { workspace = true } unicode-segmentation = "1" # Regex engine for in-buffer regex search (Q#RX1). `regex::bytes::Regex` # matches over rope-snapshot bytes and yields byte offsets directly. diff --git a/docs/active-work.md b/docs/active-work.md index 7b8c675..154da9d 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -14,8 +14,8 @@ backlog. machine-local: `origin` may name this canonical URL, a release mirror, or something else, and therefore has no authority by name alone. - Canonical base at this snapshot: - `githubsucks/main` @ `40111dc` (landed-state docs after locals-query #134; - protocol v18). + `githubsucks/main` @ `2625ec7` (tab-width parity #137 merged atop + locals-query #134 and modeline detection #132; protocol v18 on `main`). - On the transfer source, `origin/main` named a release mirror at `d3fa632` and lagged badly. On the current destination, `origin` names the canonical URL. This difference is why all recovery begins by @@ -49,35 +49,120 @@ git worktree list git status --short --branch ``` -The first command must expose `40111dc` or a newer intentional main. +The first command must expose `2625ec7` or a newer intentional main. If it does not, stop and repair the remote/fetch configuration. -## Tab-width rendering parity lane +## Vterm Stage 3 implementation lane -- Portable branch: `githubsucks/tab-width-parity`. -- Base: canonical `main` @ `40111dc`; protocol v18. -- Approved framing: `docs/tab-width-parity-framing.md` revision 2; framing - branch head `9f2f0d5`. -- Implementation head: `9f7bc77`. -- State: implementation complete; PR #137 open: - . One fixed 8-column constant now - drives core/TUI columns, GPU code projection, and minimap width. Source bytes - and protocol ranges remain unchanged. -- Verification: `cargo fmt --check`; strict workspace Clippy; 1,763 default, - 1,939 CRDT, and 1,763 Lua 5.4 library tests; 2 tab-width acceptance tests; - M4 121 passed (3 ignored, 1 filtered); required GPU 119; workspace 2,911 - passed across 83 suites (19 ignored, 1 filtered); `git diff --check`. -- Concurrent PR #135 owns overlapping `Cargo.lock`, `pmacs-protocol/src/lib.rs`, - and `pmacs-gpu/src/main.rs`. This branch deliberately remains based on - canonical `main`; rebase and rerun gates if #135 lands first. -- Recovery: +- Portable branch: `githubsucks/vterm-gpu` +- Framing carried as the first commit; implementation follows it. +- Base: cut from canonical `main` @ `1dd47fc`, NOT stacked on + `vterm-stage3-framing`, per the framing's §8. Canonical `main` @ + `2625ec7` (tab-width parity #137, locals-query #134, modeline handoff + #133/#136) is MERGED IN — see the integration entry below. +- PR: #135, , open against + canonical `main`. Never merge without explicit authorization. +- State: criteria 28-37 implemented. Protocol v19 (`SUPPORTED=[6..=19]`): + `InstanceMessage::TerminalFrame` (discriminant 26, daemon-gated), + `FrontendEvent::TerminalResize` (11) and `TerminalPointer` (12) + (frontend-gated). `pmacs-protocol/src/terminal.rs` owns the shared + bounds and `TerminalFrame::validate`; `pmacs-gpu/src/terminal.rs` is the + pure cell-space paint planner; `pmacs-gpu --headless-probe` drives the + real attach client without winit for criterion 37. +- Verification (clean tree, this machine): + - `cargo fmt --check`; + - `cargo clippy --workspace --all-targets -- -D warnings`; + - `cargo test --lib`: 1,757 passed (3 ignored); + - `cargo test --lib --features crdt`: 1,933 passed (3 ignored); + - vterm Stage 1 acceptance: 9 default / 10 CRDT; + - vterm Stage 2 acceptance: 4 default / 4 CRDT; + - vterm Stage 3 acceptance: 4 default / 5 CRDT (the CRDT-only case is + the real-daemon + real-PTY + headless-GPU path); + - statusline acceptance: 7 default / 8 CRDT; + - `cargo test --test m4_acceptance -- --skip basedpyright`: 120 passed + (3 ignored, 1 filtered); + - `PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`: 127 passed; + - `cargo test --workspace -- --skip basedpyright`: 2,919 passed across + 83 suites (19 ignored), one invocation; + - `git diff --check`. +- Review round 1 addressed (framing §0.10): hover no longer claims durable + terminal control (real defect, bite-verified); terminal motion dedupes by + cell; declarations record only once sent; unchanged frames skip + revalidation; the terminal-mode presence-sweep skip is removed. The + review's predicted presence FREEZE did not reproduce — the buffer-follow + clears the declaration before `render_frame`, so a truthful sweep always + precedes terminal mode; that test is a labelled regression guard, not + fix evidence. +- Review round 2 addressed (framing §0.11): a daemon disconnect now leaves + terminal mode so the notice is visible (real defect, hand-verified + because fix and test share a file); the per-tick full-grid clone is + gone; inbound terminal events require a negotiated v19 session; a + grid-missing press no longer arms a drag; roadmap/handoff Arc 5 lines + corrected. Named deferral: terminal wheel gestures discard scroll + magnitude. +- Post-round-2 gates (pre-integration): 1,758 default + 1,934 CRDT library + tests; required GPU 129; workspace sweep 2,923 across 83 suites; Stage 3 + acceptance 5 default / 7 CRDT; M4 120; fmt, clippy, diff check clean. +- **Post-integration gates (canonical `main` @ `2625ec7` merged in):** + `cargo fmt --check`; strict workspace Clippy; `pmacs-protocol` 17; + `cargo test --lib` 1,768; `--features crdt` 1,944 (3 ignored each); + vterm Stage 1 9/10, Stage 2 4/4, Stage 3 5/7, statusline 7/8, tab-width + 2/2 (default/CRDT); M4 121 passed (3 ignored, 1 filtered); required GPU + 139; workspace sweep 2,946 passed across 84 suites (19 ignored), one + invocation; `git diff --check` clean. +- Closed caveat: the once-seen required-GPU failure did not reproduce in + eight author runs plus five reviewer runs. Treated as environmental. +- Canonical-main integration after #137 landed: the agreed order was + #137 first (it was approved and FROZEN at `5b23e11`, so it could not + absorb a rebase without breaking its freeze), then this lane second. + Integrated by MERGING canonical main into the branch, matching repo + precedent (`Merge canonical main into vterm-tui`, `… into modeline + detection`) rather than a rebase, which would have force-pushed away + the review anchors on #135. Main had also moved past this lane's base + by #133/#134/#136, so the integration surface was wider than the + #135/#137 overlap: `src/semantic_render.rs` was a fourth overlapping + code file and auto-merged, as did `pmacs-protocol/src/lib.rs`. The one + code conflict was the `pmacs_protocol` import list in + `pmacs-gpu/src/main.rs` (`TAB_STOP_COLUMNS` against the terminal + types) — resolved as a union. +- Next: further user review rounds on the PR. - ```sh - git worktree add --track \ - -b tab-width-parity \ - ../pmacs-tab-width-parity \ - githubsucks/tab-width-parity - ``` +Recovery worktree: + +```sh +git worktree add --track \ + -b vterm-gpu \ + ../pmacs-vterm-gpu \ + githubsucks/vterm-gpu +``` + + +## Cross-PR coordination: #135 and #137 (resolved) + +**Resolved 2026-07-22: #137 merged first, #135 integrated second.** Kept +as the worked example, because the deciding argument is reusable. + +#137 was APPROVED and FROZEN at `5b23e11`. "Frozen" and "rebase onto the +resulting main" are mutually exclusive, so the frozen PR had to land +first — the alternative would have broken its freeze and voided its +approval. Three arguments pointed the same way: the approved PR should +not wait on an unapproved one; the PR carrying the protocol byte pins +should be the one that integrates, because its own suite is what detects +a disturbed discriminant; and the larger, more invasive change should +pay the integration cost, since its author has the context to verify the +merged result. + +The named overlap (`pmacs-gpu/src/main.rs`, `pmacs-protocol/src/lib.rs`, +`Cargo.lock`, both ledger docs) was accurate but incomplete — main had +also moved by #133/#134/#136, making `src/semantic_render.rs` a fourth +overlapping code file. **Lesson: derive the integration surface from +`git diff ..main`, not from the other PR's file list.** + +The feared semantic collision did not occur. Terminal cell geometry +still uses the monospace advance and is not routed through +`TAB_STOP_COLUMNS`: terminal columns come from the child, and tab +expansion is a DOCUMENT projection concern. That is verified rather than +assumed — see the integration gates on the lane above. ## Parked lane: kill-ring browser + persistence @@ -115,6 +200,27 @@ git worktree add --track \ ## Closed since the last snapshot +- **Branches deleted 2026-07-22 (authorized):** `vterm-stage3-framing` + (Revision 8 framing; its content is carried on `vterm-gpu`, verified as a + superset before deletion — the branch was NOT an ancestor of `vterm-gpu` + because the framing was copied rather than merged, so it needed a forced + local delete) and `tab-width-parity` (a clean ancestor of `main` via + #137). Both removed as worktree + local ref + `githubsucks` ref; the + `origin` tracking refs were pruned. The `-framing` branches for each are + deliberately kept. + +- **Tab-width rendering parity — MERGED as #137** (`main` @ `2625ec7`, + 2026-07-22). One fixed 8-column `TAB_STOP_COLUMNS` in `pmacs-protocol` + now drives core/TUI columns, GPU code projection, and minimap width; + source bytes and protocol ranges are unchanged. Its lane, worktree, and + `tab-width-parity` branch (local + `githubsucks`) are deleted; the + `tab-width-parity-framing` branch is kept. This closes the long-standing + "tab width is a rendering-parity + bug, NOT a config gap" deferral recorded in `docs/agent-handoff.md` §5. +- **Locals-query processing — MERGED as #134** (with handoff #136), and + **modeline detection handoff #133**. Both landed between this lane's + base and its canonical-main integration. + - **Config registry — MERGED as #127** (`main` @ `2e37c04`). Its lane (`config-registry`, worktree `../pmacs-config-registry`) is done; the branch is kept but carries nothing unmerged. Durable substrate facts diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index c442722..bf384ce 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -1,8 +1,12 @@ # Agent handoff — cross-machine continuity -**Last updated: 2026-07-22, with tab-width rendering parity implemented and -open as PR #137, after locals-query processing (#134) landed on `main`. -Vterm Stage 3 remains in review as #135.** +**Last updated: 2026-07-22, after tab-width rendering parity (#137) and +locals-query processing (#134) landed on `main`, and canonical `main` was +merged into the Vterm Stage 3 lane. Vterm Stage 3 (protocol v19, GPU +terminal) is implemented on `vterm-gpu` and in review as PR #135; see +`docs/active-work.md`. Vterm Stage 2 (#130), modeline detection (#132), +mode system wiring (#129), config registry (#127), Vterm Stage 1 (#126), +and completed Themes Arc 4 (#120/#124/#125) are on `main`.** This file is the bridge between development machines. If you are an agent reading this on a fresh clone: this document plus the `docs/*-framing.md` @@ -16,8 +20,9 @@ commands, read `docs/active-work.md` immediately after this file. ## 1. Where the project stands (2026-07-22) -- `main` @ `40111dc` (landed-state documentation after locals-query processing - #134), protocol **v18** (`SUPPORTED=[6..18]`; v16 = `ThemeFacts`, v17 = +- `main` @ `2625ec7` (tab-width parity #137 atop locals-query #134), + protocol **v18** on `main`, **v19** on `vterm-gpu` + (`SUPPORTED=[6..=19]`; v16 = `ThemeFacts`, v17 = `FontFacts`, v18 = `StatuslineSegments`). - **Config registry LANDED — #127** (`docs/config-registry-framing.md` rev 3; merge `2e37c04`; two review rounds). `pmacs.config` is the @@ -263,14 +268,43 @@ commands, read `docs/active-work.md` immediately after this file. is a clean behavioral bite. The parser dispatch has its independent clean behavioral bite; the original `main`/crate-root bite remains explicitly weaker compile-time API evidence. - - Stage 3 owns `pmacs-gpu/src/attach.rs`, authenticated source routing, - protocol-owned wire types/limits, and a deliberate complete-frame limit - decision: 16 MiB is insufficient; use a measured legal-worst cap or - aggregate bound, never silent chunking. + - **Stage 3 GPU/protocol is IMPLEMENTED on `vterm-gpu`** (framing + `docs/vterm-framing.md` Revision 9, criteria 28-37; awaiting review). + Protocol **v19**: `InstanceMessage::TerminalFrame` (discriminant 26, + daemon-gated) plus `FrontendEvent::TerminalResize` (11) and + `TerminalPointer` (12), both frontend-gated - the first bump gating in + BOTH directions. `SUPPORTED=[6..=19]`. + - `pmacs-protocol/src/terminal.rs` now owns the shared terminal bounds, + `TerminalProcessState`, `TerminalSelectionSpan`, and the single + structural policy `TerminalFrame::validate`; `src/terminal/*` + re-exports them so no duplicate type exists. `unicode-width` is a + workspace dependency so the screen and the validator measure glyph + columns identically. `MAX_TERMINAL_FRAME_GLYPH_BYTES = 8 MiB` bounds + the payload instead of widening the transport cap; the measured + maximum legal frame encodes to 13,437,863 bytes under the unchanged + 16 MiB `MAX_FRAME_BYTES`. Over-bound snapshots are rejected, never + truncated or silently chunked. + - **The `Viewport` gate keys on the AUTHENTICATED SOURCE'S ACTIVE + BUFFER, not the buffer the message names.** `Viewport` also aligns the + window to the buffer it declares, so a stale document viewport in + flight when a command opens a terminal drags the frontend back off it + - the terminal then never paints, with no error anywhere. The weaker + "is the declared buffer a terminal" reading looks right and fails + exactly this way. + - Suppression compares the COMPLETE ordered payload, never + `screen_generation`: scroll, selection, viewport, and process state all + change without advancing it. + - GPU: `pmacs-gpu/src/terminal.rs` is a pure cell-space paint planner + (testable without a GPU); the renderer builds one shaped buffer per + text run so a wide/cluster advance can never choose the next column's + origin. `pmacs-gpu --headless-probe` drives the real attach client + without winit (`attach::connect_with_sink`), which is how criterion 37 + gets one real daemon + real PTY + real wgpu path. - **Stage 2 TUI LANDED ON `main` — #130** (merge `86fc1bc`; `docs/vterm-framing.md` Revision 7, criteria 15–27). `TerminalViewKey` keys per-frontend/window projection state over one shared process/screen; logical row anchors retain + scroll/selection through reflow. One authenticated frontend controls at most one session, with atomic replacement and release on focus/switch/kill/detach. @@ -305,19 +339,19 @@ commands, read `docs/active-work.md` immediately after this file. removes owned cell snapshots from terminal mouse routing. The framing now records the transient v18 semantic-controller boundary and bracketed-paste injection deferral. - - Current-main integration (`3f0252f`) preserves per-frontend terminal - dispatch while applying the landed mode-scoped keymap, and exposes the - `mode`, `terminal`, and `lsp` statusline providers together. - - Post-integration gate: `cargo fmt --check`; strict workspace Clippy; + - Current-main integration (`3f0252f`) preserved per-frontend terminal + dispatch while applying the landed mode-scoped keymap, and exposed the + `mode`, `terminal`, and `lsp` statusline providers together. PR #130 merged + at `86fc1bc`. + - Final integrated gate: `cargo fmt --check`; strict workspace Clippy; 1,753 default + 1,929 CRDT library tests (3 ignored each); mode-system acceptance 1 default + 1 CRDT; Stage 1 acceptance 9 default + 10 CRDT; Stage 2 acceptance 4 default + 4 CRDT; statusline acceptance 7 default + 8 CRDT; M4 114 passed (3 ignored, 1 filtered); required GPU 109; workspace 2,882 passed across 82 suites (19 ignored, 1 filtered); `git diff --check` clean. -- **Tab-width rendering parity IMPLEMENTED — PR #137 OPEN** - (`docs/tab-width-parity-framing.md` rev 2; branch `tab-width-parity`; - implementation `9f7bc77`; ). +- **Tab-width rendering parity LANDED — #137** (merge `2625ec7`; + `docs/tab-width-parity-framing.md` rev 2). Source tabs remain one byte while every buffer renderer follows the shared fixed `pmacs_protocol::TAB_STOP_COLUMNS = 8`. - `src/display_width.rs` owns allocation-free Unicode/tab-aware byte-to-column @@ -333,6 +367,11 @@ commands, read `docs/active-work.md` immediately after this file. version changed. Local gates: 1,763 default + 1,939 CRDT + 1,763 Lua 5.4 library tests; 2 focused acceptance; M4 121; required GPU 119; workspace 2,911 across 83 suites; strict Clippy and diff check clean. + - This closes the standing "**tab width is a rendering-parity bug, NOT a + config gap**" deferral in §5: one shared constant now drives every + renderer. Terminal cells are deliberately OUTSIDE it — a terminal's + columns come from the child, so `pmacs-gpu`'s terminal geometry uses + the monospace advance and never `TAB_STOP_COLUMNS`. - **PARKED: kill-ring browser + persistence.** Revision 2 framing is preserved on branch `kill-ring-browser`, but its `0efb5cd` scout is stale and must be repeated before implementation. No PR or implementation is @@ -349,8 +388,9 @@ commands, read `docs/active-work.md` immediately after this file. fix (#101). - **Arc 4 (themes + extensibility) COMPLETE** — named UI faces (#120), live GPU font preferences (#124), statusline providers (#125). - - **Arc 5 terminal stage ACTIVE** — compile mode (#113), Vterm terminal core - (#126), and Vterm TUI (#130) landed; protocol/GPU Stage 3 is next. + - **Arc 5 terminal stage ACTIVE** — compile mode (#113), Vterm terminal + core (#126), and Vterm TUI (#130) landed; protocol/GPU Stage 3 is + implemented and in review as PR #135. - **Config registry COMPLETE (#127)** — not a numbered arc; it was the cross-cutting substrate ranked first on `docs/side-quest-backlog.md`'s north star, and it unblocks the @@ -472,10 +512,12 @@ buffer owns a path's recovery slot; only recover/discard release unclaimed crash data; adopt clears the old owner's skip cache. **Protocol** — encoding-breaking bumps are deliberate and versioned -(`SUPPORTED=[6..18]`). v15 = `CompletionPopup` + +(`SUPPORTED=[6..=19]`). v15 = `CompletionPopup` + `StatusFacts.message`; v16 = `ThemeFacts`; v17 = `FontFacts`; v18 = -`StatuslineSegments`. New wire surface ⇒ bump + both-frontends support + -acceptance. +`StatuslineSegments`; v19 = the vterm terminal family. New wire surface ⇒ +bump + both-frontends support + acceptance. An APPENDED variant must be +guarded by a byte pin on the PREVIOUS final variant — its own round-trip +cannot detect a discriminant shift. **Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`, `rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation), @@ -536,6 +578,31 @@ acceptance. and go through `pmacs.command.invoke("buffer.save")`. Caught only because the *other* case failed and the cause was chased instead of the assertion adjusted. +- **A message that ALIGNS state cannot be gated on the state it names.** + `FrontendEvent::Viewport` both declares a byte range and switches the + frontend's window to the buffer it names. Gating the vterm v19 dual + declaration on "is the DECLARED buffer a terminal" therefore left a + stale in-flight document viewport free to drag a frontend straight back + off a terminal a command had just opened — the window oscillated, the + terminal declaration was refused every time, and no frame ever arrived. + Nothing errored. The gate has to key on the authenticated source's + ACTIVE buffer. Generalizes: when two messages declare competing views of + "what am I showing", the arbiter is the daemon's own state, never the + claim inside either message. +- **A pass that sets a mode flag must clear it on EVERY exit.** The + semantic producer's terminal pass returned early via `?` when no + declaration existed, leaving `terminal_active` set — and the daemon uses + that flag to suppress `CursorByte` and the presence sweep, so a frontend + that went back to a document silently lost both. Caught by an acceptance + assertion, not by any type. +- **Sub-crate acceptance needs a real seam, not a fixture.** `pmacs-gpu` + depends only on `pmacs-protocol`, so "real daemon + real PTY + real + wgpu in one path" could not be an in-crate test. Generalizing + `attach::connect`'s reader sink (`connect_with_sink`) and adding + `--headless-probe` gave the acceptance the REAL handshake, outbox, + writer, and `render_to_view` — which is the whole point; a + decoded-message fixture would have proved none of the three fit + together. The probe found two real defects the in-process tests did not. - **Real-grid acceptance must budget for macOS startup and path width.** A 100 ms first-Hello timeout failed under loaded macOS CI; use the normal five-second handshake window, then short polling reads. An 80-column split @@ -587,8 +654,12 @@ currently accepted for `autosave.interval-ms`, where a per-buffer value is meaningless. **Tab width is NOT a config gap** — see §5. Mode system (SHIPPED #129): minor modes, `buffer.after-mode-change`, -mode-scoped settings, `describe-mode`, and persistence of explicit -major-mode overrides/clears across sessions. Modeline detection shipped in #132. +mode-scoped settings, `describe-mode`, and persistence of explicit major-mode +overrides/clears across sessions. +Modeline detection (SHIPPED #132): bounded first/last-line Emacs `-*-` +and Vim `ft=`/`filetype=` parsing, explicit-over-inferred precedence, +alias normalization, and shared fresh-load language pinning for +syntax/highlight/LSP startup. Highlight/detection (from the #114–#118 side-quest + injections #122): ~~locals-query processing~~ **SHIPPED #134**; remaining injection follow-ups now that the engine landed (#122) — @@ -598,6 +669,7 @@ comment schemes), child-tree incrementality + range-scoped layer rebuild runtime/Lua-registered languages (v1 resolves only against `BUILTIN_LANGUAGES`), and the next injection *consumers* gated on new grammars — HTML/CSS/GraphQL/SQL (`