Merge pull request #128 from levineuwirth/handoff-2026-07-20

docs: refresh cross-machine continuity
This commit is contained in:
Levi Neuwirth 2026-07-21 23:25:44 +00:00 committed by GitHub
commit f1a2f755a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 790 additions and 126 deletions

35
AGENTS.md Normal file
View File

@ -0,0 +1,35 @@
# pmacs agent instructions
**Start here: read `docs/agent-handoff.md`, then
`docs/active-work.md`, before taking on any work.** The handoff carries
durable project state, working method, substrate invariants, and the
standing backlog. The active-work ledger carries volatile branches,
checkpoints, verification, and exact cross-machine recovery commands.
Keep both updated according to their own update protocols.
Always true, independent of the handoff:
- Rust core + Lua runtime (`builtin/runtime/*.lua`), TUI + GPU
(`pmacs-gpu`) frontends over a versioned semantic protocol
(`pmacs-protocol`). `#![forbid(unsafe_code)]`.
- Workflow: framing doc in `docs/` -> user approval -> branch -> implement
-> full gate suite -> PR -> user review rounds -> user says when to
merge. Never merge unprompted. One feature, one branch, one PR.
- Gates before any PR: `cargo fmt --check`; `cargo clippy --workspace
--all-targets -- -D warnings` (as its own step); `cargo test --lib`;
`cargo test --lib --features crdt`; the touched acceptance suites;
`cargo test --test m4_acceptance -- --skip basedpyright`;
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`; `git diff --check`.
- The checkout may be shared with the user: check `git status` for
foreign uncommitted work before stash, checkout, or branch operations,
and never delete untracked files you did not create.
- The canonical development URL is
`https://github.com/levineuwirth/pmacs.git`; recovery docs normalize
it to the local alias `githubsucks`. Remote names such as `origin` are
machine-local and carry no authority by themselves. Bootstrap/verify
the alias via `docs/active-work.md` before basing new work.
- Work is portable only after it is committed and pushed. Uncommitted
worktree changes, untracked files, and `/tmp` dependencies do not
travel to another machine.
- Write commit messages with `git commit -F <file>`. Never use
`git add .`.

View File

@ -1,18 +1,19 @@
# pmacs agent instructions # pmacs agent instructions
**Start here: read `docs/agent-handoff.md` before taking on any work.** **Start here: read `docs/agent-handoff.md`, then
It carries current project state, the working method, substrate `docs/active-work.md`, before taking on any work.** The handoff carries
invariants, and the standing backlog — it is the continuity bridge durable project state, working method, substrate invariants, and the
between development machines. Keep it updated as part of your work standing backlog. The active-work ledger carries volatile branches,
(update protocol is in the file itself). checkpoints, verification, and exact cross-machine recovery commands.
Keep both updated according to their own update protocols.
Always true, independent of the handoff: Always true, independent of the handoff:
- Rust core + Lua runtime (`builtin/runtime/*.lua`), TUI + GPU - Rust core + Lua runtime (`builtin/runtime/*.lua`), TUI + GPU
(`pmacs-gpu`) frontends over a versioned semantic protocol (`pmacs-gpu`) frontends over a versioned semantic protocol
(`pmacs-protocol`). `#![forbid(unsafe_code)]`. (`pmacs-protocol`). `#![forbid(unsafe_code)]`.
- Workflow: framing doc in `docs/` → user approval → branch → implement - Workflow: framing doc in `docs/` -> user approval -> branch -> implement
→ full gate suite → PR → user's review rounds → user says when to -> full gate suite -> PR -> user review rounds -> user says when to
merge. Never merge unprompted. One feature, one branch, one PR. merge. Never merge unprompted. One feature, one branch, one PR.
- Gates before any PR: `cargo fmt --check`; `cargo clippy --workspace - Gates before any PR: `cargo fmt --check`; `cargo clippy --workspace
--all-targets -- -D warnings` (as its own step); `cargo test --lib`; --all-targets -- -D warnings` (as its own step); `cargo test --lib`;
@ -20,7 +21,15 @@ Always true, independent of the handoff:
`cargo test --test m4_acceptance -- --skip basedpyright`; `cargo test --test m4_acceptance -- --skip basedpyright`;
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`; `git diff --check`. `PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`; `git diff --check`.
- The checkout may be shared with the user: check `git status` for - The checkout may be shared with the user: check `git status` for
foreign uncommitted work before stash/checkout/branch operations, and foreign uncommitted work before stash, checkout, or branch operations,
never delete untracked files you didn't create. and never delete untracked files you did not create.
- Commit messages via `git commit -F <file>`, ending with the Claude - The canonical development URL is
co-author line; PR bodies end with the Claude Code attribution. `https://github.com/levineuwirth/pmacs.git`; recovery docs normalize
it to the local alias `githubsucks`. Remote names such as `origin` are
machine-local and carry no authority by themselves. Bootstrap/verify
the alias via `docs/active-work.md` before basing new work.
- Work is portable only after it is committed and pushed. Uncommitted
worktree changes, untracked files, and `/tmp` dependencies do not
travel to another machine.
- Write commit messages with `git commit -F <file>`. Never use
`git add .`.

137
docs/active-work.md Normal file
View File

@ -0,0 +1,137 @@
# Active work — cross-machine resume ledger
**Snapshot: 2026-07-21.** This file records volatile work that has not
landed on `main`. Read it after `docs/agent-handoff.md`. Remove completed
entries when their PR merges; do not let this become a second permanent
backlog.
## Repository authority
- Canonical development URL:
`https://github.com/levineuwirth/pmacs.git`. This ledger uses the
normalized local alias `githubsucks` so its refs and recovery commands
are identical on every machine. Remote names are otherwise
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` @ `2e37c04` (#127 merged; protocol v18).
- 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
verifying URLs and normalizing `githubsucks` rather than trusting
`origin/main`.
- The shared desktop checkout contained unrelated uncommitted work. The
branches below were prepared in isolated worktrees; never clean or
overwrite the shared checkout to recover them.
Start on another machine by inspecting its remotes:
```sh
git remote -v
git remote get-url githubsucks
```
If the second command says the alias is absent, add it; if it prints a
different URL, stop and resolve that collision rather than overwriting an
unknown remote:
```sh
git remote add githubsucks https://github.com/levineuwirth/pmacs.git
```
Then recover current refs:
```sh
git fetch githubsucks --prune
git log -1 --oneline githubsucks/main
git worktree list
git status --short --branch
```
The first command must expose `2e37c04` or a newer intentional main.
If it does not, stop and repair the remote/fetch configuration.
## Vterm Stage 2 framing lane
- Portable branch: `githubsucks/vterm-framing`
- Approved framing head: `fb4f8f0`
- Base: canonical `main` @ `643d1e1` (Vterm Stage 1 / PR #126 merged).
`main` has since advanced to `2e37c04` (config registry #127, no
runtime overlap with vterm); cut the Stage 2 lane from current `main`,
not from `643d1e1`.
- State: `docs/vterm-framing.md` Revision 7 is framing-only, reviewed, and
approved for implementation. It closes the final `at_bottom`, terminal
`C-c` binding-reachability, and context-implicit Lua failure-mode findings.
There is no Stage 2 runtime implementation or PR yet.
- Next lane: create `pmacs-vterm-tui` / `vterm-tui` from current canonical
`main`, carry the approved framing as its first commit, then implement and
gate Stage 2. Do not implement on `vterm-framing`.
Recovery worktree on a machine that does not already own the branch:
```sh
git worktree add --track \
-b vterm-framing \
../pmacs-vterm-framing \
githubsucks/vterm-framing
```
## Parked lane: kill-ring browser + persistence
- Portable branch: `githubsucks/kill-ring-browser`
- Parked framing head: `503c489`
- State: framing only, revision 2; no implementation and no PR.
- Status: explicitly parked by the user on 2026-07-20.
- Its original scout was based on `0efb5cd`. The preserved framing marks
this ground truth stale and requires a complete re-scout against the
then-current `githubsucks/main` before implementation.
- Compile-mode has merged since the original scout, so old
“compile-mode in flight” keybinding/touch-set assumptions are not
authoritative.
Recovery worktree, only when the user un-parks it:
```sh
git worktree add --track \
-b kill-ring-browser \
../pmacs-kill-ring-browser \
githubsucks/kill-ring-browser
```
## Documentation lane
- Portable branch: `githubsucks/handoff-2026-07-20`
- Carries synchronized `AGENTS.md` / `CLAUDE.md`, this ledger, the
durable handoff refresh, and the keybinding reference correction.
- It changes no runtime code.
- Review and merge this documentation branch separately; it must not be
folded into a feature framing branch.
- Now also absorbs both landed arcs: Vterm Stage 1 (#126) and the config
registry (#127). Canonical `main` is merged into it up to `2e37c04`,
so its diff against `main` is documentation only.
## Closed since the last snapshot
- **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
moved to `docs/agent-handoff.md` §1 per rule 3 below.
- Both this and Vterm Stage 1 ran as **concurrent lanes in sibling
worktrees off `main`**, with the shared files (`src/editor.rs`,
`src/lua_bindings/mod.rs`, `src/lib.rs`) assigned to one lane each in
advance. The rebase of the second lane onto the first had **zero
conflicts** — worth repeating for future parallel work, along with its
precondition: agree the file split before either lane starts, and keep
each lane's footprint in the other's files to a single line.
## Update protocol
Whenever a listed lane changes materially:
1. update its public branch and head/state here;
2. record new verification and remove superseded caveats;
3. keep durable architecture in `docs/agent-handoff.md`, not here;
4. remove the lane after merge or abandonment;
5. verify every recovery command from a clean worktree before calling
the transfer complete.

View File

@ -1,18 +1,66 @@
# Agent handoff — cross-machine continuity # Agent handoff — cross-machine continuity
**Last updated: 2026-07-21, after Vterm Stage 1 review round 2 was addressed **Last updated: 2026-07-21, after the config registry (#127) and Vterm
and fully gated on `vterm-core` (awaiting merge authorization; not merged). Stage 1 terminal core (#126) both landed on `main`, atop completed
Vterm Stages 2 and 3 are not implemented.** This file is the bridge between development Themes Arc 4 (#120/#124/#125). Vterm Stages 2 and 3 are not
machines. If you are an agent reading on a fresh clone: this document implemented.**
plus the `docs/*-framing.md` files ARE your memory. Read this fully This file is the
before taking on work, seed persistent memory from it, and **update this bridge between development machines. If you are an agent reading
file (and commit it) whenever project state changes materially** — the this on a fresh clone: this document plus the `docs/*-framing.md`
next machine reads it the way you just did. files ARE your memory. Read this fully before taking on work, seed
your persistent memory from it, and **update this file (and commit
it) whenever project state changes materially** — the next machine
reads it the way you just did.
For volatile branches, checkpoints, verification, and recovery
commands, read `docs/active-work.md` immediately after this file.
## 1. Where the project stands (2026-07-21) ## 1. Where the project stands (2026-07-21)
- Canonical `main` @ `7bc0c61` (#125 merged), protocol - `main` @ `2e37c04` (config registry #127), protocol **v18**
**v18** (`SUPPORTED=[6..18]`). (`SUPPORTED=[6..18]`; 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
typed, introspectable options registry the backlog ranked first, and
it closes the "config-registry-blocked" deferrals below. It was built
as a PARALLEL LANE alongside vterm in a sibling worktree; the files
were assigned per-lane up front and the rebase had zero conflicts.
- **Third registry** beside `CommandRegistry`/`HookRegistry`
(`src/config_registry.rs`), same R42/R50/duplicate-rejection/
`SourceLocation` vocabulary; Lua surface in
`src/lua_bindings/config.rs`. **No protocol change (still v18) and
ZERO changes to `src/editor.rs`.**
- **An override is ALWAYS stored**, even when equal to the value it
shadows; only `value_epoch` and listener dispatch key on effective
change. The "equal-value set is a no-op" reading silently voids a
buffer-local pin: nothing is stored, and a later global `set` flips
the very buffer the user pinned.
- **Two scopes: global and buffer-local.** `get(name, buf)` resolves
local → global → default; **`get(name)` resolves the GLOBAL CHAIN
ONLY** and never consults an ambient buffer. Per-language and
per-project are *patterns* (a hook calling `set_local`), not scopes
the registry knows about. Mode scope is impossible until the mode
system is wired — every editor `KeymapStack::resolve` passes `&[]`.
- Buffer-locals live in a registry side table purged at
`after_buffer_removed`, beside the keymap purge.
- Listeners: commit → snapshot → **drop the borrow** → re-enter Lua;
a raising listener is logged without blocking the rest or rolling
back; a depth bound turns a cycle into a pointed error. **Explicit
dispose only** — there is no `MetaMethod::Gc` anywhere in the
codebase, and GC timing differs between the two Lua backends.
- `StartupOnly` freezes off the existing `InitCompleteFlag` at write
time (which is why no `editor.rs` call was needed). In `--lib`
builds `set_init_complete` never runs, so a post-freeze test must
flip the flag explicitly or it passes vacuously.
- Adopters own their own `define`, so `SourceLocation` names the
owning module: `editing.auto-pair` (pair.lua, read against the
typed edit's SOURCE buffer), `editing.trim-on-save` (editops.lua,
read against the buffer being saved), `autosave.interval-ms`
(autosave.lua, re-read per tick). **The migration wrappers keep
their legacy coercion** — the registry is strict, the legacy setters
stay lenient (`trim_on_save("yes")`, `interval_ms(1500.7)`).
- `M-x describe-setting` renders into `*help*`.
- **Syntax-highlight / language-detection side-quest (#114#118) - **Syntax-highlight / language-detection side-quest (#114#118)
LANDED** — a one-shot arc built in sibling worktrees off main while LANDED** — a one-shot arc built in sibling worktrees off main while
the user's themes lane (`theme-faces`) ran concurrently in the shared the user's themes lane (`theme-faces`) ran concurrently in the shared
@ -69,6 +117,16 @@ next machine reads it the way you just did.
`inline` node; matches tree-sitter-md's own splitter), and the wire `inline` node; matches tree-sitter-md's own splitter), and the wire
flattener runs over the WHOLE buffer via the file-style summary, so it flattener runs over the WHOLE buffer via the file-style summary, so it
must be an event sweep, not O(spans²). must be an event sweep, not O(spans²).
- **JSON + YAML grammars and language servers (#123) LANDED** — bundled
ABI-current `tree-sitter-json` / `tree-sitter-yaml` cover `.json`,
`.yaml`, and `.yml`; the existing injection engine now highlights YAML
frontmatter and JSON/YAML fences. Default external LSP configs are the
pinned `vscode-json-language-server` provider and
`yaml-language-server`; configured settings are pushed after
`initialized`, which also supports push-model servers. The fake-server
delivery proof and PATH-gated live JSON/YAML provider smokes cover the
configuration contract. `.jsonc` / `.json5` remain a deliberate
follow-up because the JSON grammar is strict.
- **Compile-mode (Arc 5 stage 1, #113) LANDED** (2026-07-14, 7 rounds; - **Compile-mode (Arc 5 stage 1, #113) LANDED** (2026-07-14, 7 rounds;
framing `docs/compile-mode-framing.md` rev 13). `compile.run` streams framing `docs/compile-mode-framing.md` rev 13). `compile.run` streams
`/bin/sh -c "exec 2>&1; <cmd>"` into an intercept-read-only `/bin/sh -c "exec 2>&1; <cmd>"` into an intercept-read-only
@ -120,13 +178,12 @@ next machine reads it the way you just did.
workspace sweep 2,718 passed across 78 suites (19 ignored, workspace sweep 2,718 passed across 78 suites (19 ignored,
`basedpyright` filtered); `git diff --check` clean. Stage 3 landed `basedpyright` filtered); `git diff --check` clean. Stage 3 landed
as #125 and completed Arc 4 on `main`. as #125 and completed Arc 4 on `main`.
- **Vterm Stage 1 terminal core IMPLEMENTED ON `vterm-core`, FULLY GATED, - **Vterm Stage 1 terminal core LANDED ON `main`#126**
AWAITING MERGE AUTHORIZATION, NOT MERGED** (`docs/vterm-framing.md` rev 5). (`docs/vterm-framing.md` rev 5; merge `643d1e1`).
- Implementation commits: `bbc1f33` (Stage 1), `962944b` (Darwin signal - Implementation commits: `bbc1f33` (Stage 1), `962944b` (Darwin signal
normalization), first-review fixes `f0a235f`, `28f2e6c`, `bf972a7`, and normalization), first-review fixes `f0a235f`, `28f2e6c`, `bf972a7`, and
second-review hardening `9797ada`; pull request: #126, second-review hardening `9797ada`; reviewed feature head `fc4e0ce` merged
<https://github.com/levineuwirth/pmacs/pull/126> (open, non-draft, through PR #126, <https://github.com/levineuwirth/pmacs/pull/126>.
targeting `main`).
- `AnsiParserProfile::{LineOriented, FullScreen}` preserves compile/REPL - `AnsiParserProfile::{LineOriented, FullScreen}` preserves compile/REPL
behavior while terminal PTYs emit the full cursor/mode/device operation behavior while terminal PTYs emit the full cursor/mode/device operation
set. `src/terminal/{screen,input,session}.rs` owns the state machine, set. `src/terminal/{screen,input,session}.rs` owns the state machine,
@ -174,6 +231,10 @@ next machine reads it the way you just did.
authenticated source routing, protocol-owned wire types/limits, and a authenticated source routing, protocol-owned wire types/limits, and a
deliberate complete-frame limit decision: 16 MiB is insufficient; use a deliberate complete-frame limit decision: 16 MiB is insufficient; use a
measured legal-worst cap or aggregate bound, never silent chunking. measured legal-worst cap or aggregate bound, never silent chunking.
- **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
active.
- Roadmap: `docs/roadmap-2026-07.md` (ranked arcs). Position: - Roadmap: `docs/roadmap-2026-07.md` (ranked arcs). Position:
- **Arc 1 (LSP utility surface) COMPLETE** — completion popup - **Arc 1 (LSP utility surface) COMPLETE** — completion popup
(#92/#93), panels/references/outline/hover (#94#96), plus (#92/#93), panels/references/outline/hover (#94#96), plus
@ -184,6 +245,16 @@ next machine reads it the way you just did.
- **Arc 3 (persistence) COMPLETE** — saveplace/recentf (#98), - **Arc 3 (persistence) COMPLETE** — saveplace/recentf (#98),
desktop-save (#99), autosave/crash-recovery (#100), save-clobber desktop-save (#99), autosave/crash-recovery (#100), save-clobber
fix (#101). 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) and Vterm terminal
core (#126) landed; Vterm TUI is the next formal stage.
- **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
editing/indent/comment items that were config-blocked.
- Remaining ranked arcs: 6 folding, 7 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)
@ -292,8 +363,10 @@ buffer owns a path's recovery slot; only recover/discard release
unclaimed crash data; adopt clears the old owner's skip cache. unclaimed crash data; adopt clears the old owner's skip cache.
**Protocol** — encoding-breaking bumps are deliberate and versioned **Protocol** — encoding-breaking bumps are deliberate and versioned
(`SUPPORTED=[6..15]`). v15 = `CompletionPopup` + `StatusFacts.message`. (`SUPPORTED=[6..18]`). v15 = `CompletionPopup` +
New wire surface ⇒ bump + both-frontends support + acceptance. `StatusFacts.message`; v16 = `ThemeFacts`; v17 = `FontFacts`; v18 =
`StatuslineSegments`. New wire surface ⇒ bump + both-frontends support +
acceptance.
**Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`, **Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`,
`rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation), `rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation),
@ -337,6 +410,28 @@ New wire surface ⇒ bump + both-frontends support + acceptance.
in per-session baselines; and any daemon-side reset needs its in per-session baselines; and any daemon-side reset needs its
frontend mirror audited in the same round (the GPU snapshot arm frontend mirror audited in the same round (the GPU snapshot arm
missed search/menu/status the first time). missed search/menu/status the first time).
- **Tab width is a rendering-parity bug, NOT a config gap** (scouted at
`7bc0c61` while framing #127; still true). There are FIVE tab-width
sites across TWO crates with TWO different values: `TAB_WIDTH = 8` in
`src/text_view.rs`, `src/highlight.rs`, `src/diag.rs` and
`src/completion.rs`, versus `advance_minimap_col` in
`pmacs-gpu/src/main.rs` expanding to **4** — and the GPU's main text
path expands tabs *not at all* (buffer bytes reach the frontend raw,
so a literal `\t` is shaped by the font). `editor.tab-width` is
therefore the obvious-looking first config adopter and is not one:
defining the setting cannot make the GPU honor it. Doing it properly
needs frontend tab expansion plus a wire-or-frontend-local decision.
Deferred from #127 on exactly these grounds; don't re-plan it as a
config task.
- **A test that never runs passes.** Two #127 review-round tests passed
vacuously at first: `pmacs.editor.save()` is the RAW save, while
`buffer.before-save` fires inside the `buffer.save` COMMAND
(`builtin/commands/default.lua`), and `save()` no-ops on an
unmodified buffer — so a fixture that opens a file and saves it
asserts on bytes nothing rewrote. Dirty the buffer with a real edit
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.
## 6. Named deferrals (the standing backlog, consolidated) ## 6. Named deferrals (the standing backlog, consolidated)
@ -347,8 +442,9 @@ mid-line comment spans, comment-dwim append-at-EOL, per-language
comment padding. Pairing (framing "Deferred"): wrap-region on opener, comment padding. Pairing (framing "Deferred"): wrap-region on opener,
pair-aware backspace, RET-inside-pair closer-on-own-line, pair-aware backspace, RET-inside-pair closer-on-own-line,
in-string/in-comment inhibit (needs node-at-byte `pmacs.parse`), in-string/in-comment inhibit (needs node-at-byte `pmacs.parse`),
undo amalgamation (pair = one step), balance-aware quotes, undo amalgamation (pair = one step), balance-aware quotes
per-buffer toggle (config-registry-blocked). Editops deferrals (full (the per-buffer toggle SHIPPED in #127 as `editing.auto-pair`).
Editops deferrals (full
list in its framing): recenter (blocked on viewport facts — the GPU list in its framing): recenter (blocked on viewport facts — the GPU
never consumes daemon `view_top`), Unicode case/word classes, never consumes daemon `view_top`), Unicode case/word classes,
region-spanning move/duplicate, locale collation for sort-lines, region-spanning move/duplicate, locale collation for sort-lines,
@ -364,7 +460,20 @@ origin-pinned `buffer.after-edit` fan-out (a context-switching
intercept changes what later callbacks — LSP, completion — observe). intercept changes what later callbacks — LSP, completion — observe).
LSP/persistence: hidden-buffer LSP attach, daemon desktop-restore, the LSP/persistence: hidden-buffer LSP attach, daemon desktop-restore, the
*warning* half of external-change detection (verify-visited-file- *warning* half of external-change detection (verify-visited-file-
modtime), config registry (no unified config surface yet). modtime).
Config registry (SHIPPED #127; these are its own named deferrals):
persistence of settings and the `custom-file` split-brain question,
`M-x list-settings` as a listview panel, a settings completion source
for the minibuffer (`minibuffer.read`'s `source` is a fixed Rust-side
vocabulary), table-valued settings (so `pmacs.lsp.config`,
`pmacs.pair.sets`, `pmacs.comment.strings` and the `pmacs.parse.*`
write-through proxies stay raw Lua), migrating the remaining scalar
setters (`async_config` ×2, `killring.max`, the `enable` booleans) and
`pmacs.gpu.set_font`, pending-set staging for names defined after
`init.lua` runs, and a `scope = "global"` define flag — `set_local` is
currently accepted for `autosave.interval-ms`, where a per-buffer
value is meaningless.
**Tab width is NOT a config gap** — see §5.
Highlight/detection (from the #114#118 side-quest + injections #122): Highlight/detection (from the #114#118 side-quest + injections #122):
locals-query processing (run each grammar's LOCALS_QUERY so locals-query processing (run each grammar's LOCALS_QUERY so
`#is?`/`#is-not? local` is honored instead of the current fail-closed `#is?`/`#is-not? local` is honored instead of the current fail-closed
@ -377,18 +486,11 @@ runtime/Lua-registered languages (v1 resolves only against
`BUILTIN_LANGUAGES`), and the next injection *consumers* gated on new `BUILTIN_LANGUAGES`), and the next injection *consumers* gated on new
grammars — HTML/CSS/GraphQL/SQL (`<script>`/`<style>`, JS/TS template grammars — HTML/CSS/GraphQL/SQL (`<script>`/`<style>`, JS/TS template
literals, doc-comment code); modeline detection as a 5th layer literals, doc-comment code); modeline detection as a 5th layer
(`-*- mode: … -*-` / `# vim: ft=…`); (`-*- mode: … -*-` / `# vim: ft=…`);
byte-accurate multibyte cursor placement in `move_active_cursor_to` byte-accurate multibyte cursor placement in `move_active_cursor_to`
(still steps one codepoint per LSP byte column). **JSON + YAML PR #123 (still steps one codepoint per LSP byte column). A full Jupyter `.ipynb`
OPEN** (grammar-gap style, `tree-sitter-json`/`-yaml`; LSP configs setup (reader → editable → kernel execution) now has its JSON grammar
`vscode-json-language-server` with provider pin prerequisite, but remains a real arc, not a one-shot.
`@t1ckbase/vscode-langservers-extracted@2.0.2`, plus
`yaml-language-server`). The public and checkpoint branches are both at
fully gated `5c202c5`, rebased onto `f8096ff`; the JSON and YAML
PATH-gated pmacs smokes both passed, and the PR awaits user review.
Once merged, YAML `---` and TOML `+++` markdown frontmatter highlight via
the #122 engine and the Jupyter reader → editable → kernel arc has both
grammar prerequisites.
GPU: auto-reconnect after daemon restart, splits/multi-buffer, gutter GPU: auto-reconnect after daemon restart, splits/multi-buffer, gutter
riders (whitespace guides, folding, git markers). riders (whitespace guides, folding, git markers).
Themes (full list in theme-faces framing rev 9 "Deferred (named)"): Themes (full list in theme-faces framing rev 9 "Deferred (named)"):
@ -397,7 +499,8 @@ popup/menu/dropdown bg + selected-row faces, `ui.background` /
palette (+`ui.selection` for peer rects), `ui.inlay_hint` (needs the palette (+`ui.selection` for peer rects), `ui.inlay_hint` (needs the
epoch treatment on its producer), wire alpha, `Indexed` palette epoch treatment on its producer), wire alpha, `Indexed` palette
unification, named-theme registry / light theme / persistence unification, named-theme registry / light theme / persistence
(config-registry-blocked), grid-vs-wire `default_style` asymmetry, (the registry exists now, #127; theme persistence still waits on
settings persistence), grid-vs-wire `default_style` asymmetry,
mask widening (gutter bg, wash glyph recolor, statusline bg echo mask widening (gutter bg, wash glyph recolor, statusline bg echo
surface, chrome bold/italic/underline re-shaping). surface, chrome bold/italic/underline re-shaping).
Housekeeping: F-016 `lua_bindings/mod.rs` split paused mid-way Housekeeping: F-016 `lua_bindings/mod.rs` split paused mid-way
@ -419,5 +522,7 @@ Don't expect them in a clone; on the desktop, never delete them.
When a PR merges, an arc opens/closes, or a decision lands: edit the When a PR merges, an arc opens/closes, or a decision lands: edit the
snapshot (§1), append lessons (§5) and deferrals (§6) as they arise, snapshot (§1), append lessons (§5) and deferrals (§6) as they arise,
bump the date line at the top, and commit — usually riding the same PR bump the date line at the top, and commit — usually riding the same PR
as the work. Keep it under ~250 lines: this is a briefing, not a log; as the work. Keep durable architecture here; put branch hashes,
prune sections that stop being true. machine-local tools, incomplete verification, and recovery commands in
`docs/active-work.md`. This is a briefing, not a log: prune sections
that stop being true.

379
docs/keybindings.md Normal file
View File

@ -0,0 +1,379 @@
# pmacs keybindings — reference
**Last verified against `main` @ `f8096ff` (2026-07-20).** This is a
snapshot, not generated output — when a PR adds, removes, or rebinds a
key, update this file in the same PR (see §6). If you're an agent and
this file looks stale against the code it cites, trust the code.
pmacs keys come from two independent places:
- **The Lua keymap** (§12) — `pmacs.keymap.bind{...}` calls, resolved
by the Rust dispatcher against whatever `init.lua` has bound at
runtime. Fully user-rebindable: unbind or rebind any of these from
init.lua (§5).
- **Rust-hardcoded modal shadows** (§3) — isearch, query-replace,
the minibuffer/prompt, the completion popup, and the context menu
each shadow the Lua keymap while active: `EditorInstance::dispatch_key`
(`src/editor.rs:658-733`) checks these modes, highest-priority first,
before a key ever reaches the Lua dispatcher. **Not user-configurable**
— there is no `pmacs.keymap` surface for them; changing one means
editing the mode's `from_chord` decoder in Rust.
Notation matches what `pmacs.keymap.bind` accepts: `C-` = Ctrl, `M-` =
Alt/Meta, `S-` = Shift, bare letters/punctuation self-insert when
unmodified. Named keys are angle-bracketed (`<left>`, `<up>`, `<home>`)
or all-caps (`RET`, `BS`/Backspace, `DEL`/Delete, `TAB`, `SPC`).
Sequences separated by spaces (`C-x C-s`) are chords typed in order.
## 1. Global keymap
Source: `builtin/keymaps/default.lua` unless noted. All bound at
`scope = "global"`.
### Cursor motion
| Key | Command |
|---|---|
| `C-a` / `<home>` | `cursor.line-start` |
| `C-e` / `<end>` | `cursor.line-end` |
| `C-f` / `<right>` | `cursor.right` |
| `C-b` / `<left>` | `cursor.left` |
| `C-n` / `<down>` | `cursor.down` |
| `C-p` / `<up>` | `cursor.up` |
| `C-<left>` / `M-b` | `cursor.word-left` |
| `C-<right>` / `M-f` | `cursor.word-right` |
| `C-<up>` / `M-{` | `cursor.paragraph-up` |
| `C-<down>` / `M-}` | `cursor.paragraph-down` |
| `<pageup>` / `M-v` | `cursor.page-up` |
| `<pagedown>` / `C-v` | `cursor.page-down` |
| `M-g g` / `M-g M-g` | `cursor.goto-line` (`builtin/runtime/editops.lua`) |
### Selection (CUA shift-select)
Plain motion preserves an existing selection instead of dropping it
(Emacs-flavored default, not strict CUA).
| Key | Command |
|---|---|
| `S-<left>` / `S-<right>` | `cursor.select-left` / `cursor.select-right` |
| `S-<up>` / `S-<down>` | `cursor.select-up` / `cursor.select-down` |
| `S-<home>` / `S-<end>` | `cursor.select-line-start` / `cursor.select-line-end` |
| `C-S-<left>` / `C-S-<right>` | `cursor.select-word-left` / `cursor.select-word-right` |
| `C-S-<up>` / `C-S-<down>` | `cursor.select-paragraph-up` / `cursor.select-paragraph-down` |
### Editing
| Key | Command |
|---|---|
| `BS` | `buffer.delete-backward` |
| `DEL` / `C-d` | `buffer.delete-forward` |
| `RET` | `edit.newline-and-indent` |
| `TAB` | `buffer.tab` |
| `C-BS` / `C-h` | `buffer.delete-word-backward` (see §4 for the `C-h` rationale) |
| `M-BS` | `buffer.delete-word-backward` |
| `C-DEL` | `buffer.delete-word-forward` |
| `M-d` | `buffer.delete-word-forward` |
| `M-u` | `edit.upcase` (`editops.lua`) |
| `M-l` | `edit.downcase` (`editops.lua`) |
| `M-c` | `edit.capitalize` (`editops.lua`) |
| `C-t` | `edit.transpose-chars` (`editops.lua`) |
| `M-t` | `edit.transpose-words` (`editops.lua`) |
| `M-z` | `edit.zap-to-char` (`editops.lua`) |
| `M-<up>` / `M-<down>` | `edit.move-line-up` / `edit.move-line-down` (`editops.lua`) |
| `M-^` | `edit.join-line` (`editops.lua`) |
| `M-;` | `edit.toggle-comment` (`builtin/runtime/comment.lua`) |
> `M-d` / `M-BS` currently plain-delete the word — they are **not**
> kill-ring members yet (a named deferral; see `docs/agent-handoff.md`
> §6, "word kills"). `edit.kill-line` (below) is the only word/line
> kill wired into the ring so far.
### Clipboard & kill ring
| Key | Command |
|---|---|
| `M-w` | `edit.copy` |
| `C-w` | `edit.cut` |
| `C-y` | `edit.paste` |
| `C-x h` | `edit.select-all` (Emacs `mark-whole-buffer`) |
| `C-k` | `edit.kill-line` (`builtin/runtime/killring.lua`) |
| `M-y` | `edit.yank-pop` — replace the just-yanked text with the previous kill, immediately after `C-y` (`killring.lua`) |
### Undo / redo
Multiple bindings exist because terminals disagree on how `Ctrl+/`
encodes; see §4.
| Key | Command |
|---|---|
| `C-/` / `C-_` / `C-4` / `C-x u` | `buffer.undo` |
| `C-?` / `C-S-_` / `C-x r` | `buffer.redo` |
### Search & replace
Once a search is running, `C-s`/`C-r` step to the next/previous match
and `M-r` toggles literal↔regex — those are Rust-hardcoded isearch
keys, not Lua bindings (§3).
| Key | Command |
|---|---|
| `C-s` | `search.forward` (starts isearch) |
| `C-r` | `search.backward` (starts isearch) |
| `C-M-s` | `search.forward-regex` |
| `C-M-r` | `search.backward-regex` |
| `M-%` | `query-replace` (starts an interactive replace session, §3) |
| `C-M-%` | `query-replace-regexp` |
### Multi-key (`C-x`) chords
| Key | Command |
|---|---|
| `C-x C-s` | `buffer.save` |
| `C-x C-c` | `editor.quit` |
| `C-x 2` | `window.split-horizontal` |
| `C-x 3` | `window.split-vertical` |
| `C-x o` / `C-x O` | `window.focus-next` / `window.focus-prev` |
| `C-x 0` | `window.close` |
| `C-x 1` | `window.close-others` |
| `C-x b` | `editor.switch-buffer` |
| `C-x C-b` | `editor.list-buffers` (opens the `*buffer-list*` panel, §2) |
| `C-x <right>` / `C-x <left>` | `editor.next-buffer` / `editor.previous-buffer` |
| `C-x C-r` | `recent-files` (`builtin/runtime/recentf.lua`) |
### Command palette & cancellation
| Key | Command |
|---|---|
| `M-x` | `editor.execute-command` — prompts (via the minibuffer, §3) for any command by name |
| `C-g` | `editor.cancel` — resets the dispatcher / clears an unfinished prefix |
### Completion
| Key | Command |
|---|---|
| `C-M-i` | `completion.at-point` (`builtin/runtime/completion.lua`) — opens the popup; popup navigation is Rust-hardcoded (§3) |
### LSP
Source: `builtin/runtime/lsp.lua`. `M-.` follows the cross-editor
go-to-definition convention; the rest sit on the `C-c` prefix to keep
printable letters free for self-insert.
| Key | Command |
|---|---|
| `M-.` | `lsp.go-to-definition` |
| `M-?` | `lsp.find-references` (opens `*references*` panel, §2) |
| `M-,` | `lsp.jump-back` (unwind the cross-file jump ring) |
| `C-c o` | `lsp.document-symbols` (opens `*outline*` panel, §2) |
| `C-c r` | `lsp.rename` |
| `C-c a` | `lsp.code-actions` |
| `C-c i` | `lsp.inlay-hints` |
| `C-c y` | `lsp.semantic-tokens` |
| `C-c h` | `lsp.hover` |
| `C-c H` | `lsp.hover-doc` (opens `*lsp-help*` panel, §2) |
| `C-c s` | `lsp.signature-help` |
| `C-c f` | `lsp.format-buffer` |
`builtin/runtime/lsp.lua` initially binds `M-g n` / `M-g p` to
diagnostic navigation. `compile.lua` loads afterward and deliberately
replaces them with the unified error dispatcher below.
### Compile, shell command, and unified errors
Source: `builtin/runtime/compile.lua`.
| Key | Command |
|---|---|
| `M-g n` / `M-g p` | `error.next` / `error.previous` — compile/grep errors when that source has claimed navigation, otherwise LSP diagnostics |
| `` C-x ` `` | `error.next` |
| `M-!` | `shell.command` — asynchronous output in `*shell-command*` |
`compile.run` and `compile.recompile` are available through `M-x`; no
global key is assigned to them.
## 2. Buffer-local panel keymaps
Read-only panel buffers built on `pmacs.listview.open` (buffer scope
`{ scope = "buffer", buffer = <id> }`; see `builtin/runtime/listview.lua`)
all share one keymap:
| Key | Action |
|---|---|
| `RET` / `SPC` | `listview.visit` — act on the item under the cursor |
| `n` / `<down>` | `cursor.down` |
| `p` / `<up>` | `cursor.up` |
| `g` | `listview.refresh` — re-run the data source and re-render |
| `q` | `listview.quit` — restore the buffer that was active before the panel opened |
Panels currently built on this: `*references*`, `*outline*`,
`*lsp-help*` (hover docs). Header text always spells out the same
`RET`/`n`/`p`/`g`/`q` legend inline.
`*buffer-list*` (`editor.list-buffers`, `C-x C-b`) uses its own
keymap, layered on the same idiom, in `builtin/commands/default.lua`:
| Key | Command |
|---|---|
| `RET` / `SPC` | `editor.buffer-list-visit` |
| `n` / `<down>` | `cursor.down` |
| `p` / `<up>` | `cursor.up` |
| `d` | `editor.buffer-list-mark-delete` |
| `u` | `editor.buffer-list-unmark` |
| `x` | `editor.buffer-list-execute` — kill every marked buffer |
| `k` | `editor.buffer-list-kill-now` |
| `g` | `editor.buffer-list-refresh` |
| `q` | `editor.buffer-list-quit` |
One-off buffer-local bindings, each scoped to a single generated
buffer:
| Buffer | Key | Command |
|---|---|---|
| `*workers*` (`editor.list-workers`) | `C-c C-k` | `workers.cancel-at-point` (`builtin/runtime/async.lua`) |
| `*pmacs-instance*` (`editor.describe-instance-buffer`) | `q` | `buffer.kill-this` (`commands/default.lua`) |
| `*help*` (`editor.describe-command`) | `q` | `buffer.kill-this` |
| REPL buffers (`builtin/packages/repl/init.lua`) | `RET` | `pmacs.repl.submit-current` |
| REPL buffers | `C-c` | `pmacs.repl.send-sigint-current` |
| REPL buffers | `C-d` | `pmacs.repl.send-eof-current` — closes stdin on an empty line, else deletes forward |
Compile-mode generated buffers (`*compilation*` and
`*shell-command*`) have their own buffer-local map:
| Key | Command |
|---|---|
| `RET` | `compile.visit-error` |
| `n` / `p` | `compile.next-error-line` / `compile.previous-error-line` |
| `q` | `compile.quit` |
| `C-c C-k` | `compile.kill` |
| `g` | `compile.recompile` (`*compilation*` only) |
| every shipped undo/redo chord | `compile.undo-noop` — generated output is intercept-read-only |
The REPL package (`builtin/packages/repl/`) is shipped but opt-in —
loaded via `require`, not part of the always-on `builtin/runtime`
lane. Its bindings only exist in a buffer created by a REPL session.
## 3. Rust-hardcoded modal keys
These live in `src/editor.rs` (and `src/minibuffer.rs` for the
prompt) as small `from_chord(chord) -> Action` decoders, one per mode,
checked in priority order by `EditorInstance::dispatch_key`
(`src/editor.rs:658-733`, highest first): **context menu → isearch →
query-replace → minibuffer → completion popup → normal Lua dispatch.**
Each decoder's rustdoc names its own key list; this table mirrors
those. They are not reachable through `pmacs.keymap` — there is
deliberately no Lua surface for them (keeps the set curated; see the
`R51` rationale cited in `lib.rs`/`lua_bindings/mod.rs`).
**Isearch** (`SearchKey`, `editor.rs:1858-1919`) — active after
`C-s`/`C-r`/`C-M-s`/`C-M-r`:
| Key | Action |
|---|---|
| `C-s` / `<down>` | next match |
| `C-r` / `<up>` | previous match |
| `RET` / `C-m` | accept — keep cursor + highlights |
| `C-g` / `Esc` | cancel — restore the origin cursor |
| `BS` / `C-h` | shorten the query by one character |
| `M-r` | toggle literal ↔ regex |
| any printable char | extend the query |
**Query-replace** (`QueryReplaceKey`, `editor.rs:1926-1961`) — active
after `M-%`/`C-M-%`:
| Key | Action |
|---|---|
| `y` / `SPC` | replace this match, advance |
| `n` / `BS` / `Delete` | skip this match, advance |
| `!` | replace this and every remaining match, no more prompts |
| `.` | replace this match, then quit |
| `q` / `RET` / `Esc` / `C-g` | quit (replacements already made are kept) |
**Minibuffer / prompt** (`MinibufferAction`, `minibuffer.rs:418-527`)
— backs every `pmacs.minibuffer.read` call: `M-x`, query-replace's
from/to prompts, `find-file`, etc.:
| Key | Action |
|---|---|
| `RET` / `C-m` | accept |
| `C-g` | cancel |
| `TAB` / `C-i` | complete to the selected candidate |
| `<up>` / `<down>` | prev/next candidate if a dropdown is showing, else history navigation |
| `C-p` / `C-n` | history prev/next, unconditionally |
| `<left>` / `C-b`, `<right>` / `C-f` | cursor move |
| `<home>` / `C-a`, `<end>` / `C-e` | line start/end |
| `BS` | delete backward |
| `Delete` / `C-d` | delete forward |
| `M-n` / `M-p` | scroll the selected candidate forward/back |
| any other printable char | self-insert |
**In-buffer completion popup** (`CompletionPopupKey`,
`editor.rs:2019-2059`) — active after `C-M-i` or an LSP-triggered
popup. Unlike the others this is a **partial** shadow: only the keys
below are intercepted; everything else (typing, motion) falls through
to normal dispatch, so typing keeps self-inserting while the popup is
open.
| Key | Action |
|---|---|
| `<down>` / `C-n` | next candidate |
| `<up>` / `C-p` | previous candidate |
| `TAB` / `RET` | accept the highlighted candidate |
| `Esc` / `C-g` | dismiss |
**Context menu** (`MenuKey`, `editor.rs:1966-2009`) — opened by
right-click, not a keybinding itself, but shadows the keymap while
open:
| Key | Action |
|---|---|
| `<down>` / `C-n` | next item |
| `<up>` / `C-p` | previous item |
| `RET` | invoke the highlighted item |
| `Esc` / `C-g` | cancel |
| any other key | dismiss (click-away semantics) |
**Frontend detach** — `F12` (any modifiers) detaches an attached
frontend from the daemon (`src/attach.rs:997-1006`, checked at
`attach.rs:818`). Not a UI mode inside the editor core, but another
literal-`KeyCode` interception outside the Lua keymap; tentative for
v0.1 per the comment there (chosen because F12 is rarely bound to
anything else).
## 4. Terminal-compatibility caveats
- **`C-h` doubles as `C-BS`.** Most terminals without the kitty
keyboard protocol can't disambiguate `Ctrl+Backspace` from
`Ctrl+H` — both legacy paths send byte `0x08`. `C-h` is bound to
`buffer.delete-word-backward` alongside `C-BS` so the shortcut works
on legacy terminals. pmacs does not use `C-h` as an Emacs-style help
prefix; a user who wants that can rebind it.
- **Undo/redo have redundant bindings** (`C-/`, `C-_`, `C-4` for undo;
`C-?`, `C-S-_` for redo) because terminals encode `Ctrl+/` several
different ways. Kitty's keyboard protocol routes most cleanly
through `C-/`; the alternates keep legacy/remote terminals working.
- Kitty-protocol-only chords (e.g. distinguishing `C-i` from `TAB`)
degrade gracefully where noted above — check the frontend's terminal
capability negotiation if a chord seems to not fire.
## 5. Changing bindings
`pmacs.keymap.bind` / `pmacs.keymap.unbind` are ordinary Lua API,
callable from `init.lua`:
```lua
pmacs.keymap.bind { scope = "global", sequence = "C-c g", command = "cursor.goto-line" }
pmacs.keymap.unbind { scope = "global", sequence = "M-z" }
```
`scope = "buffer"` additionally takes `buffer = <id>`; buffer-local
bindings are pruned automatically when that buffer is removed. This
covers §1 and §2 only — §3's Rust-hardcoded modal keys have no Lua
surface (see §3's intro).
## 6. Keeping this file honest
Update this file in the same PR whenever a binding is added, removed,
or moved — same discipline as `docs/agent-handoff.md`. To re-derive it
from scratch instead of trusting the table: grep `builtin/` for
`pmacs.keymap.bind`/`.bind(` and `pmacs.listview.open`, and grep
`src/editor.rs` / `src/minibuffer.rs` for `from_chord`.

View File

@ -4,6 +4,10 @@ Date: 2026-07-07. Produced from a five-way codebase/docs/memory sweep
(core editing + persistence, LSP surface, GPU parity, extensibility + (core editing + persistence, LSP surface, GPU parity, extensibility +
terminal, deferred-work inventory across all framing docs). terminal, deferred-work inventory across all framing docs).
> **Historical planning snapshot.** Several arcs below have since
> landed. Use `docs/agent-handoff.md` for durable current state and
> `docs/active-work.md` for open branches and recovery instructions.
**Decision (2026-07-07): push Arc 1 (LSP utility surface), with Arc 2 **Decision (2026-07-07): push Arc 1 (LSP utility surface), with Arc 2
(editing table stakes) items interleaved between sub-arcs.** (editing table stakes) items interleaved between sub-arcs.**
@ -50,64 +54,46 @@ modes; DAP debugging; GPU splits / multi-buffer / auto-reconnect.
## Arcs, ranked by value-per-effort ## Arcs, ranked by value-per-effort
### Arc 1 — LSP utility surface: "light up the dark matter" ← ACTIVE ### Arc 1 — LSP utility surface: "light up the dark matter" — COMPLETE
Data layer is done; only UI is missing. The completion popup and LSP utility panels shipped across #92#96,
followed by hardening in #102, #105, and #106. Completion, hover,
references, document symbols, and the supporting semantic/signature
paths are now wired through both frontend contracts.
- **1a. In-buffer completion popup** (first). Trigger on typing, ### Arc 2 — Editing table stakes — COMPLETE
TAB/RET accept, both frontends. GPU needs a wire message — mirror the
minibuffer-dropdown pattern (protocol v12). Framework + popup view
already exist.
- **1b. Panels**: hover popup, code-action picker, references list,
document-symbol outline. Generalize the buffer-list UI pattern
(`*buffer-list*` buffer-local bindings) into a reusable list-buffer
idiom.
- **1c. Semantic-token auto-pull fix** (small): pull on attach + on
edit-flush, like inlay hints already do.
- **1d. Signature-help auto-trigger** on `(`.
### Arc 2 — Editing table stakes (interleave with Arc 1) Query-replace (#97), the real kill ring and `M-y` (#103/#105/#106),
comment toggle (#107), auto-indent (#109), and auto-pairing (#110)
landed.
Each small, core-only, frontend-agnostic: query-replace (isearch ### Arc 3 — Persistence/serialization — COMPLETE
exists; `search.rs` has no replace API), real kill ring + `M-y`,
comment/uncomment, auto-indent on newline, auto-pairing.
### Arc 3 — Persistence/serialization Saveplace/recentf (#98), desktop save/restore (#99),
autosave/crash-recovery (#100), and the save-clobber fix (#101) landed.
Desktop-save (buffer set + layout + cursors → restore), recentf, ### Arc 4 — Themes + extensibility surface — COMPLETE
saveplace, autosave + crash recovery, optional backups. Generalize the
`$XDG_STATE_HOME/pmacs/` pattern from minibuffer history. Framing
question: what is a "session" in a daemon world; do CRDT snapshots
ride along.
### Arc 4 — Themes + extensibility surface — COMPLETE ON `main` Stages 13 landed as #120, #124, and #125: named `ui.*` faces with
daemon-resolved `ThemeFacts`; the live global `pmacs.gpu.set_font`
preference at protocol v17; and composable per-window
`pmacs.statusline` providers transported to semantic/GPU frontends by
protocol-v18 `StatuslineSegments`.
All three stages landed: #120 added named `ui.*` faces and daemon-resolved ### Arc 5 — Terminal, staged — VTERM CORE LANDED
`ThemeFacts`; #124 added the live global `pmacs.gpu.set_font` preference at
protocol v17; and #125 added composable `pmacs.statusline` providers,
per-window TUI composition, a pure built-in LSP segment, dynamic modeline
faces, and semantic/GPU transport through protocol v18.
### Arc 5 — Terminal, staged — VTERM STAGE 1 ON FEATURE BRANCH - **Compile mode landed in #113**: line-oriented PTY/ANSI output,
- **Compile-mode landed** in #113: line-oriented PTY/ANSI output,
error-regex navigation, and `M-x compile`. error-regex navigation, and `M-x compile`.
- **Vterm Stage 1 terminal core** is implemented, two review rounds are - **Vterm Stage 1 terminal core landed in #126**: compatibility parser
addressed, and the branch is fully gated on `vterm-core`; PR #126 awaits profiles, bounded VT screen/scrollback/reflow state, input encoders,
merge authorization and is **not merged**. It adds compatibility parser internal `TerminalManager`, read-only identity buffers, process lifecycle,
profiles, bounded VT screen/scrollback/reflow state, IND/NEL/RI, input control-free renderer-boundary cells, and headless real-PTY acceptance.
encoders, internal `TerminalManager`, read-only identity buffers, process - **Vterm Stage 2 TUI is next**: terminal-window composition, input/resize,
lifecycle, renderer-safe control-free cells, and headless real-PTY per-context scroll/selection/copy, and the Lua surface.
acceptance. It intentionally adds no interactive Lua command or frontend - **Vterm Stage 3 protocol/GPU follows Stage 2**: additive protocol v19
rendering. complete frames, authenticated daemon routing, and native GPU terminal
- **Vterm Stage 2 TUI** starts only after Stage 1 merges: terminal-window rendering. Its framing must resolve the 16 MiB transport cap's incompatibility
composition, input/resize, per-context scroll/selection/copy, and the Lua with the legal worst complete terminal frame; never silently chunk.
surface.
- **Vterm Stage 3 protocol/GPU** starts only after Stage 2 merges: additive
protocol v19 complete frames, authenticated daemon routing, and native GPU
cell rendering. Its framing must resolve the current 16 MiB transport cap's
incompatibility with the legal worst complete terminal frame; never silently
chunk.
### Arc 6 — Folding (keystone gutter rider) ### Arc 6 — Folding (keystone gutter rider)

View File

@ -42,9 +42,11 @@ The direct continuation of the #114#118 grammar/detection stack.
(`<script>`/`<style>`, template literals, doc-comment code). (`<script>`/`<style>`, template literals, doc-comment code).
- **Modeline detection** — a 5th detection layer (`-*- mode: … -*-`, - **Modeline detection** — a 5th detection layer (`-*- mode: … -*-`,
`# vim: ft=…`) after extension → filetype → filename → shebang. `# vim: ft=…`) after extension → filetype → filename → shebang.
- **JSON + YAML** grammars + LSP (`tree-sitter-json` + - **JSON + YAML — PR #123 open.** Grammars and LSP configs exist on the
vscode-json-languageserver; yaml-language-server). JSON is also the feature line; review fixes are preserved on
prerequisite for the notebook path. `json-yaml-handoff-2026-07-20`. A real YAML-through-pmacs smoke,
rebase, and full gates remain before review resumes. JSON is also the
prerequisite for the notebook path; see `docs/active-work.md`.
- **More grammars for languages with neither grammar nor LSP** — ruby, - **More grammars for languages with neither grammar nor LSP** — ruby,
php, html, css, sql, etc. php, html, css, sql, etc.
- **Grapheme / combining-mark awareness** in the text view - **Grapheme / combining-mark awareness** in the text view
@ -112,10 +114,20 @@ The direct continuation of the #114#118 grammar/detection stack.
## Cross-cutting substrate (unblocks whole clusters — high leverage) ## Cross-cutting substrate (unblocks whole clusters — high leverage)
- **Config/settings registry** (`pmacs.config` / defcustom) — blocks - ~~**Config/settings registry**~~**SHIPPED as #127.** `pmacs.config`
language-aware indent, the five hardcoded tab-width sites, per-buffer exists with global + buffer-local scopes. Unblocked: the per-buffer
auto-pair toggle, per-language comment padding, per-project compile auto-pair toggle (shipped in the same PR as `editing.auto-pair`),
commands. language-aware indent, per-language comment padding, and per-project
compile commands — the last three are now ordinary work, expressed as
a `buffer.after-load` hook calling `set_local`, not blocked work.
- **Tab-width rendering parity** — was listed above as a config
consequence; it is not. `TAB_WIDTH = 8` appears four times in the
daemon (`text_view`, `highlight`, `diag`, `completion`), the GPU
minimap's `advance_minimap_col` uses **4**, and the GPU main text path
expands tabs *not at all* — raw `\t` reaches glyphon and is shaped by
the font. Defining `editor.tab-width` cannot make the GPU honor it;
this needs frontend tab expansion plus a wire-or-frontend-local
decision. Deferred from #127 on those grounds.
- **Real `read_only` buffer flag** on both edit paths — true immutability - **Real `read_only` buffer flag** on both edit paths — true immutability
for panels / REPL / generated buffers. for panels / REPL / generated buffers.
- **Mode system wiring** — dispatch passes an empty mode list (`&[]`); - **Mode system wiring** — dispatch passes an empty mode list (`&[]`);
@ -211,8 +223,8 @@ domain excluded — see below), `editor.rs` split (7 k lines),
## Excluded — themes / faces main quest (seen, routed elsewhere) ## Excluded — themes / faces main quest (seen, routed elsewhere)
`pmacs.gpu.set_font` + the statusline-segment API; background/selection The statusline-segment API (Arc 4 stage 3; framing awaiting review);
theming; per-peer stable presence colors; exact quad colors per background/selection theming; per-peer stable presence colors; exact quad colors per
decoration kind; GPU gutter background layer / wash recolor / chrome decoration kind; GPU gutter background layer / wash recolor / chrome
bold-italic-underline; current-line highlight refinements; multi-server bold-italic-underline; current-line highlight refinements; multi-server
semantic-token *style* blending; the compile-mode **severity→color** semantic-token *style* blending; the compile-mode **severity→color**
@ -225,14 +237,20 @@ guides (visual, not color).
## North star (highest-leverage first) ## North star (highest-leverage first)
**Multi-language injections shipped (#122)** — one of the two original **Both original north-star items have now shipped** — multi-language
north-star items is done, so the highest-leverage board is now: injections (#122) and the config registry (#127) — and JSON + YAML
(#123) merged too. The remaining board:
1. **Config registry** — frees ~5 editing/indent/comment features at once. 1. **Locals-query processing** — restores `.builtin` styling for
2. **JSON (+ YAML) grammar** — the one remaining gate on the Jupyter
`.ipynb` path now that injections exist, and a clean highlight one-shot.
3. **Locals-query processing** — restores `.builtin` styling for
non-shadowed builtins, the last rough edge of the highlight stack. non-shadowed builtins, the last rough edge of the highlight stack.
2. **Mode-system wiring** — every editor `KeymapStack::resolve` still
passes `&[]`, so mode-scoped keybindings and any mode-scoped setting
remain unreachable. Promoted here because #127 made it the largest
remaining scoping gap.
3. **Tab-width rendering parity** — five constants across two crates
with two different values, and no tab expansion at all on the GPU
main text path. Explicitly NOT a config-registry task; see the entry
under "Cross-cutting substrate".
Beyond those, the cleanest one-shots in the highlight family are Beyond those, the cleanest one-shots in the highlight family are
**modeline detection** and the HTML/CSS grammars that light up more **modeline detection** and the HTML/CSS grammars that light up more

View File

@ -1,8 +1,7 @@
# Vterm — framing (Arc 5 stage 2, three-PR delivery) # Vterm — framing (Arc 5 stage 2, three-PR delivery)
**Revision 5 — 2026-07-21. Status: Stage 1 review round 2 fixes implemented **Revision 5 — 2026-07-21. Status: Stage 1 landed on `main` through PR #126
and fully gated on `vterm-core`; awaiting merge authorization, not merged. at merge `643d1e1`. Stages 2 and 3 are not implemented.**
Stages 2 and 3 are not implemented.**
Revision 5 establishes the renderer-facing cell invariant before Stage 2: Revision 5 establishes the renderer-facing cell invariant before Stage 2:
terminal text discards C0/C1 controls rather than storing host-terminal control terminal text discards C0/C1 controls rather than storing host-terminal control
@ -37,11 +36,10 @@ later stage starts only after the preceding stage lands on `main`.
## 0. Revision 5 — Stage 1 implementation and review record ## 0. Revision 5 — Stage 1 implementation and review record
The first of the three vterm PRs is implemented, reviewed, and fully gated on The first of the three vterm PRs is implemented, reviewed, fully gated, and
`vterm-core`, awaiting merge authorization. Initial feature commit `bbc1f33`, landed on `main` as merge `643d1e1`. Initial feature commit `bbc1f33`,
first-review fixes through `bf972a7`, and second-review hardening `9797ada` are first-review fixes through `bf972a7`, and second-review hardening `9797ada`
published as open, non-draft PR #126, shipped through PR #126, <https://github.com/levineuwirth/pmacs/pull/126>.
<https://github.com/levineuwirth/pmacs/pull/126>, targeting `main`.
It is deliberately headless: there is no `pmacs.terminal` Lua module, It is deliberately headless: there is no `pmacs.terminal` Lua module,
interactive terminal command, TUI paint branch, or GPU/protocol surface yet. interactive terminal command, TUI paint branch, or GPU/protocol surface yet.
@ -837,16 +835,13 @@ Per-stage utilization:
## 8. Branch and PR plan ## 8. Branch and PR plan
Stage 1 is implemented and fully gated on `vterm-core`; review fixes through Stage 1 landed on `main` through PR #126 at merge `643d1e1`. Continue the
`bf972a7` are published in open, non-draft PR #126 approved sequential plan:
(<https://github.com/levineuwirth/pmacs/pull/126>), targeting `main` and not
merged. Continue the approved plan only after the preceding PR lands:
1. review PR #126; merge only when the user says; 1. create `pmacs-vterm-tui`, branch `vterm-tui`, from post-#126 `main`;
2. after stage 1 merges, create `pmacs-vterm-tui`, branch `vterm-tui`, from the implement, gate, and open the second PR;
new `main`; implement/gate/open a second PR; 2. after Stage 2 merges, create `pmacs-vterm-gpu`, branch `vterm-gpu`, from
3. after stage 2 merges, create `pmacs-vterm-gpu`, branch `vterm-gpu`, from the the new `main`; implement, gate, and open the third PR.
new `main`; implement/gate/open the third PR.
The framing branch is `vterm-framing` in worktree `pmacs-vterm-framing`. The framing branch is `vterm-framing` in worktree `pmacs-vterm-framing`.
Implementation branches are not stacked across an unmerged parent. This avoids Implementation branches are not stacked across an unmerged parent. This avoids