Commit Graph

29 Commits

Author SHA1 Message Date
Levi Neuwirth 40ebdd8d7e feat(gpu): relative + hybrid line numbers over protocol v14 (sub-arc 3, GPU half)
Carry the line-number mode to the GPU so it renders relative/hybrid, not
just on/off. The v13 wire carried `LineNumbers { enabled: bool }`
(off/absolute only); v14 carries the full mode.

- Protocol: `LineNumberMode {Off, Absolute, Relative, Hybrid}` moves into
  pmacs-protocol (with `number_for`/`is_on`) so the wire, daemon, and both
  frontends share ONE enum and ONE number rule (Q#UX7); `pmacs` re-exports
  it as `crate:🪟:LineNumberMode`. `LineNumbers.enabled: bool` →
  `mode: LineNumberMode`. PROTOCOL_VERSION 13 → 14, SUPPORTED → [6..14],
  daemon-gated `< 14` (a v13 peer gets no LineNumbers, like the v10
  SearchPrompt bump).
- Producer (`line_numbers_msg`): ships the window's mode (cached-suppress
  on the mode now, seeded to Off).
- GPU: `line_numbers` field becomes the mode; `refresh_gutter_buffer`
  computes each number via `mode.number_for(line, cursor_line)` against the
  GPU's own cursor line (`cursor_line()` off `current_line_starts`). The
  buffer rebuilds every render, so relative numbers track the cursor for
  free. Gutter width unchanged (sized by line count → stable).

Tests: GPU headless render proves relative ≠ absolute with the cursor on
line 2; producer test asserts the mode ships; protocol version pins → 14.
fmt + clippy --all-targets clean both flavors + gpu; 1446 lib + 12 protocol
+ 55 pmacs-gpu tests pass. Needs a GPU eyeball.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-07 09:59:27 -04:00
Levi Neuwirth 1ea2d5f17f feat(gutter): daemon-owned line-number mode over protocol v13 (unified toggle)
Fix the control plane for the line-number gutter: M-x
window.toggle-line-numbers now works from EITHER frontend, each affecting
its own window.

Root cause (scores framing bet Q#UX1 false): rendering a gutter is
frontend-local, but the TOGGLE is a daemon command, so the mode has to
reach the GUI over the wire. My earlier GPU control (a --line-numbers flag)
left M-x-in-the-GUI a no-op and the two frontends' settings disconnected.

- Protocol: new additive `InstanceMessage::LineNumbers { buffer_id,
  enabled }`; PROTOCOL_VERSION 12 → 13, SUPPORTED grows to [6..13].
  Daemon-gated < 13 (a v12 peer keeps its gutter off), like every prior
  additive bump — no encoding break.
- Producer: SemanticRenderState::line_numbers_msg reads the frontend's
  active window mode (via active_window_for(frontend_id)) and emits on
  change; cached-compare suppression seeded to the frontend's `off`
  default, so a plain window adds zero traffic and existing frames are
  unchanged.
- Daemon: gate LineNumbers >= 13 in the write loop.
- TUI: drops LineNumbers silently (reads its window directly).
- GPU: consumes LineNumbers → drives local `line_numbers`; the
  --line-numbers flag retired.

Now the daemon Window.line_numbers is the single source of truth; both
frontends render locally from it.

Tests: line_numbers_msg emit-on-toggle/suppress-when-unchanged; protocol
version pins updated to 13. Validated: fmt + clippy --all-targets clean
both flavors; 1440 lib + 12 protocol + 53 pmacs-gpu tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-06 14:27:44 -04:00
Levi Neuwirth b9bd231e64 pmacs GPU minibuffer: wire v12 + band prompt + candidate dropdown (Q#MB1)
The pmacs-gpu frontend can now render the minibuffer, so M-x, C-x-prefixed
commands, and the LSP rename prompt work in the GUI. Render-only — the
minibuffer logic already lives in the core, which is untouched (its fields
are public, so the producer reads them directly).

Protocol v12 (additive; SUPPORTED = [6..12]):
- `InstanceMessage::MinibufferPrompt { prompt, input, cursor, candidates,
  selected, total }` — bufferless (the minibuffer is one global core
  instance), daemon-gated >= 12. The candidate list ships as a windowed
  slice (<= MB_VISIBLE = 10) around the selection, so a 1000-command M-x
  sends ~10 strings per keystroke, not 1000.

Producer / daemon / TUI:
- `semantic_render::minibuffer_prompt_msg` — cached-compare suppressed
  (a single value, not per-buffer), emitted from the active-buffer
  viewport. daemon gates the variant >= 12. The TUI ignores it (it paints
  the minibuffer via its own bottom row).

GPU:
- The bottom band shows `prompt + input` (ahead of search/status) with a
  band caret at the input cursor (monospace advance off the shaped band
  width); the buffer caret hides while a prompt is open.
- A vertical completion dropdown above the band — best match at top,
  selected row highlighted — via a third `TextRenderer` over bg quads
  (the menu popup pattern, reusing its colors). Only shows when there are
  candidates.
- `is_minibuffer_open_chord` forwards M-x and the C-x prefix (otherwise
  withheld) so the GUI can open a prompt / enter a prefix; the daemon then
  flips `dispatch_idle` false and the intercept gate round-trips the rest.
  (Also collapsed two unnested_or_patterns clippy nits in the chord
  helpers.)

Tests: candidate windowing, the producer (open M-x via Lua -> prompt +
windowed candidates -> cached-compare -> cancel clears), a v12 postcard
round-trip, and the version pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-30 21:03:32 -04:00
Levi Neuwirth 640b998d6b pmacs context menu: protocol v11 + dispatch + TUI/GPU surfaces (Q#CM1/Q#CM5)
The wiring that makes the menu and OS clipboard work end-to-end. The
protocol bump touches every exhaustive match on the wire enums, so the
daemon / frontend / GPU consumers all land together.

Protocol v11 (additive; SUPPORTED = [6..11]):
- `PointerKind::Context` (right-click), `FrontendEvent::MenuPointer`
  (GPU->daemon navigation, index-only), `InstanceMessage::MenuPrompt` +
  `MenuPromptRow` (daemon->GPU rows + highlight, daemon-gated >= 11).

Dispatch + producer:
- `EditorState`: menu interception in `dispatch_key`/`dispatch_mouse`,
  `MenuKey`, `dispatch_menu_key`/`_mouse`, `open_context_menu` (TUI) /
  `open_menu_at_byte` + `dispatch_menu_pointer` (GPU), `build_menu_rows`
  (calls the Lua resolver), `dispatch_idle` now false while a menu is
  open. `dispatch_pointer` gains the `Context` arm.
- daemon: routes `Context` -> open, `MenuPointer` -> navigate; gates
  `MenuPrompt` >= 11; drains the clipboard publish as
  `InstanceSignal::Clipboard`; honors the previously-dropped
  `FrontendEvent::Paste` (so paste works for the first time).
- `semantic_render`: `MenuPrompt` producer with cached-compare.

Frontends:
- TUI (`frontend.rs`): OSC 52 clipboard write; ignores `MenuPrompt`
  (the cell overlay renders the menu).
- GPU (`pmacs-gpu`): `arboard` dep; clipboard write/read + Ctrl-V inbound
  paste; right-click -> `Context`; `MenuLocal` + `MenuPrompt` handler;
  the popup (a second `TextRenderer` over bg quads) at the click pixel;
  hover/click -> `MenuPointer`; key intercept while open.

Also folds a pre-existing clippy `unnested_or_patterns` nit in a search
test (`Color::Indexed(11 | 3)`) that newer CI clippy surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:19:00 -04:00
Levi Neuwirth 6e47fb4725 regex-search: GUI regex prompt + protocol v10 (Q#RX5/RX6)
Carries regex mode to the GUI status band and lets the GUI start a
regex search.

SearchPrompt gains `regex` + `invalid` (protocol v10; SUPPORTED grows
to [6,7,8,9,10]). The fields changed that variant's encoding, so the
daemon's per-session gate moves from >= 9 to >= 10 — a v9 peer
negotiates v9 and is simply sent no SearchPrompt (the decorations
still highlight) rather than mis-decoding the wider shape. The
producer fills both from the active SearchSession.

GUI: `is_search_entry_chord` also forwards C-M-s / C-M-r (Ctrl+Alt) so
a regex search can start; M-r (the toggle) already round-trips via the
intercept path once a search runs. The status band reads
`Regex I-search:` in regex mode and `[invalid]` when the pattern won't
compile. Multi-line regex matches needed no GUI change —
push_glyph_extent_rects already fans a byte range across lines.

Tests: SearchPrompt postcard round-trip extended to regex/invalid
shapes; protocol version pin 9→10 + ladder grows to v10; GUI entry
chord accepts C-s/C-r and C-M-s/C-M-r. (last_search_prompt's 5-tuple
factored into a SearchPromptFacts alias to satisfy type_complexity.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:46:43 -04:00
Levi Neuwirth 5111ae82e7 search: GPU isearch surface (Q#SR5, protocol v9)
Brings incremental search to pmacs-gpu, which has no minibuffer, by
reusing the shared daemon-side search core from the previous commit.

Key routing needs no new mechanism: `dispatch_idle` now also reports
false while a search is running, so the GPU's existing M11.6
optimistic-apply gate round-trips every keystroke to the daemon —
where `dispatch_search_key` extends the query / steps — instead of
self-inserting it. The match highlights were already wired (commit
2's SearchMatch / SearchMatchActive decoration colors), so they
light up live the moment keys round-trip.

The one thing a semantic frontend can't derive locally is the query
text, so a new additive `InstanceMessage::SearchPrompt { buffer_id,
query, active, total }` carries it (protocol v9, SUPPORTED grows to
[6,7,8,9]). The producer emits it cached-compare-suppressed like
StatusFacts — `query: Some` while searching, `None` to clear on
accept/cancel (matches keep highlighting via decorations), and
stays silent on a fresh buffer that never searched. The daemon's
per-session filter keeps the variant off wires negotiated < 9. The
GPU mirrors it into the status band: while searching, the band's
left side shows `I-search: <query> (n/m)` (or `[no match]`) in
place of the buffer name, returning to the name when the search
ends.

Tests: protocol version pin + SearchPrompt postcard round-trip
(active / failing / cleared shapes); producer emit-on-change +
suppress + clear-on-accept + first-sight silence; dispatch_idle
flips false during search (the GPU round-trip contract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:26:25 -04:00
Levi Neuwirth 7c46288ef0 search: render matches in both frontends (Q#SR3/SR4)
The search store now hangs off EditorCore (reachable by the
producer, the Lua commands, and the TUI view). The decorations
producer emits SearchMatch for every visible match and
SearchMatchActive for the active one, byte-range-direct (no
line/col conversion), viewport-clipped, and stale-skipped on the
M11.8 model. pmacs-gpu wires the two decoration_kind_to_bg_color
arms (translucent yellow / stronger amber). The TUI gets a
SearchView overlay mirroring DiagnosticView (black-on-yellow wash,
brighter for the active match), reusing diag.rs's now-pub(crate)
line/col helpers.

Nothing populates the store yet (commit 3 wires the input), so the
paths are dormant until then — verified by populating the store
directly: producer emits the right kinds + stale-suppresses, the
TUI view washes the cells.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:48:06 -04:00
Levi Neuwirth 44430e8377 StatusFacts (protocol v8): name, modified, exact diag counts
The wire-authoritative half of the status band (Q#S1): an additive
InstanceMessage::StatusFacts { buffer_id, name, modified,
diag_errors, diag_warnings }, emitted by the semantic producer on
change (cached-compare). Counts freeze at their last value while
the diag store is stale — positions go wrong mid-edit but counts
merely lag, and flickering to zero per keystroke would be worse.
The daemon's write loop keeps the variant off wires negotiated
< 8, the DispatchIdle gate shape; SUPPORTED grows to [6, 7, 8].

GPU side: the band's left shows name + modified dot, the right
gains severity-colored E:n/W:n ahead of the local L:C/scroll
readout (rich-text spans, change-detected per side).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:17:07 -04:00
Levi Neuwirth afd5e80466 producer: widen zero-width diagnostics to a visible byte
User validation found nothing rendered in pmacs-gpu for the
missing-comma error — rust-analyzer's zero-width EOL anchor. The
TUI's anchor-cell fix lives in DiagnosticView, but the semantic
wire ships Decorations straight from line/col conversion: a
zero-width range clips to None at the frontend and overlaps no
glyph, so the GPU drew nothing. Widen at the producer (one byte;
forward mid-line, backward at EOL where forward covers only the
glyph-less newline) so every semantic frontend gets a paintable
range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:24:31 -04:00
Levi Neuwirth c2f6905b08 minimap diagnostic marks: severity color rides FileStyleSummary
The GPU's gutter-sign equivalent (framing Q#D3). The producer folds
diagnostics into the per-line summary: each touched line's dominant
style gets the severity's canonical underline_color (most severe
wins), skipped while the URI's store entry is stale — same
discipline as the decorations producer. The minimap stroke prefers
underline_color over the syntax fg, so error/warning lines read at
a glance.

Diagnostics publish without a CRDT generation bump, so the
summary's generation-keyed cache gains a second key: a new per-URI
epoch on DiagnosticStore (bumped on set/clear, not mark_stale). A
republish re-emits the summary; everything else stays suppressed
(framing bet #3 — the gate widens precisely, not naively).

DiagnosticSeverity::underline_color() becomes the canonical palette
(TUI squiggles, col-0 markers, and minimap marks all share it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:16:45 -04:00
Levi Neuwirth 68a75a7422 session R follow-up — post-burst navigation cost (scroll reuse, changed-line frames, carried diags)
Typing was fixed but arrow navigation after a burst — especially
Shift+arrows — stayed slow. Three compounding mechanisms:

1. Edge navigation ran the FULL pipeline per scrolled line: slice
   reshape + viewport re-declaration + a full StyleSpans frame +
   another full reshape on its arrival. The shaped slice is now
   rebuilt by REUSING retained BufferLines (their shape caches
   survive; only newly exposed lines shape), keyed by absolute line
   index — sound because every builder keeps the per-line chunk
   cache current.
2. Every incoming frame (StyleSpans / fg Decorations /
   InlineAdornments) re-shaped the whole slice even when one line's
   styling changed. refresh_changed_lines compares each line's fresh
   chunk set against the cache and re-shapes only differing lines —
   a parse-settle frame after a burst recolors a line or two, and a
   scroll-triggered resync only the newly exposed ones.
3. Daemon: a selection change during the post-burst stale window
   (didChange debounce + server latency) broke the diagnostics hold
   with a FULL Decorations frame per Shift+arrow press that also
   dropped the held diagnostics (blink + churn). The producer now
   CARRIES the previously shipped diagnostic items through the
   frame set while stale — selection motion diffs as a tiny
   selection-only segment, carried diag ranges never re-ship at
   stale positions, and the baseline's generation stays current so
   the eventual unstale frame diffs instead of full-resyncing.

set_rich_text is gone: full reshape, line surgery, scroll reuse, and
frame refresh all assemble lines through one builder
(chunks_for_line + line_from_chunks), so all paths agree by
construction and the per-line chunk cache is always authoritative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:35:11 -04:00
Levi Neuwirth e7669c9d83 producer: settle-gated styling + hold-while-stale for diagnostics/inlays/tokens
Typing-perf + render-churn fixes in the semantic producer:

- Grammar styling waits for parse settle (pending edits, in-flight
  parse job, or no installed bundle ⇒ hold the previous spans rather
  than querying stale syntax per typed byte); FileStyleSummary
  debounces on the same condition.
- CurrentLine is no longer emitted for semantic frontends — the GPU
  paints its own caret/current-line and the derivation forced a
  whole-buffer line table every frame.
- Hold-while-stale: while the diag / inlay-hint / semantic-token
  stores are stale (document edited since the last server response),
  emit NOTHING instead of a clearing frame. The frontend's
  last-received set — which it translates through its own local
  edits — is strictly better than an empty wipe (diagnostics blinked
  out per typing burst and back in per publish, a full frontend
  reshape each way; inlay wipes visibly shifted line layout; the
  LSP-token path blanked C++ colors). A selection change during the
  stale window still ships, without the diagnostic kinds.
- Diagnostics byte<->line table cached per buffer revision (was an
  O(buffer) rope copy + scan on every tick a diagnostic was visible).
- Empty->empty Decorations frames on generation bumps suppressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:48:16 -04:00
Levi Neuwirth 6bebd770e1 S1 follow-up — scope the per-edit highlight queries (typing perf, Q#S6)
Scrolling became fast after S1 but typing stayed slow: scrolling
doesn't bump the CRDT generation, so the daemon's StyleGate caches and
no query runs — but every keystroke bumps the generation and forced
TWO whole-file tree-sitter passes on the daemon, which S1 deferred as
Q#S6. With the GPU now O(visible), this was the remaining O(file)
per-keystroke cost.

1. StyleSpans query scoped to the viewport. New
   `compute_highlight_spans_in_range` sets `QueryCursor::set_byte_range`
   so the capture walk is proportional to the visible range, not the
   whole tree; `scoped_style_spans` passes the declared viewport. The
   StyleGate still recomputes on the edit's generation bump (M11.7
   resync), but that recompute is now O(visible).

2. FileStyleSummary (the minimap — inherently a whole-file pass)
   debounced to reparse-completion: skip the recompute while a reparse
   is in flight (`pending_edit_count() > 0`). During continuous typing
   the whole-file pass runs at reparse rate, not keystroke rate;
   when typing settles and the parse lands, it recomputes once.

Together these drop the daemon's per-keystroke cost from two whole-file
tree-sitter passes to one viewport-scoped pass (+ an amortized
whole-file summary). Only the semantic (pmacs-gpu) path is affected;
the grid/TUI path doesn't use this producer.

Gates green: fmt; clippy --all-targets --workspace -D warnings (default
+ crdt); pmacs lib 1334; syntax 6; semantic_render 28;
m11_5_semantic_acceptance 2; m4_acceptance 88.

Awaiting visual confirmation: typing in a large file is now responsive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:47:43 -04:00
Levi Neuwirth 965cdd9560 9.3 perf — gate StyleSpans recompute on cursor-only ticks
Root cause of "slow only when the GUI is attached": the daemon's
single-threaded dispatcher loop runs the semantic frontend's
render_frame every tick, and `scoped_style_spans` runs the tree-sitter
highlights query over the *whole declared viewport* — which the GPU
frontend sets to the entire buffer — plus clones the theme, on EVERY
tick. Since render_frame recomputes the projection to diff it, every
TUI keystroke forced a full-file tree-sitter query in the daemon
before TUI input could be serviced. Smooth without the GUI; the
attached semantic frontend is what loads the loop.

Fix: a recompute gate. Style spans for a grammar-backed buffer are a
pure function of (parse bundle, CRDT generation, viewport) — never the
cursor — so a cursor-only tick can skip the query and the diff
entirely. `StyleGate` holds the current parse bundle `Arc` (kept alive
so its address is stable; compared via `Arc::ptr_eq`, immune to the
ABA a raw-pointer compare would hit) plus generation + viewport.
`render_frame` skips `emit_style_spans` when the gate matches the
last one and a baseline was already sent.

Correctness:
- Edit → generation bumps → gate differs → recompute → full=true
  resync preserved (M11.7).
- Async reparse lands → bundle Arc changes → gate differs → recompute
  → incremental emit. The fresh parse is never missed.
- Cursor move → bundle, generation, viewport all unchanged → skip.
- LSP-token path (no grammar, e.g. C/C++) has no cheap bundle handle,
  so `grammar_style_key` returns None and that path recomputes every
  tick exactly as before — no behavior change, no new staleness.

`emit_style_spans` is the former inline StyleSpans block extracted
verbatim so the gate can wrap it.

Combined with the earlier scoped_decorations single-materialization
fix, the per-tick daemon cost for an idle (cursor-only) semantic
frontend drops from "full-file tree-sitter query + theme clone + 2
rope copies" to "one rope copy for the current-line/diagnostic
decoration set."

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11; semantic_render unit 33
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:39:57 -04:00
Levi Neuwirth 8e29e8b90b 9.3 perf — collapse per-tick rope copy; add frame timing
Investigating the cursor slowdown reported after the wash became
visible.

Confident daemon-side win: `scoped_decorations` (run every tick per
semantic frontend, in the daemon's single-threaded loop that also
serves the TUI) was materializing the whole buffer via
`buffer_source_bytes` — an O(n) rope→Vec copy — TWICE per tick: once
in the 9.2 CurrentLine branch and again in the diagnostics branch. For
an LSP buffer (diagnostics present, the common case) that doubled the
per-tick copy cost, and the daemon's tick latency gates TUI cursor
responsiveness. Now the source + line-start table is materialized at
most once per call via `get_or_insert_with` and shared between both
branches (and skipped entirely when neither branch needs it).

Consumer instrumentation to localize any remaining cost:
- `PMACS_GPU_DEBUG_FRAME=1` logs per-`render()` sub-phase timings
  (background rects / minimap rects / glyph prepare+submit / total /
  peer count). winit defaults to ControlFlow::Wait, so renders are
  on-demand (one per coalesced redraw request), not a continuous
  loop — the timing isolates the cost of a single cursor-driven frame.
- The `PMACS_GPU_DEBUG_PRESENCE` check is now one-shot via OnceLock
  instead of a per-message `std::env::var_os` (which locks the global
  env table); same for the new frame flag.

No behavior change to the rendered output. `render()` gains the
clippy too_many_lines allow (now 115 lines with the timing block),
matching the precedent on the other linear GPU-setup functions.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11; pmacs-gpu unit 15
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:26:38 -04:00
Levi Neuwirth 7dfcd79d72 session 9.2 — CurrentLine quad backgrounds
Closes the second half of Phase A's deferred finding A8. The producer
now emits DecorationKind::CurrentLine derived from the active window's
cursor; pmacs-gpu paints it as a very subtle blue-grey wash under the
line carrying the cursor.

## Q-stance implementation status

- **Q#1 stance (α) — producer-side emission**: `scoped_decorations`
  reads `core.active_window_for(self.frontend_id).cursor`, derives the
  enclosing line via a new `current_line_range` helper, and pushes a
  `Decoration { kind: CurrentLine, range }` clipped to the viewport.
  Same per-frontend access path used for Selection (line 378).
- **Q#3 stance (β) — per-line cadence**: implementation-revealed
  simplification. The framing doc proposed a `last_cursor_line` cache
  on SemanticRenderState; in practice the existing M11.4 diff
  (`changed_intervals`) already gives this for free. A same-line
  cursor move produces a byte-identical decoration Vec, so
  `changed_intervals` returns empty and nothing ships. A line change
  produces a different range and re-emission fires. No extra state
  needed. Recorded as a small finding under rule (iii); the stance
  holds, only the implementation tightens.
- **Q#2 (render order)** continues to apply from 9.1 — quad
  backgrounds first, text second, minimap last.
- **Q#4 (search backgrounds)** still deferred awaiting search.

## Producer

- New `current_line_range(line_starts, source_len, cursor) -> (u64,
  u64)` helper at `src/semantic_render.rs`: binary-searches line_starts
  for the largest `start <= cursor`, returns the half-open byte range
  `(line_start, next_line_start_or_source_len)`. Clamps to source_len
  so a cursor at or past EOF resolves to the last line cleanly.
- `scoped_decorations` restructured: the Selection branch and the new
  CurrentLine branch share the `win.buffer_id == vp.buffer_id` gate so
  per-window state never leaks into a viewport projecting a different
  buffer (the `decorations_use_vp_buffer_not_active_buffer` invariant).
- Four new tests:
  - `current_line_range_finds_enclosing_line` — unit test covering
    line-zero, mid-line, start-of-line, last-line, and past-EOF.
  - `current_line_projects_as_a_decoration_for_cursor_on_seed` —
    cursor at byte 0 of "abc\\nde" emits CurrentLine for [0, 4).
  - `current_line_skipped_when_active_window_is_a_different_buffer` —
    multi-frontend invariant: projecting a non-active buffer does not
    emit CurrentLine.
  - `same_line_cursor_motion_does_not_re_emit_decorations` — Q#3
    cadence: horizontal motion within a line is silent; crossing `\n`
    re-emits.
- Existing test `diagnostics_project_with_line_col_to_byte_and_severity`
  updated: the seeded "abc\\nde" buffer now produces both a
  DiagnosticWarning and a CurrentLine. The test now finds the warning
  by `kind` and asserts its byte range rather than asserting a total
  count of 1.

## Consumer

- `decoration_kind_to_bg_color` in pmacs-gpu/src/main.rs adds the
  CurrentLine arm: `[0.55, 0.60, 0.75, 0.08]` — a very subtle blue-grey
  with low alpha. CurrentLine is always on, so it wants to be visually
  quietest of the four background kinds; just enough tint to track
  cursor line, not enough to compete with Selection or syntax color.
- `bg_color_helper_covers_selection_and_returns_none_for_unrendered_kinds`
  renamed to `bg_color_helper_covers_selection_and_current_line` and
  updated to assert CurrentLine now returns Some.
- `fg_and_bg_helpers_are_disjoint_total_cover` updated: CurrentLine is
  no longer in the "deferred neither yet" set, only the search pair.

## Bet status

- **Bet #2 (overlap composition between Selection and CurrentLine)**:
  exercised. CurrentLine has alpha 0.08, Selection 0.30. When both
  cover the same bytes (cursor on a selected line), they alpha-blend
  in draw order. Composition is left to the M11.4 dirty-merge ordering
  (decorations sorted by range.start): CurrentLine paints first
  (covers the whole line, lower start), Selection paints on top. The
  resulting visual is selection-blue with a slight CurrentLine tint
  visible at the line's non-selected ends. Honest composition rule
  if surfaced as wrong: refine.
- **Bet #3 (cadence)**: predicted producer-side `last_cursor_line`
  cache; implementation revealed the M11.4 diff already throttles.
  Score: predicted category surfaced (true positive on the cadence
  concern), but the *implementation* category for the resolution did
  not match. Recorded as rule-(iii) small finding.

## Gates (all green)

- `cargo fmt --all -- --check`
- `cargo clippy --all-targets --workspace -- -D warnings`
- `cargo clippy --all-targets --workspace --features crdt -- -D warnings`
- pmacs lib + pmacs-protocol: **1329 + 11 = 1340** (+4 new producer
  tests)
- pmacs-gpu unit: **13** (unchanged count; one test renamed +
  re-scoped)
- m4_acceptance: **88**, m11_5_semantic_acceptance (--features crdt):
  **2**

## Manual validation walkthrough

Same daemon + TUI attach + pmacs-gpu attach shape. In the GPU window:

- Verify a subtle blue-grey wash appears under the cursor's line.
- Move the cursor up/down — the wash tracks the new line.
- Move the cursor left/right within a line — visible behavior should
  be identical (Q#3 cadence: no re-render needed).
- Select text crossing the current line — Selection paints over
  CurrentLine; both alpha-blends visible at the line's non-selected
  edges.
- Resize the window — both backgrounds reshape correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:20:26 -04:00
Levi Neuwirth a67cb8a6f1 Close pmacs-gpu phase A audit 2026-05-28 12:49:23 -04:00
Levi Neuwirth 9718958c4c Fix stale TUI styling after edits 2026-05-26 11:15:23 -04:00
Levi Neuwirth 886239480e
T M11.8 — diag-store stale-flag closes LSP-re-analysis-gap surface (#48)
Surfaced during session-5 manual validation as the final iteration
of bet #1 from the framing pass: edits that shift byte positions
left old diagnostic colors painted over post-edit text in both
`pmacs-gpu` and (now-visible) the TUI. Persisted for the full LSP
re-analysis window (100ms–5s).

PR #47 fixed the StyleSpans side via generation-tracked full=true
emission, but Decorations remained vulnerable: the producer's diff
shipped old diagnostics from the diag store, whose entries were
indexed at pre-edit byte positions until clangd republished.

Fix: a per-URI `stale_uris` flag in `DiagnosticStore`. The LSP
layer's `did_change_full` marks the URI stale right after sending
the notification; the next `publishDiagnostics` absorb path's
`set` clears it. The `semantic_render` producer reads `is_stale`
and skips diagnostic emission entirely while stale.

Effect: between an edit and clangd's next publish, the producer
ships zero diagnostic decorations. Frontend's replace/merge clears
old positions cleanly. Brief uncolored window (≤ LSP re-analysis
latency) replaces the previous wrong-position-color persistence.
The correct visual tradeoff: honest emptiness over deceptive
staleness.

Files changed:

- `src/diag.rs` — `DiagnosticStore` gains `stale_uris: HashSet<String>`;
  new `mark_stale` / `is_stale` API; `set` and `clear` reset the
  flag on the assumption that absorption / explicit removal mean
  the LSP has caught up.
- `src/lsp.rs` — `LspManager::did_change_full` calls
  `diag_store.lock().mark_stale(uri)` after `send_notification`.
- `src/semantic_render.rs` — `scoped_decorations` reads `is_stale`
  alongside `for_uri`; when stale, suppresses the diagnostic
  loop (selection and other non-diagnostic kinds still emit).

Tests (all crdt-gated where they reference semantic_render):

- `diag::tests::stale_flag_default_false`
- `diag::tests::mark_stale_sets_flag` (per-URI scoping)
- `diag::tests::set_clears_stale_flag`
- `diag::tests::empty_set_clears_stale_flag_too`
- `diag::tests::clear_drops_stale_flag`
- `semantic_render::tests::diagnostics_suppressed_while_diag_store_stale`
  — assert no diagnostic kinds emit while stale; assert they
  re-emit after a fresh `set` clears the flag.

Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1482 (+6) with crdt; 1319 (+6) without; m4 83; m11_5 2.

This is approach (A) from the session-5 ask: track per-URI freshness
relative to buffer edits, suppress emission until LSP catches up.
Approach (B) — clangd's didChange/publishDiagnostics version
matching — would be more precise but requires plumbing version
tracking through the LspManager's document state, which is a
larger change deferred. The stale-flag captures the same semantic
("any edit since last publish ⇒ stale") at a cheaper cost.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:01:10 +00:00
Levi Neuwirth 36a53f8d5a
T M11.7 — producer forces full=true on generation transition (#47)
Surfaced during session-5 manual validation (probe #3, the bet-#1
shape from the framing pass): editing at a diagnostic boundary in
the TUI left stale color fragments visible in pmacs-gpu against
shifted text.

Root cause: SemanticRenderState ships `full=true` styling only on
viewport-region changes, not on text-edit transitions. When the
buffer's CRDT generation advances (an edit) but the viewport stays
the same, the producer ships an incremental — but the frontend's
cached spans + decorations are indexed at *pre-edit* byte
positions. The incremental only ships dirty-range items, expecting
the frontend to retain everything else; combined with shifted
positions, the result is wrong-position color persisting until
the next viewport change.

Fix: track `generation` per buffer in `LastFrame`. Force `full=true`
when generation differs from the last-shipped value, so the
frontend's next `replace_*` operation rebuilds the cache wholesale
at the new positions.

Tradeoff: one extra full-viewport ship per edit. Negligible over
the local Unix socket; bounded by viewport size; and exactly what
the contract requires after position shifts.

Affects both diff-shaped families:

- `StyleSpans` — tree-sitter / LSP semantic tokens
- `Decorations` — diagnostics + selection

`InlineAdornments` uses whole-set replacement (M11.2-level
suppression), not dirty-segment diff, so doesn't have the same
issue. Tracking `generation` in its `LastFrame` for struct
uniformity; predicate unchanged.

Regression test: `full_resync_on_generation_transition` upgrades a
buffer to CRDT-backed, lands an initial full frame, edits the
buffer to bump `version_scalar`, asserts the next StyleSpans +
Decorations both ship `full=true`.

Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1476 (+1) with crdt; 1313 unchanged without (new test is
crdt-gated); m4 83; m11_5 2.

This closes the bet-#1 surface for the consumer side. A separate
follow-up to session-5 PR #43 will document the resolution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:43:16 +00:00
Levi Neuwirth 875669a49c
semantic_render: resolve URI from vp.buffer_id, not active_buffer (#46)
Surfaced during session-5 manual validation: pmacs-gpu opened a file
that had 8 LSP diagnostics in the shared store (verified via the
debug.diag-status command on the TUI side), yet pmacs-gpu's
`Decorations` message arrived empty.

Root cause: three sites in `semantic_render.rs` resolved the lookup
URI from `core.active_buffer_path()` — the *editor's* active buffer.
In a multi-frontend setup (TUI + pmacs-gpu attached at once), the
daemon's per-tick render loop temporarily flips `active_frontend`
to each fid before that frontend's frame. Each frontend has its own
`FrontendView` with its own active window; pmacs-gpu's was
registered against a fresh scratch buffer at attach time (the v0.1
default in `handle_session_established`). So when the producer
ran for pmacs-gpu, `active_buffer_path()` returned `None` — the
scratch has no file path — and the diag / inlay / LSP-semantic-token
lookups all came back empty.

Fix: route the URI through `vp.buffer_id` via a new
`buffer_file_uri(core, buffer_id)` helper. Single-frontend case is
unchanged (the active buffer equals the projected buffer); multi-
frontend now finds the right URI.

Three sites updated:

- `scoped_decorations` (line 366) — diagnostics
- `inline_adornments_msg` (line 425) — inlay hints
- `lsp_scoped_style_spans` (line 701) — LSP semantic tokens for
  grammar-less languages

Regression test: `decorations_use_vp_buffer_not_active_buffer`
constructs the multi-frontend shape (LOCAL's active is scratch; a
second buffer with a file path holds a seeded diagnostic; the
viewport projects the second buffer) and asserts the decoration
surfaces.

Separately surfaced (not fixed here, documented in task #23):
`DiagnosticView` is defined in `diag.rs` but never attached to any
buffer. The TUI grid path has no diagnostic underline rendering as
a result — an unrelated M4.6 incompleteness from v0.1's initial
commit. Tracked as its own thread; will need a framing pass for
scope (view attach? navigation bindings? statusline summary?
gutter signs?).

Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1475 (+1) with crdt; 1313 (+1) without; m4 83; m11_5 2.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 14:56:26 +00:00
Levi Neuwirth c692710321 M1+M2: FileStyleSummary (minimap producer, Q#2 resolved) + doc
New InstanceMessage::FileStyleSummary { buffer_id, generation, lines:
Vec<Style> }: a coarse whole-file styling summary for a Zed/VSCode-
style minimap, resolving the design note's Open Q#2. One dominant
Style per source line (by byte count across the producer's current
spans); the frontend maps minimap rows to one or more lines.

Producer scoped_file_summary reuses scoped_style_spans with a whole-
buffer synthetic viewport, so policy A's authority pick (tree-sitter
for grammar-backed languages, LSP semantic tokens otherwise) is
inherited automatically — no separate styling path. file_style_summary_msg
is keyed on the buffer's CRDT generation: an idle buffer at the same
generation pays nothing (the whole-file summary is the expensive bit
on large files, so re-emit only after edits). First frame for a
buffer always emits; the existing first-frame test updated to expect
3 messages (StyleSpans + Decorations + FileStyleSummary).

Per-line dominant style is the v1 representation. Future refinements
(fixed-N bands; whole-file RLE style runs) are recorded in the design
note as straightforward extensions if a real frontend prefers them.

Structural gating same as the other semantic families: the daemon
only constructs a SemanticRenderState for sessions that negotiated
semantic_render, so non-semantic sessions never receive it. Grid TUI
adds the variant to its ignore list. Round-trip fixture covers it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:23:20 -04:00
Levi Neuwirth 8ec3abcad5 Step 3+5: InlineAdornments producer from LSP inlay hints + doc update
scoped_inline_adornments (free fn, mirrors scoped_style_spans) reads
the inlay-hint store via for_uri and maps each InlayHint to an
InlineAdornment { at, AtOffset, Text{padded label, default style} },
clipped to the declared viewport (anchor in [vis_start, vis_end)).
Step 0 established inlay columns are already byte offsets by the time
they reach the store (inbound_converted rewrites the Position-shaped
InlayHint.position), so line_col_to_byte is exact with no per-server
encoding — unlike semantic-token styling.

inline_adornments_msg does the suppression: the InlineAdornments wire
variant has no generation/full/segments, so this is M11.2-level only
(whole-set re-send on any change, nothing when byte-identical, and
never an empty frame when there is nothing to say — no spam).

Tests: clip-to-viewport + padding + AtOffset, suppress-then-resync,
no-emit-without-hints; the old never-emitted invariant is split into
block_adornments_and_fold_state_still_never_emitted (Block/Fold are
still unwired) plus inline_adornments_not_emitted_without_hints.
assert_semantic_only now admits InlineAdornments.

Step 5 (folded): docs/semantic-frontend-protocol.md moves StyleSpans
(policy A) + InlineAdornments out of "declared, not wired", and adds
two deferred Open questions — per-byte tree-sitter/LSP blend, and
multiple-servers-one-URI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:00:44 -04:00
Levi Neuwirth 0c9e46de6c Step 2: StyleSpans LSP-semantic-token authority (policy A) + tests
Languages with no bundled tree-sitter grammar (C/C++, …) had empty
StyleSpans — the visible "no C++ syntax coloring" gap. scoped_style_spans
now applies per-language styling authority (policy A): a grammar-backed
language stays tree-sitter-only (unchanged); a grammar-less buffer falls
through to lsp_scoped_style_spans, which reads the semantic-token store
(for_uri), resolves the owning server's encoding + legend via
LspManager::semantic_style_context, converts UTF-16 start/length to byte
per line with char_to_byte (now pub(crate); semantic-token data is NOT
byte-rewritten upstream, unlike inlay hints — see Step 0), names the
token via the legend, maps through the existing Theme::lookup, and drops
default-style spans. Output is shape-identical to the tree-sitter path,
so the M11.4 diff pipeline consumes it unchanged. Never two authorities
on one buffer: a still-parsing grammar-backed buffer returns empty
rather than briefly borrowing LSP styling.

Step 4 folded in: golden tests in the semantic_render module —
cpp_style_comes_from_lsp_when_no_tree_sitter_grammar (headline),
suppression (M11.4 reuse), incremental-on-token-change, and honest
empty-without-tokens. A #[cfg(test)] LspManager::insert_initialized_
test_client supplies a synthetic Initialized client (legend caps +
encoding) with no process, so the producer path is exercised without
a live server.

Per-byte tree-sitter/LSP blend and multi-server-same-uri merge remain
deferred open questions (recorded in the design note by Step 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:22:03 -04:00
Levi Neuwirth f7267cd720 T M4.5 L1: cross-file nav foundations — per-buffer file_path, jump ring, x-file go-to-def
Lays the groundwork for WorkspaceEdit/rename (L2+) by making
navigation cross-file-correct.

- Relocate file_path/file_meta from the EditorCore global onto
  Buffer itself, so each buffer keeps its own filesystem identity
  across cross-file navigation. Accessors + registry/editor/lua/
  semantic_render call sites migrated; zero behavioural change for
  single-file flows.
- uri->path: project_index::uri_to_path made pub; pmacs.lsp.path_for_uri.
- find-or-open: BufferRegistry::find_by_path + pmacs.buffer.find_or_open
  dedups an already-open file instead of spawning a duplicate buffer
  (SP-4 Gap A).
- Bounded jump ring on EditorCore (cap 64, oldest-evict, stale-buffer
  skip): push_jump/jump_back + pmacs.editor.* bindings + lsp.jump-back
  command bound to M-,.
- pmacs.lsp.go_to_definition cross-file branch: decode URI ->
  push_jump -> find_or_open -> reposition, with a failure path that
  unwinds the pushed origin. ensure_server now passes cfg.env through.

Tests: 5 jump-ring unit tests; m4_12_cross_file_go_to_definition_and_
jump_back end-to-end via a new `defenv` fake-LSP mode. All gates green
(lib 1262/0, m4 67/0, m8_1/m8_9/m8_10, m9_1, m11_5 --features crdt 2/0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:53:29 -04:00
Levi Neuwirth 80c0679a5b T M4.5: documentSymbol / workspace symbol / documentHighlight
Completes the read-only LSP feature set (everything except edits).
Same request→store→Lua pattern as the nav batch; these three need
new parsing (new response shapes), so they were deliberately split
from the Location-family PR (#16).

- src/symbol.rs: one flat Symbol type for both symbol requests.
  from_lsp_value handles BOTH LSP shapes — hierarchical
  DocumentSymbol[] (flattened with depth + parent chain) and flat
  SymbolInformation[]/WorkspaceSymbol[] (location.uri, range
  optional for WorkspaceSymbol). Scope-keyed (Document(uri) vs
  Workspace(query)) so an outline and a query don't collide.
- src/document_highlight.rs: range + DocumentHighlightKind (absent
  defaults to Text=1 per spec), (server,uri)-keyed.
- lsp.rs: three ResponseRoute variants + absorb arms + request
  methods. documentSymbol/documentHighlight ranges convert via the
  requested-doc codec; workspace/symbol results are cross-file →
  route uri "" → non-destructive passthrough (same rule as
  cross-file definition).
- Lua: raw bindings + pmacs.document_symbol / .workspace_symbol /
  .document_highlight read surfaces (the new LSP Symbol is aliased
  to avoid the pre-existing project_index::Symbol name clash);
  lsp.lua wrappers + an lsp.document-symbols command on C-c o
  (modeline summary; outline buffer is future UX).
- Tests: 6 parser unit tests (hierarchical depth/parent, flat
  SymbolInformation, range-less WorkspaceSymbol, highlight kind
  default, scope non-collision) + an e2e driving all three through
  the async bridge asserting shape correctness.

Also includes a pre-existing rustfmt normalization of the #15
semantic-frontend files (protocol.rs / semantic_client.rs /
semantic_render.rs) — main was not rustfmt-clean there after the #15
merge; bundled here per operator decision so the fmt gate is green.

Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1257/0; m4_acceptance 66/0; m9_1 18/0; m8_1/m8_9/m8_10 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:15:03 -04:00
Levi Neuwirth 071e79ffee M11.4: segment diffing for StyleSpans/Decorations
Replace the M11.2/M11.3 coarse whole-payload suppression with a
CellDelta-style diff lifted from positional cells to byte-anchored
ranges.

- protocol.rs: StyleSpans/Decorations refined to
  { buffer_id, generation, full: bool, segments: Vec<...Segment> }.
  New StyleSegment{range,spans} / DecorationSegment{range,decorations}.
  full=true → frontend discards prior state for the buffer; full=false
  → replace styling only within each segment's range, bytes in no
  segment keep prior state. Decorations gains generation for parity.
  Each segment carries ALL current items intersecting its range
  (clipped), so an unchanged span overlapping a dirty range is
  reconstructed. ResourceOffer stays an honest stub (no producer).
- semantic_render.rs: LastFrame baseline per buffer (viewport region
  + full item set). full on first frame / viewport-region change;
  else symmetric-difference the ordered sets, coalesce changed ranges
  into maximal disjoint dirty intervals, emit one segment per
  interval with current items clipped to it; suppress when no dirty
  interval. Independent baselines for styling vs decorations.

Byte offsets cascade on edits (an insert shifts later spans), so an
incremental post-edit frame dirties [edit, viewport_end) — bounded;
no-edit frames (cursor/scroll/selection-only) still cost nothing.

10 semantic_render tests (full-on-first/viewport-change, incremental
dirty intervals, independent suppression, unchanged-overlapping
reconstruction) + updated protocol round-trip. Lib (1397 crdt / 1242
non-crdt) + integration green both flavors; clippy -D warnings clean
both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:04:49 -04:00
Levi Neuwirth efa8f6f8ed M11.3: Decorations projection (selection + diagnostics)
SemanticRenderState now also projects InstanceMessage::Decorations
from the instance-side state pmacs actually has.

- Selection: per-window byte-native state via active_window_for(fid)
  (SemanticRenderState now carries the session FrontendId), gated to
  the declared buffer and clipped to the viewport →
  DecorationKind::Selection.
- Diagnostics: the shared DiagnosticStore keyed by file URI. Made
  lsp::path_to_file_uri pub(crate) (byte-identical to the Lua
  file_uri_for) so the projection reproduces the exact store key from
  core.file_path. LSP (line,col) -> byte via a line-start scan
  against the buffer source; severity -> DiagnosticError/Warning/
  Info/Hint. Clipped to the viewport.
- StyleSpans and Decorations suppress unchanged frames independently
  (separate last_* maps): a selection move doesn't force a styling
  re-send and vice versa.
- Deliberately NOT emitted: SearchMatch/SearchMatchActive (no
  instance search-hit store), CurrentLine (frontend derives from
  CursorByte; emitting it would breach the contract boundary).
- InlineAdornments/BlockAdornments/FoldState remain unproduced by
  design — no inlay/blame/lens/fold/diff source exists in pmacs yet.
  Honest stubs (the M11.1 "declared, not yet wired" discipline), not
  empty messages every frame.

Dispatcher updated for SemanticRenderState::new(frontend_id). Lib
(1394 crdt / 1239 non-crdt) + integration green on both feature
flavors; clippy -D warnings clean on both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:47:18 -04:00
Levi Neuwirth 4ba6fd7bb8 M11.2: semantic projection seam (SemanticRenderState)
The first real producer of the semantic-frontend arc. The instance
projects syntax styling to semantic_render sessions without
rasterizing to a cell grid.

- src/semantic_render.rs: SemanticRenderState, sibling of
  instance_render::RenderState. Reads the same EditorState, emits
  InstanceMessage::StyleSpans (tree-sitter spans via the active
  Theme), scoped + clipped to the FrontendEvent::Viewport byte range.
  Emits nothing until a viewport is declared; suppresses
  byte-identical frames (per-span delta encoding deferred to M11.4).
- CrdtState::version_scalar(): oplog version vector summed to a
  monotonic non-decreasing u64 — the StyleSpans.generation anchor.
- daemon dispatcher: semantic_states map parallel to render_states;
  projection selected per session. Semantic sessions get StyleSpans +
  CursorByte + BufferSnapshot + CrdtOp + presence, never CellDelta /
  grid Cursor. FrontendEvent::Viewport consumed (routed by
  authenticated source). SessionEstablished body extracted to
  handle_session_established (clippy 100-line ceiling). Grid-less
  sessions no longer panic the _ => apply_event arm.
- InstanceCapabilities default semantic_render flipped to
  cfg!(feature = "crdt") — the "M11.2 enables semantic" moment,
  analogous to the M10.8 Day-4 flip. M11.1 negotiation test comment
  updated for the flip (frontend-side default still false keeps M10.7
  outcomes unperturbed).

Lib (1392 crdt / 1237 non-crdt) + integration suites green on both
feature flavors; clippy -D warnings clean on both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:14:08 -04:00