Commit Graph

254 Commits

Author SHA1 Message Date
Levi Neuwirth d6d4be6cfb Merge remote-tracking branch 'githubsucks/main' into gpu-initial-target 2026-07-23 19:03:41 -04:00
Levi Neuwirth 2dd30ec730 Implement session-scoped GPU initial targets
Add protocol-v20 semantic bootstrap and readiness result framing so
`pmacs --gpu FILE` opens the requested path before the GPU window becomes
ready. Keep target identity scoped to the authenticated frontend, preserve
legacy/no-target attach behavior, and publish fresh buffers coherently to
existing replicas.

Carry Unix path bytes and launcher cwd through the root broker, resolve paths
lexically in the daemon, reuse or create buffers without ambient-view state,
and preserve the managed daemon lifecycle from #141. Add focused parser,
wire, lifecycle, hook, isolation, and real-connector acceptance coverage.
2026-07-23 19:03:25 -04:00
Levi Neuwirth 47581f4034
Merge pull request #146 from levineuwirth/web-grammars
feat(web): HTML + CSS grammars + HTML script/style injections
2026-07-23 20:23:33 +00:00
Levi Neuwirth 515c76f5ab docs+test(web): name and pin the @attribute retro-paint (review round 3)
Review found that adding ("attribute", fg(3)) for HTML/CSS also colours the
@attribute capture three already-bundled grammars emit — rust (attribute_item),
lua (<const>), yaml (directives) — which were previously unpainted. Verified on
a Rust buffer: #[derive(Debug)] now paints uniformly yellow (fg 3), an
improvement over unpainted and the distinct-attribute convention most editors
follow.

Name this retro-paint as intended in the framing (Q#WEB4, rev 4) and pin it with
rust_attribute_repaints_via_shared_attribute_capture so it is a chosen effect,
not incidental. @tag is unaffected (HTML/CSS only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:11:47 -04:00
Levi Neuwirth 394d39942c feat(web): bundle HTML + CSS grammars + HTML injections
Register `html` (.html/.htm/.xhtml) and `css` (.css) entries in
BUILTIN_LANGUAGES, backed by the official tree-sitter-html 0.23 and
tree-sitter-css 0.25 grammars over the tree-sitter-language shim (ABI-fine, no
overlay — both export their query constants). The single `extensions` field
wires detection ahead of the LSP filetype map.

HTML's crate-exported INJECTIONS_QUERY lights up <script> -> javascript
(already registered) and <style> -> css (registered here) via the #122
injection engine — the north-star injection consumer.

The only capture reconciliation (Q#WEB4): the two web captures both grammars'
queries use that pmacs did not recognize — ("tag", fg(5)) and
("attribute", fg(3)) — added to highlight.rs's table; @tag.error prefix-walks
to tag, and everything else already maps.

Tests: table guards; load-and-parse smokes (roots document/stylesheet);
highlights-resolve (node-name compat gate, asserts @tag present); extension
resolution; a tag+attribute paint test (the attribute assertion is
load-bearing); and the injection payoff — an HTML buffer with embedded
<style>/<script> paints a CSS property and a JS keyword INSIDE the injected
regions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:04:10 -04:00
Levi Neuwirth c49a8c71be
Merge pull request #142 from levineuwirth/folding
Arc 6 folding — Stage 1: instance fold engine
2026-07-23 18:50:02 +00:00
Levi Neuwirth f09b0a1142
Merge pull request #144 from levineuwirth/latex-grammar
feat(latex): Stage 1 — LaTeX/TeX grammar highlighting (inline-math substrate)
2026-07-23 17:54:04 +00:00
Levi Neuwirth 9691dd4e9f fix(fold): address PR #142 review round 1
- **Finding 1 (bug):** a delete starting exactly at a fold's `end`
  removed the `\n` that `end` names — the last hidden line's terminator —
  but the strictly-after arm (`os >= e`) kept the fold, leaving a mid-line
  end. `translate`'s after-arm is now `os > e || (os == e && old_len == 0)`
  so a pure insert at `e` still stays outside while a delete at `e` falls
  to the drop arm, symmetric with the head side. New unit test
  `delete_starting_at_tail_boundary_drops_fold` (bite-verified).
- **Finding 2:** `pmacs.buffer.kill` didn't clean the fold registry.
  Added `FoldRegistry::forget_buffer(id)` (id-keyed; the view died with the
  buffer) and wired it into `after_buffer_removed`, mirroring the
  keymap/config cleanup; the registry is now stashed as Lua app-data.
  `forget(&mut Buffer)` is clarified as the revert/reload reset.
- **Finding 3:** `fold.close-all` now moves the invoking point to the head
  when it closes a fold around it (Q#FD3); the data-API `fold` exemption
  (programmatic, no invoking point) is named in the module doc.
- Nits: dropped the dead `!(both empty)` conjunct in `fold_state_msg`;
  replaced the trivial fresh-registry assert; added coverage for the
  stale-tree refuse via a fold command, the read-only-buffer rejection
  (Q#FD11), and unfold normalizing an arbitrary range.

Gates green: fmt, clippy --workspace --all-targets, --lib (1786),
--features crdt (1962), folding_acceptance (24), m4 (skip basedpyright),
required-GPU, git diff --check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 13:42:20 -04:00
Levi Neuwirth 1d66089a8e test(latex): add paint half of acceptance 3 (review round 1)
The shipped latex_highlights_resolve covered only the compile / capture-name
gate; the approved framing's acceptance 3 also requires parsing a document and
asserting painted output (the grid_paints_injected_child_keyword template).
tree-sitter compiles a pattern that uses a valid field on the wrong node and
then silently never matches, which the compile gate cannot catch — a real risk
given this grammar cut's structural drift from what the nvim query targets.

latex_grid_paints_section_and_command parses `\section{Intro}` + `\foo{bar}` and
asserts the \foo command_name paints @function (non-default) and the section
title text paints @keyword.control (bold), proving the reconciled query actually
matches, not merely compiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:16:29 -04:00
Levi Neuwirth 3b411dbb2a feat(fold): Arc 6 Stage 1 — instance fold engine (headless)
The fold engine behind `docs/folding-framing.md` (approved rev 5): a
per-buffer fold store, a structural tree-sitter fold source, the
state-aware Lua command + data-API surface with the Emacs hideshow
`C-c @` bindings, the dispatch-layer pre-edit unfold, and `FoldState`
production. No rendering — Stages 2 (grid) and 3 (GPU) consume the store.

- `src/fold.rs`: `FoldStore` (a buffer-attached `View` that translates
  ranges on every edit and drops any whose head/tail the edit crosses,
  provenance-blind — Q#FD6), the structural source (nearest block-like
  node >= 2 source lines -> introducer<->body -> **derived head line**,
  the line immediately above the first hidden line, so wrapped signatures
  and `where` clauses stay visible per R3-1 -> **closer-aware tail**, a
  closing-delimiter line stays visible per R2-5), injection-layer walk,
  `(start, end]` containment, and the state-aware ops (close innermost
  open / open outermost closed / org-TAB cycle). Stale/absent tree
  refuses (Q#FD10).
- `src/lua_bindings/fold.rs`: `pmacs.fold.*` — explicit-buffer data API
  (`fold`/`unfold`/`folds`/`toggle`) + interactive helpers, validation
  (Q#FD11: document buffer, UTF-8 boundaries, >= 1 hidden line — Q#FD9
  falls out of the last clause), point-moves-to-head (Q#FD3).
- `builtin/runtime/fold.lua`: `fold.toggle/close/open/close-all/open-all`
  commands + the `C-c @` prefix set (Q#FD4).
- `src/editor_core.rs`: the six point-anchored edit primitives run the
  pre-edit unfold keyed on the authenticated source's point (Q#FD5,
  command path); `EditorCore` owns the shared `FoldRegistry`.
- `src/semantic_render.rs`: the `FoldState` producer —
  authoritative-empty, diff-suppressed, baseline resets on
  `BufferSnapshot` (Q#FD8); the "never emitted" pin split so
  `BlockAdornments` stays unproduced.
- `tests/folding_acceptance.rs` (16) over real Rust/Python/markdown
  grammars + `fold_state_producer_transitions` + 15 engine unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 12:14:00 -04:00
Levi Neuwirth f11d625fb0 feat(latex): bundle LaTeX grammar + reconcile highlights + tests
Register a `latex` entry in BUILTIN_LANGUAGES (.tex/.latex/.sty/.cls) backed by
codebook-tree-sitter-latex 0.6.1 — the linkable republish of latex-lsp's grammar
over the tree-sitter-language shim (the squatted `tree-sitter-latex` 0.1.0 ships
no scanner.c and cannot link). The single `extensions` field wires the whole
detection chain ahead of the LSP filetype map, so no Lua edit is needed.

The crate exports no query constants, so highlighting is driven by the in-repo
overlay builtin/queries/latex/highlights.scm — the first such overlay,
include_str!'d as LATEX_HIGHLIGHTS (the audit-rules.scm precedent). This commit
reconciles the vendored nvim-treesitter query (previous commit) onto pmacs'
recognized capture set:
  * strip @spell/@nospell — meaningless to pmacs, and clobber-risk on a
    multi-capture node;
  * remove the 8 #eq?/#any-of?/#lua-match? patterns — pmacs evaluates only
    `#is? local`, so unevaluated they would over-match every generic command
    (as conditional/emphasis) and every line comment (as a magic directive);
  * remap fall-through captures: @module->keyword, @label->type,
    @markup.heading*->keyword.control, @markup.link*->constant,
    @markup.math->string;
  * fix node-name drift: this grammar cut uses curly_group_label(_list) for the
    label commands where newer latex-lsp unified them onto curly_group_text.

Tests (framing acceptance): table guard; load-and-parse including a verbatim
environment (exercises the external scanner the broken crate lacked);
highlights-resolve (doubles as the grammar/query node-name compatibility gate);
and extension resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:04:49 -04:00
Levi Neuwirth 82355ca529 Address GPU invocation review findings
Buffer attach events until winit state exists, keep spawned daemon ownership
until the reaper handoff, and detach daemon stderr from the launcher terminal.
Tighten direct GPU CLI guidance and sibling discovery. Strengthen managed
connector unit and process acceptance coverage for transient retries, timeout
reporting, hermetic paths, and deterministic loser reaping.
2026-07-23 11:50:33 -04:00
Levi Neuwirth 6fd583417b Add one-command managed GPU invocation
Add the root --gpu broker, strict GPU entry points, daemon connect-or-start orchestration, process-group isolation, bounded retry, named child reaping, and a deterministic headless lifecycle probe. Cover the complete launch matrix with real subprocess acceptance, make root Cargo runs unambiguous, and document the coherent build and one-command workflow.
2026-07-23 11:02:09 -04:00
Levi Neuwirth 3c4d969aba Merge canonical main into vterm stage 3
Integrates canonical `main` @ 2625ec7 after PR #137 (tab-width parity)
merged. The agreed order was #137 first, this lane second: #137 was
approved and FROZEN at 5b23e11, and "frozen" is incompatible with
"rebase onto the resulting main" — landing it second would have broken
its freeze and voided its approval.

Integrated by MERGING main into the branch rather than rebasing, matching
repo precedent (Merge canonical main into vterm-tui, ... into modeline
detection). A rebase would have force-pushed away the review anchors on
the two completed review rounds of #135.

Main had also moved past this lane's base by #133/#134/#136, so the
integration surface was wider than the #135/#137 overlap: src/
semantic_render.rs was a fourth overlapping code file. It auto-merged, as
did pmacs-protocol/src/lib.rs. The single code conflict was the
pmacs_protocol import list in pmacs-gpu/src/main.rs — TAB_STOP_COLUMNS
against the terminal types — resolved as a union.

The feared semantic collision did not occur, and this is verified rather
than assumed: terminal cell geometry still uses the monospace advance and
never TAB_STOP_COLUMNS. pmacs-gpu/src/terminal.rs references neither the
constant nor display_width, and terminal_cell_viewport / terminal_run_rect
/ hit_test_cell derive from mono_advance() and code_line_height() alone.
That separation is correct by construction: a terminal's columns come
from the child, while tab expansion is a document projection concern.

Doc conflicts resolved toward landed state: the tab-width lane moves to
"Closed since the last snapshot", the #135/#137 coordination section is
kept as a resolved worked example, and the Arc 5 lines in the roadmap and
handoff now read "implemented and in review". While resolving, restored a
clause main had dropped from the handoff's injection-follow-ups list
("literals, doc-comment code);"), keeping main's strikethrough-and-SHIPPED
convention for the modeline entry.

Post-integration gates, from a clean tree: cargo fmt --check; strict
workspace clippy; pmacs-protocol 17; cargo test --lib 1,768; --features
crdt 1,944 (3 ignored each); vterm Stage 1 9/10, Stage 2 4/4, Stage 3
5/7, statusline 7/8, tab-width 2/2 (default/CRDT); M4 121 passed (3
ignored, 1 filtered); required GPU 139; workspace sweep 2,946 passed
across 84 suites (19 ignored), one invocation; git diff --check clean.
2026-07-22 17:39:58 -04:00
Levi Neuwirth 9ff6a62623 fix(vterm): address stage 3 review round 2
One real defect, three cleanups, and a named deferral.

A daemon disconnect in terminal mode hid the disconnect notice. The
Disconnected arm set the placeholder text but never left terminal mode,
where the document code layer is not prepared at all and the terminal glyph
layer keeps painting its last frame — so the user was left looking at a
frozen, live-looking terminal that silently ignored input. GPU auto-reconnect
is a named deferral, so that state persisted until relaunch. State::
on_daemon_disconnected now leaves terminal mode, forces a repaint even when
the notice text is byte-identical, and requests a redraw.

The fix and its test share a file, so scripts/bite's file granularity cannot
bite it; the equivalent was done by hand. Neutralizing only the
exit_terminal_mode() call makes the test fail on the "must leave terminal
mode" assertion; restoring it makes it pass.

sync_semantic_terminal_layout no longer clones the whole visible cell grid to
read one size. It ran every dispatcher tick for any semantic frontend with a
declared terminal; TerminalManager::screen_size reads the value from the
borrowed projection instead.

Inbound terminal events now require a negotiated v19 session. The outbound
TerminalFrame was gated twice while TerminalResize/TerminalPointer relied on
the frontend's send gate alone. A pre-v19 peer cannot construct those
variants, so this only refuses a hand-rolled client — and the a32 forgery
tests already prove such an event reaches nothing but the sender's own
authenticated active view — but the asymmetry was not deliberate.

A terminal-mode press that misses the grid no longer arms a drag, so a later
in-grid motion cannot send a Drag with no preceding Down. Daemon-side impact
was nil; the state is now honest. A release still always ends the drag.

The roadmap and handoff Arc 5 lines still said Stage 3 was framed and
awaiting approval, contradicting this PR's own ledger. Both corrected.

Named deferral: terminal wheel gestures discard scroll magnitude. One winit
wheel event becomes one gesture regardless of the lines it accumulated, while
the document path scrolls by lines. Closing it means either N gestures
(chattier) or a magnitude field on the pointer event — a protocol change.
Neither belongs in this stage.

Gates: fmt; strict workspace clippy; 1,758 default + 1,934 CRDT library
tests; Stage 1 9/10, Stage 2 4/4, Stage 3 5/7, statusline 7/8
(default/CRDT); M4 120; required GPU 129; workspace sweep 2,923 across 83
suites; diff check clean.
2026-07-22 15:33:26 -04:00
Levi Neuwirth 9f7bc77f44 feat(render): unify tab-width projection
Share one fixed eight-column tab-stop contract across core and GPU renderers. Consolidate byte-to-display-column accounting, expand GPU code tabs with source provenance, align caret/hit/decoration geometry, and refresh minimap projection on edits.
2026-07-22 15:03:30 -04:00
Levi Neuwirth 50fd9a08e4 fix(vterm): address stage 3 review round 1
Five findings, all addressed. One was a real defect; one prediction did not
reproduce and is documented as such rather than papered over.

Hover no longer claims durable terminal control (finding 2, the real one).
apply_terminal_gesture claimed the controller before dispatching, including
for Move, which does nothing. A semantic frontend reports motion at pixel
rate, so sweeping the mouse across a passive split's terminal took durable
control, and the next layout sync resized the shared PTY to that background
view's geometry — precisely the theft the controller rule exists to prevent.
Bare motion no longer claims; every deliberate gesture still does.
scripts/bite HEAD src/editor.rs on the new test is a clean behavioral bite.

The terminal-mode presence-sweep skip is removed (finding 1), but the
predicted failure did NOT reproduce. The review reasoned that skipping the
sweep freezes last_broadcast at the abandoned document position. It does
not: the buffer-follow clears the terminal declaration when it ships the
snapshot, so terminal_active is false on the tick a window first shows a
terminal, and the declaration cannot arrive until a later tick — the
frontend learns the buffer id from that very snapshot. One truthful sweep
always lands first. The real-daemon two-frontend test written to catch the
freeze passes against the pre-fix tree; the bite is vacuous and the test is
labelled a regression guard, not fix evidence. The skip goes anyway: it was
load-bearing on tick ordering and bought nothing, and removing it makes
"presence follows the frontend" structural.

Terminal motion is deduplicated by cell (finding 3). Sub-cell motion
resolved to the same coordinate and still crossed the wire, where every
event is a daemon-side gesture. Press and release re-arm the memo so the
first drag after a press still reports. Its unit test cannot bite — the
seam did not exist pre-fix — and says so.

Declarations record only once sent (finding 4).
terminal_declaration_if_changed is now a pure query;
note_terminal_declaration_sent records. A failed write is retried instead of
suppressed as already-declared. The existing a35 test caught the contract
change and now pins both halves.

Unchanged frames skip revalidation (finding 5). The complete-payload
comparison runs before validate; only validated frames are ever stored, so a
frame equal to the baseline has already passed. The chrome tail is factored
into terminal_chrome so both exits emit it identically.

Gates: fmt; strict workspace clippy; 1,757 default + 1,933 CRDT library
tests; Stage 1 9/10, Stage 2 4/4, Stage 3 5/7, statusline 7/8
(default/CRDT); M4 120; required GPU 128; workspace sweep 2,921 across 83
suites; diff check clean.
2026-07-22 14:49:23 -04:00
Levi Neuwirth bdf2b6e4b4 feat(vterm): protocol v19 terminal frames and a native GPU terminal
Vterm Stage 3 — the final vterm stage. A semantic frontend can now host a
terminal: the daemon ships complete validated cell grids, and pmacs-gpu
renders them with fixed-cell geometry, its own input path, and no document
projection at all.

Protocol v19 appends three variants after their enums' final v18 members:
InstanceMessage::TerminalFrame (daemon-gated), and FrontendEvent::
TerminalResize / TerminalPointer (frontend-gated). It is the first bump to
gate in both directions, so criterion 28 pins each filter independently and
byte pins on StatuslineSegments and MenuPointer guard the placements.

pmacs-protocol gains src/terminal.rs: the shared row/column/visible-cell/
grapheme/metadata bounds, TerminalProcessState, TerminalSelectionSpan, and
TerminalFrame::validate — the ONE structural policy the daemon runs before
emission and the frontend runs after decode. src/terminal/* re-exports them
so no duplicate type exists, and unicode-width becomes a workspace dependency
so the screen and the validator measure glyph columns with one table. A new
8 MiB aggregate glyph bound keeps the largest legal frame (measured:
13,437,863 bytes) under the unchanged 16 MiB transport cap rather than
widening every connection's allocation ceiling.

The semantic producer suppresses the whole document family for a terminal
buffer while keeping the status band, theme, font, statusline, menu, and
minibuffer, and compares the complete ordered payload rather than
screen_generation — scroll, selection, and process state all change without
advancing it.

Two things the framing did not spell out, both found by the real-daemon
acceptance:

The Viewport gate keys on the authenticated source's ACTIVE buffer, not the
buffer the message names. Viewport also aligns the window to what it
declares, so a stale document viewport in flight when a command opened a
terminal dragged the frontend straight back off it: the window oscillated,
every terminal declaration was refused, and no frame ever arrived, with
nothing logged anywhere.

The producer clears terminal mode on every exit path. The daemon uses that
flag to suppress CursorByte and the presence sweep, so an early return that
left it set kept both suppressed after the frontend returned to a document.

pmacs-gpu/src/terminal.rs is a pure cell-space paint planner, unit-testable
without a GPU. The renderer builds one shaped buffer per text run, so a wide
or cluster glyph's advance can never choose the next column's origin.

Criterion 37 needed a seam rather than a fixture: pmacs-gpu depends only on
pmacs-protocol, so attach::connect's reader sink was generalized and a
--headless-probe mode added. The acceptance drives a real daemon, a real
/bin/sh child, the real attach client, and real composited pixels in one
path — which is how both defects above were found.

Gates: fmt; strict workspace clippy; 1,757 default + 1,933 CRDT library
tests; vterm Stage 1 9/10, Stage 2 4/4, Stage 3 4/5 acceptance
(default/CRDT); statusline 7/8; M4 120; required GPU 127; workspace sweep
2,919 across 83 suites; diff check clean.
2026-07-22 13:28:35 -04:00
Levi Neuwirth e5db4a98f8 test: enforce locals-query coverage
Assert that every bundled local-sensitive highlight query has a non-empty,
compilable locals query. Clarify the upstream-parity worst-case complexity of
lexical reference resolution in the framing document.
2026-07-22 13:05:33 -04:00
Levi Neuwirth 47ffe5dcff syntax: process bundled locals queries
Compile grammar locals metadata, resolve lexical definitions and references
once per settled layer, and apply local property predicates in both highlight
producers. Restore non-shadowed JavaScript builtins while suppressing local
shadows, with lexical, viewport, render, and edit-freshness regressions.
2026-07-22 12:28:31 -04:00
Levi Neuwirth 3f0252fb97 Merge canonical main into vterm-tui
Integrate mode-system wiring and handoff updates before PR #130 lands.
Preserve per-frontend terminal dispatch while resolving major-mode keymaps,
and expose mode, terminal, and LSP statusline providers together.
2026-07-22 10:28:56 -04:00
Levi Neuwirth b9a7e40855 fix(vterm): harden view anchors and interactive authority
Clamp anchors into partially evicted wrapped lines, require authenticated
interactive origins, and avoid per-mouse cell snapshots. Restore dispatcher
rationale, named context errors, and focused regressions for the corrected
contracts.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-22 09:09:44 -04:00
Levi Neuwirth 8702791de9 fix(vterm): honor terminal escape and view contracts
Require the fixed C-c escape before editor-local terminal bindings, reject context-implicit Lua operations from document windows, and make controller replacement atomic per frontend. Borrow screen rows during view projection instead of deep-cloning retained history, preserve view anchors through zero-area layouts, and remove redundant detach paths.

Add focused child-input coverage for unescaped bound keys and C-c C-c, plus controller, zero-area, context-error, and clone-free projection assertions.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-22 08:28:18 -04:00
Levi Neuwirth 4c382ae797 fix: harden mode acceptance startup
Give the daemon its normal five-second handshake window before switching
the mode-system acceptance client to short frame polling. Document reload
and session-persistence boundaries and correct stale describe-key guidance.
2026-07-22 08:18:18 -04:00
Levi Neuwirth da8f6aeae4 fix(vterm): harden integrated Stage 2 behavior
Resolve post-main integration drift in authenticated routing, terminal view projection, Lua installation, and inherited acceptance callers. Preserve the terminal statusline provider alongside the landed Themes provider and record the final Stage 2 gate evidence.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-21 21:38:08 -04:00
Levi Neuwirth 99cd7ec240 feat: wire major modes through key dispatch
Store a detected major mode on each buffer and expose it through Lua.
Resolve mode-scoped bindings in dispatch, describe-key, and help links,
including exact encoded mode context after entering the help buffer.

Initialize modes once at buffer load, preserve explicit overrides and
clears across switches, and publish the mode through a per-window
statusline provider. Add daemon acceptance for the complete mode lifecycle.
2026-07-21 20:25:48 -04:00
Levi Neuwirth 0ddff24589 Merge canonical main into vterm-tui
Integrate config-registry and handoff updates landed after the Stage 2
framing branch was cut.
2026-07-21 20:18:53 -04:00
Levi Neuwirth dc9225778a test(vterm): prove Stage 2 TUI integration
Add cross-surface Lua, shared-view, clipboard, authenticated routing,
BEL, resize, and real-host PTY acceptance. Ensure terminal-local keymaps
run before raw child transport and document the criterion-to-test map.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 20:16:23 -04:00
Levi Neuwirth 0dacac7e8d feat(vterm): compose terminal views in the TUI
Render exact per-frontend terminal snapshots inside split-window content rects,
route terminal key, paste, focus, and mouse input through durable controllers,
and preserve logical row anchors across reflow, selection, and scrollback.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 19:21:50 -04:00
Levi Neuwirth 0a846d91ce feat(vterm): bind exact frontend terminal contexts
Resolve terminal commands and buffer switches against the authenticated invoking
frontend, publish copied selections to that frontend, and wire daemon rendering,
resize, focus, paste, bell, and detach lifecycle through exact view identities.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 19:20:00 -04:00
Levi Neuwirth 7c3953563c feat(vterm): add strict Lua and daemon foundations
Install strict owned terminal specification parsing, fresh global state tables,
default-name uniquification, durable view/controller lifecycle, and the builtin
terminal command/statusline surface. Route daemon key and mouse input by the
authenticated source and add non-replaying per-frontend BEL baselines.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 19:10:45 -04:00
Levi Neuwirth 39e07cbf9b feat(vterm): expose atomic screen projections
Publish retained row metadata, alternate-screen identity, cursor, title, and
cells through the same synchronized-output generation gate. This gives each
Stage 2 view one coherent projection without borrowing or mirroring mutable
screen state.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 19:06:14 -04:00
Levi Neuwirth 1b32f81033 feat(vterm): establish Stage 2 view authority contracts
Add exact frontend/window/buffer view identities, logical-cell selections,
durable controller ownership, lifecycle cleanup, default-name uniquification,
and a nested authenticated interactive-command origin shared with Lua. These
contracts let the terminal core and TUI lanes proceed without inventing
parallel state.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 18:56:15 -04:00
Levi Neuwirth f86c966090 fix(config): reject wrongly-typed spec fields; make trim-on-save buffer-aware
Review round 1, findings 2-4 plus doc notes. Finding 1 landed in fd80bcb.

Finding 3 --- spec fields meaningless for the declared type are now
rejected. DEFINE_SPEC_FIELDS whitelists all nine keys for every type and
the kind parser only reads its own arm's fields, so
`{ type = "string", choices = {...} }` silently defined a string that
accepts anything (the author meant enum) and `min` on a boolean was
dropped. These are typo-shaped bugs the R50 whitelist structurally
cannot see: the key is spelled correctly, it is on the wrong type.
`check_fields_relevant_to_kind` closes it with a pointed error naming
the misplaced field, and a companion test pins that each field is still
accepted where it belongs, including `min`/`max` on number as well as
integer.

Finding 4 --- the after_buffer_removed purge had no end-to-end test.
Every existing test called ConfigRegistry::remove_buffer directly, so
deleting the three lines wired into mod.rs would have left the whole
suite green. The new acceptance test kills a buffer through
pmacs.buffer.remove (the real remove_buffer_and_fire route) and asserts
the locals are gone; bite-verified by removing the hunk and watching it
fail.

Finding 2 (the half with a natural buffer) --- editing.trim-on-save is
now resolved against the buffer being saved rather than the global
chain. Reading globally meant set_local was accepted, stored, and
reported by describe, then never consulted: a pin the user believes in
that does nothing, which is the shape F1 exists to prevent. Two tests,
one for the override and one for the global fallback the change could
have broken; the override test fails against the old global read.

Both new save tests initially passed VACUOUSLY and were rewritten:
pmacs.editor.save() is the raw save, while buffer.before-save fires
inside the buffer.save COMMAND (default.lua:224), and save() no-ops on
an unmodified buffer --- so the original form asserted on a file that
was never rewritten. They now insert content to dirty the buffer and go
through pmacs.command.invoke("buffer.save").

The other half of finding 2 --- a per-buffer autosave.interval-ms is
semantically meaningless yet still accepted --- is recorded as a named
deferral proposing a define-time `scope = "global"` flag, alongside
deferrals for bound-parse field naming and StartupOnly reset symmetry.
Also recorded: interval_ms(1e30) now raises instead of storing a
nonsense float, an improvement but a real divergence from "the wrapper's
shape stays exactly as it was".

Doc: the module header cited framing revision 2; the shipped doc is
revision 3, whose corrections are what the code implements.

Gates: fmt, clippy -D warnings, --lib (1691), --lib --features crdt
(1865), lua54 backend, config_registry_acceptance (16), editops (72),
autosave (29), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), and the full
workspace sweep (2806 tests, exit 0). git diff --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 18:29:18 -04:00
Levi Neuwirth 82ae2fb5ca fix(config): reject the saturating i64 upper boundary in int_from_f64
Review round 1, finding 1. `i64::MAX as f64` rounds UP to 2^63 =
9223372036854775808.0, one greater than `i64::MAX`. The guard used `>`,
so a value of exactly 2^63 passed validation and `v as i64` then
saturated it to 9223372036854775807 --- the registry silently stored a
different number than the caller wrote.

The bounds are asymmetric on purpose, and the fix must not be applied to
both ends: `i64::MIN as f64` IS exactly -2^63 and round-trips, so the
lower comparison stays `<`. Tightening it in sympathy would wrongly
reject a legitimate value.

Three tests: the boundary is rejected (fails against the `>` form,
bite-verified), `i64::MIN` is still accepted, and the largest
representable integer below the boundary (2^63 - 1024) still converts.

Also from review round 1, finding 2, which does NOT reproduce: the
concern was that because `set_local` never calls
`maybe_freeze_after_init`, a `set_local` as the first post-init
operation would defer the lazy freeze and let a later `StartupOnly`
write slip through. It cannot --- `set` and `reset` call
`maybe_freeze_after_init` as their first statement, ahead of the
definition lookup and the mutator, so the freeze always lands before the
check within the same call. No code change; the ordering is now pinned
by a test that drives exactly that sequence (post-init `set_local` on a
Live key, then a `StartupOnly` write) and asserts the write is still
refused and that the refused set is itself what triggered the freeze.

Gates: fmt, clippy -D warnings, --lib (1687), --lib --features crdt
(1861), lua54 backend, config_registry_acceptance (13), and the full
workspace sweep (2799 tests, exit 0). git diff --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 18:29:18 -04:00
Levi Neuwirth 6844262495 feat(config): typed configuration registry with buffer-local scope
A third registry beside CommandRegistry and HookRegistry, per
docs/config-registry-framing.md. Unblocks the per-buffer auto-pair
toggle, the first of the five backlog items the missing config surface
was gating.

Substrate (src/config_registry.rs):

  * ConfigRegistry keyed by name with definition order preserved, R42
    mandatory descriptions, R50 typo detection, duplicate rejection,
    and SourceLocation provenance -- the command/hook vocabulary.
  * Closed scalar kinds: boolean, integer, number, string, enum. Owned
    Rust values; Lua tables, functions and userdata are never stored.
    Integer exactness is checked by value, never math.type, so the
    luajit and lua54 builds agree.
  * Two scopes. get(name, buf) resolves buffer-local -> global ->
    default; get(name) with no buffer resolves the global chain only
    and never consults an ambient buffer. Buffer-locals live in a
    registry-owned side table purged at after_buffer_removed, beside
    the keymap purge already there.
  * An override is ALWAYS stored, even when equal to the value it
    shadows; only value_epoch and listener dispatch key on effective
    change. Without this a buffer pinned to the current value stores
    nothing and a later global set flips it -- the pin silently never
    existed. equal_valued_local_override_is_still_stored_and_shields_buffer
    fails against the naive reading.

Bindings (src/lua_bindings/config.rs):

  * define/get/set/set_local/reset/is_set/describe/list/on_change.
    Spec tables are read raw, so neither an unknown key nor a
    metatable-provided value can smuggle a field in.
  * Listeners commit inside the borrow, snapshot, drop the borrow, and
    only then re-enter Lua -- verified by holding the borrow and
    watching the test panic with "RefCell already borrowed". A raising
    listener is logged without blocking later ones or rolling back, and
    a depth bound turns an accidental cycle into a pointed error.
    Listeners persist until explicitly disposed; there is no Gc path,
    matching the rest of the codebase.
  * StartupOnly freezes off the existing InitCompleteFlag at write
    time, so this arc adds no editor.rs call at all.

Adopters, each defining its own key so SourceLocation names the owning
module: editing.auto-pair (pair.lua, read per-buffer against the typed
edit's SOURCE buffer), editing.trim-on-save (editops.lua),
autosave.interval-ms (autosave.lua). No public function is removed or
deprecated, and both migration wrappers keep their legacy coercion --
trim_on_save("yes") still enables, interval_ms(1500.7) still floors to
1500 -- coercing before handing the strict registry a conforming value.

M-x describe-setting renders into *help*, modeled on describe-command.

Framing revision 3 records four defects implementation found in the
document itself: acceptance 30 and 31 contradicted each other; the
planned builtin/runtime/config.lua had nothing to hold and would have
broken the source-location contract had it held the one helper it might
have; F5 asked define to police a call it cannot see, moved to
set_local; and list() ordering was underspecified.

No protocol change; SUPPORTED stays [6..18]. No wire surface. Zero
changes to src/editor.rs.

Gates: fmt, clippy -D warnings, --lib (1683), --lib --features crdt
(1857), the new config_registry_acceptance (13) plus auto_pair (45),
editops (72), autosave (29) and m9_6 (25), m4 --skip basedpyright
(114), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), the lua54 backend build,
and the full workspace sweep (2795 tests, exit 0). git diff --check
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 18:29:18 -04:00
Levi Neuwirth 9797adaa0b fix(vterm): harden terminal cell and input invariants
Reject C0/C1 controls before terminal text reaches screen cells, and preserve
the released button code in SGR mouse reports.

Remove dead screen branches, keep logical-line allocation saturating, and clear
round-trip input state when pruning externally removed terminal buffers.
2026-07-21 16:27:43 -04:00
Levi Neuwirth bf972a76fa fix(vterm): share line-feed screen dispatch
Merge the identical LF and IND match arms so the post-review workspace Clippy
gate remains warning-free without changing terminal behavior.
2026-07-21 15:35:25 -04:00
Levi Neuwirth 28f2e6ce66 fix(vterm): parse VT index operations
Dispatch ESC D, ESC E, and ESC M as typed full-screen IND, NEL, and RI
operations while preserving the line-oriented parser contract.

Extend both the every-split parser matrix and parser-to-screen integration test
so missing dispatch fails behaviorally rather than only at compile time.
2026-07-21 15:31:15 -04:00
Levi Neuwirth f0a235f635 fix(vterm): harden Stage 1 terminal contracts
Add typed IND, NEL, and RI operations with exact screen semantics, preserve
application tab stops across resize, and default terminal children to the
supported xterm-256color capability set.

Make shutdown liveness acceptance portable with kill(pid, 0), and document the
public TerminalScreen methods consumed by later stages.
2026-07-21 15:30:06 -04:00
Levi Neuwirth 962944b744 fix(vterm): normalize Darwin PTY signal names
portable-pty forwards libc strsignal text, which includes a numeric suffix on
Darwin. Strip a verified numeric suffix before mapping known descriptions so
terminal snapshots and exit annotations retain the protocol's symbolic
SIGFOO form across platforms.

Add focused coverage for glibc, Darwin, and unknown descriptions.
2026-07-21 14:37:54 -04:00
Levi Neuwirth bbc1f33a7c feat(vterm): add Stage 1 terminal core
Add compatibility-preserving full-screen ANSI operations, the bounded terminal
screen and input encoders, and a transactional TerminalManager owning one
read-only identity buffer, PTY process, and screen per session.

Drain terminal-owned process events before process.after-tick, retain exact
final output and PID/outcome annotations, reap killed buffers and shutdown
children safely, and enforce buffer-owned read-only checks across ordinary,
host, undo/redo, and CRDT mutation paths.

Cover split parser and grapheme boundaries, screen/reflow/history invariants,
device responses, lifecycle cleanup, and a real adversarial alternate-screen
PTY. Record the fully gated Stage 1 delivery and downstream TUI/GPU contracts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 14:20:28 -04:00
Levi Neuwirth 1be7a30468 fix(statusline): harden narrow-band review edges
Document and pin the GPU built-in-only narrow-band clipping policy,
including the intentional ability of a wide right group to hide the
left identity. Guard the fixed UI face ordering used by binary search,
preserve flattened provider tracebacks in *errors*, and rename the
phase-one unavailable reason to cover missing layout contexts.

Refresh the implementation verification record after the full gate
suite.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 12:43:22 -04:00
Levi Neuwirth 4b65b9e1e5 feat(statusline): add composable modeline segments at protocol v18
Add the strict pmacs.statusline provider registry, deterministic
borrow-released per-window evaluation, context-scoped failure latches,
and a pure built-in LSP provider.

Preserve the legacy TUI modeline while composing faced custom runs,
and append authoritative complete StatuslineSegments replacements for
semantic frontends. Expand dynamic ThemeFacts, reset producer/frontend
baselines symmetrically, and gate all provider work off protocol v18.

Teach the GPU to atomically validate, resolve, shape, clip, and cache
custom modeline runs without displacing the protected status suffix.
Document the public Lua lifecycle, wire ownership, snapshot semantics,
and the fully gated Arc 4 stage-3 delivery state.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 12:01:25 -04:00
Levi Neuwirth 19ad5cc8ac fix(json-yaml): checkpoint reviewed LSP configuration fixes
Preserve PR #123's unpushed review fixes on a transfer branch: initial
didChangeConfiguration delivery, explicit JSON validation, the pinned
JSON server provider, corrected YAML configuration sections, and
deterministic plus real-provider acceptance coverage. Record the
observed yaml-language-server 1.24.0 standalone smoke and leave the
real YAML-through-pmacs test, rebase, and full gates explicitly pending
for the destination machine.
2026-07-20 16:49:37 -04:00
Levi Neuwirth 9ce6f1abf3 feat(json-yaml): JSON + YAML grammars and language servers
Add tree-sitter-json (0.24) and tree-sitter-yaml (0.7) to
BUILTIN_LANGUAGES (both ABI-current via tree-sitter-language, verified
compiling under tree-sitter 0.26), each self-contained highlights, no
injections of their own. Extensions json=.json, yaml=.yaml/.yml; root
kinds json `document`, yaml `stream`.

The payoff from the #122 injection engine is free: the markdown block
injection query already sets injection.language "yaml" for `---`
frontmatter (minus_metadata) and "toml" for `+++` (plus_metadata), so
registering yaml lights up YAML frontmatter highlighting with no extra
wiring, and ```json / ```yaml / ```yml fences resolve through the engine
(yml->yaml alias already present). Two acceptance tests pin this synergy.

LSP (builtin/runtime/lsp.lua): pmacs.lsp.config.json uses the maintained
extracted-bundle binary `vscode-json-language-server --stdio` (NOT the
stale standalone vscode-json-languageserver); MIT, no telemetry, remote
$schema fetch left enabled (no handledSchemaProtocols). pmacs.lsp.config
.yaml uses `yaml-language-server --stdio` with Red Hat telemetry
disabled by default. Both ship the exact workspace/configuration sections
each server pulls (json+http; yaml+http+redhat.telemetry) present-not-null
so the servers get defaults rather than erroring — the CMake #117 lesson.
Sections derived from server source/docs (neither binary installed on
this build machine to observe live; verify where present). Filetype
fallback entries added. JSON is the standing prerequisite for the Jupyter
.ipynb arc; handoff §6 updated.

Nine acceptance tests (grammar ABI, highlights compile, detection,
grammar<->LSP-key alignment, the two frontmatter/fence synergy proofs,
and the pinned LSP-config sections).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
2026-07-20 16:49:37 -04:00
Levi Neuwirth 6cd78f0870 test(font): wire acceptance suite + protocol/design docs (items 2-8, 15)
tests/gpu_font_acceptance.rs (items 2-7; the header manifest routes
item 1 to src/protocol.rs pins, item 8 to the src/frontend.rs TUI
drop unit, items 9-14/16-19 to pmacs-gpu's headless suite, and item
15 to the docs):

- 2: a fresh attachment's first frame carries the REAL (None, None)
     default; unchanged ticks are silent; a late joiner receives the
     current preference without post-attach mutation
- 3: a mid-session set_font emits exactly one FontFacts on the next
     frame; an identical re-set advances the epoch without emitting
- 4 (producer half): on_buffer_snapshot_sent re-ships buffer facts
     but never the bufferless FontFacts
- 5: real-daemon probe -- a v17 semantic session receives FontFacts,
     a v16 peer never does (crdt feature)
- 6: the strict Lua contract -- 5.999/72.01/0/-16/NaN/inf/non-number
     sizes error naming `size`; empty/non-string family errors
     naming `family`; unknown keys rejected by name; hostile
     __index/__pairs metatables never invoked and never inject
     values; quantization pins 15.994->1599 / 15.996->1600 and both
     boundaries; set_font {} resets both axes; the getter returns a
     fresh quantized plain table; every rejected shape leaves the
     preference and the wire untouched
- 7: a load_user_config_at fixture's init.lua set_font lands in the
     handle installed before user config, and a pre-attach
     preference ships on the first frame

Bites (mutate, observe the acceptance test fail, restore): the
Option-seeded first-frame send, the payload compare, the snapshot
survival of the font baselines, the v17 gate (BOTH halves widened --
producer for_peer alone leaks nothing because the daemon skip arm
still filters; the wire test only fails when belt AND braces are
cut), the unknown-key rejection, the range-check-original ordering,
and the raw_get metatable isolation.

Docs (item 15): docs/semantic-frontend-protocol.md gains the
FontFacts variant entry (v17 gate, authoritative default, no-pixels
preference relay, fail-closed receiver) and folds FontFacts into the
BufferSnapshot reset-contract paragraph (bufferless facts survive;
the frontend's caret-follow scroll residual is the buffer-scoped
part). docs/pmacs-gpu-design.md's "future customization needs no
wire-protocol changes" claim is corrected in place and points at the
framing as design of record; the Lua-override bullet now names the
landed set_font shape. The framing status bumps to implemented.

Also re-homes the ProcessSupervisor doc comment that the make_font_pref
insertion had orphaned onto the wrong function.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-18 11:46:44 +01:00
Levi Neuwirth 661b4968d9 feat(font): FontFacts wire + daemon half (v17, FontPref, producer)
Protocol v16->17: InstanceMessage::FontFacts { family,
size_centi_px } appended after ThemeFacts (integer hundredths of a
logical pixel -- the enum derives Eq, f32 cannot; range 600..=7200
documented on the wire). Pins updated: version 17, ladder accepts
6..=17 rejects 18, FontFacts round-trip (populated + all-None), and
a ThemeFacts byte pin ([23, 0]) guarding the appended placement.

Daemon half: FontPref { family, size_centi_px, epoch } behind a
shared handle on EditorState, installed with the new pmacs.gpu Lua
module BEFORE load_user_config so init.lua set_font lands in the
state the first attachment reads. set_font is strict plain data
(raw_get, unknown raw keys rejected by name, metatables never
consulted, parse/validate/quantize fully before locking -- range-
check the ORIGINAL value so 5.999 errors, then nearest-hundredth
round); pmacs.gpu.font() returns a fresh quantized table.

Producer: font_facts_msg (the theme_facts_msg discipline --
Option-seeded epoch + payload baselines, advance on computation,
one authoritative send per attachment incl (None, None),
bufferless so on_buffer_snapshot_sent never touches it); for_peer
gains peer_knows_font_facts (>= 17); daemon write-loop skip arm;
TUI silent-drop arm + regression test; first-frame count test now
expects 6 messages. GPU application follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-18 11:45:51 +01:00
Levi Neuwirth c3a57be3fa fix(injections): pin sibling priority regression 2026-07-15 13:24:37 +01:00
Levi Neuwirth a231257894 chore(injections): rebase onto main (#120 themes stage 1 / v16)
Rebase the injection branch onto main after #120 (themes stage 1,
protocol v16) merged. Conflicts were confined to appended test blocks
in pmacs-gpu/src/main.rs (kept both #120's GPU face tests and the
injection source_color test); highlight.rs, lua_bindings/mod.rs, and
semantic_render.rs auto-merged (my injection code and #120's theme
epochs / resolve_faces summary path / ThemeFacts occupy disjoint
regions). The one API adaptation: #120 gave scoped_file_summary a
`resolve_faces: bool` parameter, so the full-buffer flatten perf gate
passes `false` (it exercises the syntax flatten path).

Gates re-run on the rebased base: fmt, clippy -D warnings, lib (1582),
lib crdt (1756), m4 (110), theme_faces (22, #120's suite unbroken),
PMACS_REQUIRE_GPU gpu (70), full workspace sweep (0 failures),
git diff --check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
2026-07-15 12:17:27 +01:00