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.**
It carries current project state, the working method, substrate
invariants, and the standing backlog — it is the continuity bridge
between development machines. Keep it updated as part of your work
(update protocol is in the file itself).
**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's review rounds → user says when to
- 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`;
@ -20,7 +21,15 @@ Always true, independent of the handoff:
`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/branch operations, and
never delete untracked files you didn't create.
- Commit messages via `git commit -F <file>`, ending with the Claude
co-author line; PR bodies end with the Claude Code attribution.
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 .`.

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
**Last updated: 2026-07-21, after Vterm Stage 1 review round 2 was addressed
and fully gated on `vterm-core` (awaiting merge authorization; not merged).
Vterm Stages 2 and 3 are not implemented.** This file is the bridge between development
machines. If you are an agent reading on a fresh clone: this document
plus the `docs/*-framing.md` files ARE your memory. Read this fully
before taking on work, seed 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.
**Last updated: 2026-07-21, after the config registry (#127) and Vterm
Stage 1 terminal core (#126) both landed on `main`, atop completed
Themes Arc 4 (#120/#124/#125). Vterm Stages 2 and 3 are not
implemented.**
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`
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)
- Canonical `main` @ `7bc0c61` (#125 merged), protocol
**v18** (`SUPPORTED=[6..18]`).
- `main` @ `2e37c04` (config registry #127), protocol **v18**
(`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)
LANDED** — a one-shot arc built in sibling worktrees off main while
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
flattener runs over the WHOLE buffer via the file-style summary, so it
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;
framing `docs/compile-mode-framing.md` rev 13). `compile.run` streams
`/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,
`basedpyright` filtered); `git diff --check` clean. Stage 3 landed
as #125 and completed Arc 4 on `main`.
- **Vterm Stage 1 terminal core IMPLEMENTED ON `vterm-core`, FULLY GATED,
AWAITING MERGE AUTHORIZATION, NOT MERGED** (`docs/vterm-framing.md` rev 5).
- **Vterm Stage 1 terminal core LANDED ON `main`#126**
(`docs/vterm-framing.md` rev 5; merge `643d1e1`).
- Implementation commits: `bbc1f33` (Stage 1), `962944b` (Darwin signal
normalization), first-review fixes `f0a235f`, `28f2e6c`, `bf972a7`, and
second-review hardening `9797ada`; pull request: #126,
<https://github.com/levineuwirth/pmacs/pull/126> (open, non-draft,
targeting `main`).
second-review hardening `9797ada`; reviewed feature head `fc4e0ce` merged
through PR #126, <https://github.com/levineuwirth/pmacs/pull/126>.
- `AnsiParserProfile::{LineOriented, FullScreen}` preserves compile/REPL
behavior while terminal PTYs emit the full cursor/mode/device operation
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
deliberate complete-frame limit decision: 16 MiB is insufficient; use a
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:
- **Arc 1 (LSP utility surface) COMPLETE** — completion popup
(#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),
desktop-save (#99), autosave/crash-recovery (#100), save-clobber
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)
@ -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.
**Protocol** — encoding-breaking bumps are deliberate and versioned
(`SUPPORTED=[6..15]`). v15 = `CompletionPopup` + `StatusFacts.message`.
New wire surface ⇒ bump + both-frontends support + acceptance.
(`SUPPORTED=[6..18]`). v15 = `CompletionPopup` +
`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`,
`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
frontend mirror audited in the same round (the GPU snapshot arm
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)
@ -347,8 +442,9 @@ mid-line comment spans, comment-dwim append-at-EOL, per-language
comment padding. Pairing (framing "Deferred"): wrap-region on opener,
pair-aware backspace, RET-inside-pair closer-on-own-line,
in-string/in-comment inhibit (needs node-at-byte `pmacs.parse`),
undo amalgamation (pair = one step), balance-aware quotes,
per-buffer toggle (config-registry-blocked). Editops deferrals (full
undo amalgamation (pair = one step), balance-aware quotes
(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
never consumes daemon `view_top`), Unicode case/word classes,
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).
LSP/persistence: hidden-buffer LSP attach, daemon desktop-restore, the
*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):
locals-query processing (run each grammar's LOCALS_QUERY so
`#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
grammars — HTML/CSS/GraphQL/SQL (`<script>`/`<style>`, JS/TS template
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`
(still steps one codepoint per LSP byte column). **JSON + YAML PR #123
OPEN** (grammar-gap style, `tree-sitter-json`/`-yaml`; LSP configs
`vscode-json-language-server` with provider pin
`@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.
(still steps one codepoint per LSP byte column). A full Jupyter `.ipynb`
setup (reader → editable → kernel execution) now has its JSON grammar
prerequisite, but remains a real arc, not a one-shot.
GPU: auto-reconnect after daemon restart, splits/multi-buffer, gutter
riders (whitespace guides, folding, git markers).
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
epoch treatment on its producer), wire alpha, `Indexed` palette
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
surface, chrome bold/italic/underline re-shaping).
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
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
as the work. Keep it under ~250 lines: this is a briefing, not a log;
prune sections that stop being true.
as the work. Keep durable architecture here; put branch hashes,
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 +
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
(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
### 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,
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 — COMPLETE
### 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
exists; `search.rs` has no replace API), real kill ring + `M-y`,
comment/uncomment, auto-indent on newline, auto-pairing.
### Arc 3 — Persistence/serialization — COMPLETE
### 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,
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
### 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
`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 CORE LANDED
### 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`.
- **Vterm Stage 1 terminal core** is implemented, two review rounds are
addressed, and the branch is fully gated on `vterm-core`; PR #126 awaits
merge authorization and is **not merged**. It adds compatibility parser
profiles, bounded VT screen/scrollback/reflow state, IND/NEL/RI, input
encoders, internal `TerminalManager`, read-only identity buffers, process
lifecycle, renderer-safe control-free cells, and headless real-PTY
acceptance. It intentionally adds no interactive Lua command or frontend
rendering.
- **Vterm Stage 2 TUI** starts only after Stage 1 merges: terminal-window
composition, input/resize, per-context scroll/selection/copy, and the Lua
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.
- **Vterm Stage 1 terminal core landed in #126**: compatibility parser
profiles, bounded VT screen/scrollback/reflow state, input encoders,
internal `TerminalManager`, read-only identity buffers, process lifecycle,
control-free renderer-boundary cells, and headless real-PTY acceptance.
- **Vterm Stage 2 TUI is next**: terminal-window composition, input/resize,
per-context scroll/selection/copy, and the Lua surface.
- **Vterm Stage 3 protocol/GPU follows Stage 2**: additive protocol v19
complete frames, authenticated daemon routing, and native GPU terminal
rendering. Its framing must resolve the 16 MiB transport cap's incompatibility
with the legal worst complete terminal frame; never silently chunk.
### 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).
- **Modeline detection** — a 5th detection layer (`-*- mode: … -*-`,
`# vim: ft=…`) after extension → filetype → filename → shebang.
- **JSON + YAML** grammars + LSP (`tree-sitter-json` +
vscode-json-languageserver; yaml-language-server). JSON is also the
prerequisite for the notebook path.
- **JSON + YAML — PR #123 open.** Grammars and LSP configs exist on the
feature line; review fixes are preserved on
`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,
php, html, css, sql, etc.
- **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)
- **Config/settings registry** (`pmacs.config` / defcustom) — blocks
language-aware indent, the five hardcoded tab-width sites, per-buffer
auto-pair toggle, per-language comment padding, per-project compile
commands.
- ~~**Config/settings registry**~~**SHIPPED as #127.** `pmacs.config`
exists with global + buffer-local scopes. Unblocked: the per-buffer
auto-pair toggle (shipped in the same PR as `editing.auto-pair`),
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
for panels / REPL / generated buffers.
- **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)
`pmacs.gpu.set_font` + the statusline-segment API; background/selection
theming; per-peer stable presence colors; exact quad colors per
The statusline-segment API (Arc 4 stage 3; framing awaiting review);
background/selection theming; per-peer stable presence colors; exact quad colors per
decoration kind; GPU gutter background layer / wash recolor / chrome
bold-italic-underline; current-line highlight refinements; multi-server
semantic-token *style* blending; the compile-mode **severity→color**
@ -225,14 +237,20 @@ guides (visual, not color).
## North star (highest-leverage first)
**Multi-language injections shipped (#122)** — one of the two original
north-star items is done, so the highest-leverage board is now:
**Both original north-star items have now shipped** — multi-language
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.
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
1. **Locals-query processing** — restores `.builtin` styling for
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
**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)
**Revision 5 — 2026-07-21. Status: Stage 1 review round 2 fixes implemented
and fully gated on `vterm-core`; awaiting merge authorization, not merged.
Stages 2 and 3 are not implemented.**
**Revision 5 — 2026-07-21. Status: Stage 1 landed on `main` through PR #126
at merge `643d1e1`. Stages 2 and 3 are not implemented.**
Revision 5 establishes the renderer-facing cell invariant before Stage 2:
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
The first of the three vterm PRs is implemented, reviewed, and fully gated on
`vterm-core`, awaiting merge authorization. Initial feature commit `bbc1f33`,
first-review fixes through `bf972a7`, and second-review hardening `9797ada` are
published as open, non-draft PR #126,
<https://github.com/levineuwirth/pmacs/pull/126>, targeting `main`.
The first of the three vterm PRs is implemented, reviewed, fully gated, and
landed on `main` as merge `643d1e1`. Initial feature commit `bbc1f33`,
first-review fixes through `bf972a7`, and second-review hardening `9797ada`
shipped through PR #126, <https://github.com/levineuwirth/pmacs/pull/126>.
It is deliberately headless: there is no `pmacs.terminal` Lua module,
interactive terminal command, TUI paint branch, or GPU/protocol surface yet.
@ -837,16 +835,13 @@ Per-stage utilization:
## 8. Branch and PR plan
Stage 1 is implemented and fully gated on `vterm-core`; review fixes through
`bf972a7` are published in open, non-draft PR #126
(<https://github.com/levineuwirth/pmacs/pull/126>), targeting `main` and not
merged. Continue the approved plan only after the preceding PR lands:
Stage 1 landed on `main` through PR #126 at merge `643d1e1`. Continue the
approved sequential plan:
1. review PR #126; merge only when the user says;
2. after stage 1 merges, create `pmacs-vterm-tui`, branch `vterm-tui`, from the
new `main`; implement/gate/open a second PR;
3. after stage 2 merges, create `pmacs-vterm-gpu`, branch `vterm-gpu`, from the
new `main`; implement/gate/open the third PR.
1. create `pmacs-vterm-tui`, branch `vterm-tui`, from post-#126 `main`;
implement, gate, and open the second PR;
2. after Stage 2 merges, create `pmacs-vterm-gpu`, branch `vterm-gpu`, from
the new `main`; implement, gate, and open the third PR.
The framing branch is `vterm-framing` in worktree `pmacs-vterm-framing`.
Implementation branches are not stacked across an unmerged parent. This avoids