Add tree-sitter HTML and CSS grammars for .html/.htm/.xhtml and .css
highlighting, and light up HTML's <script>->javascript / <style>->css
injections (the north-star injection consumer). Both crates export their query
constants over the tree-sitter-language shim (no overlay, ABI-fine). The only
reconciliation is two new capture entries (tag, attribute) in highlight.rs.
Framing approved through three review rounds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advance the volatile ledger to the current canonical base and record the
portable Revision 1 framing checkpoint, scope, recovery command, and approval
boundary.
Round 2 correctly found the Finding-2/3 fixes were unpinned (reverting
them left the suite green). Both are now bite-verified:
- **Kill-path purge (Finding 2).** Replaced the direct
`forget_buffer(id)` unit test with
`killing_a_buffer_through_the_real_path_purges_its_fold_store`, which
drives `pmacs.buffer.remove` — the production route through
`after_buffer_removed` — and asserts the store is gone via the dead id
(BufferIds never recycle). Mirrors config_registry's real-kill-path
test. Bite-verified: reverting the `after_buffer_removed` fold branch
turns it red.
- **close-all point move (Finding 3).** Added
`close_all_command_moves_point_to_enclosing_head`, which invokes the
`fold.close-all` command with the point inside the second of two
top-level fns and asserts the cursor landed on that fn's head-line
content end (and both folds exist). Bite-verified: reverting close_all's
`maybe_move_point` loop turns it red.
- Ledger: `docs/active-work.md` folding lane now records PR #142 OPEN +
the two landed review rounds (was "opens once the gate suite is green").
Correction to the round-1 gate report: the acceptance suite is **21**
tests (round 1 was 20, not 24 — a tally slip), green under default and
`--features crdt`. Full gate suite otherwise green (fmt, clippy
--workspace --all-targets, git diff --check).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
The parent-arc framing was authored on the desktop but never tracked, so
references to it — from the merged latex-grammar lane framing and its Cargo.toml
comment (#144) — dangled on a fresh clone. Commit it as its own docs change and
drop the now-stale "untracked, desktop-only" caveat from the lane framing.
Frames the full four-tier inline-math renderer (detection -> parse -> layout ->
GPU render). Stage 1 (LaTeX grammar) shipped as #144; the math parser (Tier 2),
layout (Tier 3), GPU render (Tier 4), and (math_environment) @math injection are
deferred, tracked in docs/latex-grammar-math-substrate-framing.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- **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
Advance the durable snapshot to merge 63fbc66, record the green CI rerun, and
remove the completed GPU invocation lane from the volatile active-work ledger.
The Cargo.toml comment and the lane framing both cited docs/inline-math-framing.md,
which is an untracked, desktop-only doc — the path dangles on a fresh clone.
Point the Cargo.toml comment at the committed lane framing instead, and note the
parent's untracked status in the framing header (committing it as its own docs
PR, or listing it in the handoff's machine-local inventory, remains a tracked
follow-up).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Advance the framing to Revision 6 and record the strengthened non-CRDT,
Ctrl-C, strict-operand, probe-throttling, and PID-cleanup contracts. Update the
durable and volatile checkpoints to implementation commit 154cb9f.
Throttle the managed probe after its event channel closes, reject option-like
path operands, and document the connector test seam. Strengthen non-CRDT and
Ctrl-C acceptance so socket side effects and a pre-signal surviving frontend
are exercised, while avoiding cleanup signals to already-reaped daemon PIDs.
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
Advance the active and durable checkpoints to 69825d0 and clarify that every
spawned managed daemon enters the named reaper before connection or handshake
work can fail.
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>
Transfer every successfully spawned daemon child to the named reaper before
any connection or handshake step can fail. Read early exit status from shared
reaper facts, eliminating fallible child-handle paths that could return without
reaping.
Advance the framing to Revision 5 and record the reviewed lifecycle, CLI, and
acceptance contracts. Update the durable and volatile handoffs with checkpoint
82355ca and the completed verification matrix.
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.
Source: nvim-treesitter/nvim-treesitter, queries/latex/highlights.scm
(branch master, retrieved 2026-07-23). License: Apache-2.0, compatible with
pmacs' MIT OR Apache-2.0.
Committed byte-for-byte before any reconciliation so the follow-up commit's
diff shows exactly which captures/predicates were curated onto pmacs' recognized
set. This is the first in-repo grammar-query overlay (per framing Q#LX2); the
audit-rules.scm include_str! path is the precedent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carve the frontend-agnostic, conflict-free substrate out of the parent
inline-math arc: Stage 1 bundles a LaTeX/TeX grammar for .tex/.latex/.sty/.cls
highlighting; the math parser (Tier 2) and GPU render (Tier 4) are deferred to
the inline-math arc. Framing approved through three review rounds; provenance
diff of the chosen grammar crate against upstream latex-lsp is discharged
(grammar.js/scanner.c byte-identical).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update the durable handoff and volatile active-work ledger for open PR #141, including the implementation checkpoint, accepted architecture, verification record, visible Wayland/Vulkan smoke, and cross-machine recovery commands.
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.
Q#FD4 settled: the user chose Emacs hideshow parity, so Stage 1 ships the
`C-c @` prefix set (`C-c <letter>` is fully taken by the LSP surface; the
hs-minor-mode prefix collides with nothing). §6/§9 now list the five
bindings; §0 records the rev 4 -> rev 5 approval note; §14 records the
rebase onto canonical `main` @ 96d0bae at implementation start. Bet B1
accepted as framed. active-work.md folding lane flipped to APPROVED /
Stage 1 implementing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
One major, three minors, and a nit from the third review, all fixed:
R3-1 (major, derived head line): rev 3's head-selection ascend was not a
no-op for brace languages — rustfmt wraps long signatures
(fn foo( / a: u32, / ) -> bool {) and puts { on its own line under where
clauses, so block.start_line > parent.start_line, the ascend fired, and
the fold hid the wrapped signature: the R2-5 defect class one level up.
Replaced by a derived head line — the interior comes from the body node
alone (closer-aware tail unchanged) and the head is the line immediately
above the first hidden line (B.start_line - 1 for an introduced
delimiter-less body, B.start_line otherwise). Emacs hideshow / LSP
foldingRange parity: the fold hides the body, nothing else. The
introducer<->body association survives for matching and close-all only.
Acceptance 1 gains wrapped-signature cases in both grammar shapes.
R3-2: "innermost-first" on a shared head line made the outer fold
unreachable via fold.toggle (close inner, reopen inner, forever) and
allowed zero-visible-change presses. Replaced by state-aware ordering:
close acts on the innermost open fold, open on the outermost closed
fold, toggle cycles org-TAB-style (close inward-out, then open all).
Acceptance 9 updated.
R3-3: Stage 1's "command path" is dispatch_key self-insert/delete only;
interactive Lua commands (yank, query-replace, comment-toggle) mutate
through the Lua mutator path and classify programmatic, so their edits
land inside a fold without unfolding. Stated as the intended Stage 1
line; widening the classifier to interactive Lua command contexts is a
named Stage 2 obligation beside Stage 3's CRDT-origin unfold.
R3-4: the data API's normalization of an arbitrary range is now defined
(head = line containing start; hidden = full lines strictly after it
through the line containing end, exclusive of an end at a line start).
Nit: stored-range containment pinned start-exclusive/end-inclusive with
the matching View boundary bias, so typing at the end of a head line
neither unfolds nor lands hidden; acceptance 6 asserts it.
Also: Sec 14 records that canonical main has advanced past the cac4961
base (docs + tab-width #137, no Stage 1 overlap; rebase at
implementation start), and the active-work folding lane is brought
current (head was stale at rev 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five majors + four minors from the second review, all fixed:
R2-1 (head line on indentation grammars): tree-sitter-python's block
starts on the first statement line, so the old heuristic made a body line
the fold head and left def foo(): headless. Added a head-selection ascend
rule (block-like node -> ascend to the introducer parent while
parent.start_line < block.start_line), a no-op for brace languages.
Acceptance 1 now tests both Rust and Python.
R2-2 (Q#FD5 cannot live in the View): View::on_edit(&Buffer,&Edit) and
Edit carry no source frontend and no point-inside signal (only crdt_op),
verified at overlay.rs:248 / rope.rs. The store View now does translate +
drop only; the interactive unfold is a pre-edit step at the dispatch layer
that holds the authenticated frontend and its point.
R2-3 (CRDT typing misclassified): the classifier is the authenticated
source frontend's point, not the transport — a GPU user's CRDT-op insert
inside a fold is interactive. Stage 1 implements the command path;
CRDT-origin unfold is a named Stage 3 obligation.
R2-4 (#120 stale-mirror trap): revert drops the store + emits
BufferSnapshot + resets the baseline, so the empty store is suppressed as
'initial empty' and the GPU keeps stale folds unless its snapshot arm
clears the fold mirror. Pinned as a Stage 3 obligation and in acceptance 7.
R2-5 (line-aligned tail hid non-member text): } else { / }, [deps]) —
now the closing-delimiter line stays visible (closer-aware tail);
delimiter-less nodes still hide through the last body line. Decided, not
bet.
Minors: unfold is plural (nested); shared head lines toggle
innermost-first; Q#FD9's reason corrected to the >=1-hidden-line rule (not
bounds); and the Stage 2/3 sketch now names fold-aware LineNumbers,
visible-line viewport/scroll accounting, and hidden-line sign/presence
clamp-or-drop.
Seven findings from the first review, all fixed:
F1 (architectural): the grid TUI is daemon-rendered and never receives
FoldState (advertises semantic_render:false), so its fold collapse is
instance-side work in the daemon grid renderer reading the store directly
— the vterm Stage 2 shape. FoldState on the wire serves only semantic
(GPU) sessions. Staging reworked: Stage 2 = grid/daemon rendering, Stage
3 = wire-fed GPU.
F2: stored range pinned to the line-aligned hidden interior (head line
visible, closing-delimiter line hidden); one normalized form everywhere,
resolving the R1 5/7 contradiction.
F3: the store's edit-translation is the instance-side buffer-attached View
(BufferStyleSpanTranslator pattern, overlay.rs:235), which sees every real
edit — not the frontend-side translate_byte_range. Split the two resets:
per-session producer baseline vs per-buffer store lifecycle on content
replacement.
F4: stale-tree fold creation refuses with a message when
ParseViewHandle::current() is None or pending_edit_count() > 0.
F5: multi-frontend point + edit-vs-fold pinned — invoking point moves to
the head; interactive-point-inside edits unfold, programmatic/remote edits
translate; the no-cursor-inside invariant is creation-time-only in Stage 1.
F6: Lua data API takes an explicit buffer (no ambient resolution, per
#127) with full range validation, which is also what makes terminals
never fold.
F7: FoldState follows the authoritative-empty discipline; open-all emits
exactly one empty frame; the flipped pin test keeps asserting
BlockAdornments is never emitted.
Minors: source lines not display rows; close-all folds top-level only
(hs-hide-all parity); an injected-layer (markdown fence) acceptance added;
and an explicit note that FoldState needs no protocol bump.
The ledger inherited from #135's merge still listed Vterm Stage 3 as an
open 'never merge without authorization' implementation lane; it is merged
(main @ cac4961), so per the update protocol it moves to Closed and Arc 5's
terminal stage is marked complete. Adds the folding framing lane (branch
folding, off cac4961, framing-only) and points the canonical base at
cac4961 / protocol v19.
Draft framing for the folding arc, committed to the `folding` branch for
review before any implementation. Branch is cut from canonical main @
cac4961 (post Vterm Stage 3 #135).
The load-bearing finding: the bundled tree-sitter grammars ship no fold
query and no folds.scm — they export HIGHLIGHTS/INJECTIONS/LOCALS/TAGS
only. The roadmap's "tree-sitter fold ranges" premise is therefore not
free, so the fold source is a real decision (Q#FD1). The draft recommends
structural node folding (fold the nearest enclosing block-like node
spanning >= 2 rows), which reuses the existing parse trees for every
grammar and injection layer with zero per-language authoring; indentation
folding (grammarless fallback) and curated per-language queries (quality
pass) are deferred.
FoldState already exists in the protocol, declared but unproduced, with a
test pinning that it is never emitted; no frontend consumes it; gutter
markers are frontend-derived like the diagnostic sign bars, so no new wire
type is needed. Staged like vterm: Stage 1 engine (instance-side fold
model + structural source + Lua commands + FoldState production, headless),
Stage 2 TUI collapse+gutter, Stage 3 GPU at parity.
Numbered decisions Q#FD1-9, three falsifiable bets, named deferrals, and a
Stage 1 acceptance list. Awaiting review rounds; bindings (Q#FD4) and the
block-kind heuristic (Bet B1) are the two calls flagged for the user.
Record the approved managed-launch contract, daemon lifecycle and process-group rules, strict GPU CLI behavior, headless acceptance seam, and the complete acceptance matrix before implementation.
Update the Status and Highlights sections through protocol v19, covering the terminal, themes, statusline, mode, config, modeline, syntax, and tab-width work shipped since the previous snapshot.
Advance canonical state to protocol v19, close the Vterm lane and roadmap arc, preserve the cross-PR integration lesson, mark PR #91 landed, and point machine-local housekeeping at the durable policy.
vterm-stage3-framing (superseded; content carried on vterm-gpu) and
tab-width-parity (merged via #137) were deleted with authorization —
worktree + local ref + githubsucks ref, origin tracking pruned. The
-framing branches for each are kept. Retires the now-dangling
superseded-lane recovery entry.
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.
PR #137 (tab-width-parity) is approved and frozen at 5b23e11. Neither
lane copies from or merges the other; whichever lands second rebases
onto the canonical resulting main and reruns the complete gate suite.
The overlap is pmacs-gpu/src/main.rs, pmacs-protocol/src/lib.rs,
Cargo.lock, and the two ledger docs. The lock and docs are mechanical;
the two source files are not — both PRs edit the GPU renderer's
measurement path and widen the protocol crate's export surface in the
same region, so a conflict-free apply is not evidence of a correct
merge.
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.
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.
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.