First Phase B session: pmacs-gpu can move its own cursor. Consumer-only
(the daemon already dispatches FrontendEvent::Key through the same
keymap/command stack the TUI uses; verified in the Phase B framing).
- `AttachClient::send_key` emits `FrontendEvent::Key`.
- `translate_key` maps winit logical key + modifier state → protocol
(Key, Modifiers). Covers the full editing set; `is_motion_key` gates
B1 to cursor-motion keys only (arrows, Home/End, PageUp/PageDown) so
no buffer mutation happens yet — editing keys open in B2 by dropping
the gate. Modifiers tracked via winit `ModifiersChanged`.
- `window_event` rework: Escape stays a local quit; other pressed keys
translate and (motion-gated) `send_key`.
- Consume `InstanceMessage::CursorByte` → `own_cursor` (Q#B3: the
daemon is authoritative; the caret follows whatever it reports, incl.
command-driven motion this frontend never interprets).
- Caret: a thin quad bar drawn *over* the text at the cursor glyph,
byte→glyph mapping rebased per line via `line_byte_offsets[line_i]`
(bet B4 / the QB3 lesson applied up front).
- Un-suppress own-window `Selection`/`CurrentLine` washes from
`current_decorations` alongside peer presence (Q#B4): the QB1
suppression lifts now that the own cursor is live. The bg-wash
builder split into `collect_own_decoration_rects` +
`collect_peer_rects`.
- own_cursor cleared on BufferSnapshot (prior-buffer offsets).
Tests: `translate_key_maps_motion_named_keys_and_chars`,
`translate_key_carries_modifiers`. pmacs-gpu unit 18 (+2).
Gates green: fmt; clippy --all-targets --workspace -D warnings (default
+ crdt); pmacs-gpu unit 18. Daemon/lib untouched.
NOT YET VISUALLY VALIDATED — per the Phase B framing's process
correction, this must be confirmed in a running pmacs-gpu (arrow keys
move the caret + own current-line wash; TUI unaffected) before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The debug aid confirmed peer presence arrives with real selections
(sel=Some { anchor: 1837, active: 1844 }), yet no wash drew — because
the rect geometry was computed against the wrong coordinate space.
cosmic-text's `LayoutGlyph::{start,end}` are byte offsets within the
*original line* (`LayoutRun::line_i`), not the whole buffer.
`push_glyph_extent_rects` was comparing those line-relative offsets
against whole-buffer byte ranges from presence/`source_line_range`.
They only coincide on line 0, so any Selection or CurrentLine past the
first line never matched a glyph and produced no rect — "blind." This
was latent since 9.1 (Selection was never visually validated) and was
masked in 9.2 whenever the cursor happened to sit on line 0.
Fix: build `line_byte_offsets(current_text)` — the buffer-absolute
start of each `\n`-delimited line — and rebase each run's glyphs by
`line_offsets[run.line_i]` before comparing. Computed once per
`peer_background_rects` call and threaded into
`push_glyph_extent_rects`.
New test `line_byte_offsets_indexes_each_logical_line`.
Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu
unit 16 (+1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Manual retest still showed nothing. Two changes to localize and fix:
1. CurrentLine alpha 0.08 → 0.22. The original value computed to only
~10/255 above the dark clear color and was swamped by glyphs on any
text line — effectively invisible even when the wash was being
drawn correctly. 0.22 reads as a current-line band while staying
below Selection's 0.30. Static analysis of the full wire path
(daemon sweep → multi_frontend broadcast → reader → apply_attach_
message → peer_background_rects) found no break, so faint alpha is
the leading explanation for "still nothing."
2. Env-gated diagnostic in the PresenceUpdate arm. Running with
`PMACS_GPU_DEBUG_PRESENCE=1` prints each received presence
(frontend, buffer, current buffer, cursor, selection). If presence
lines appear, the wash geometry/alpha was the issue; if none appear,
the broadcast isn't reaching the mirror and the next step moves to
the daemon side. Off by default — no effect on normal runs.
Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu
unit 15.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Manual validation of 9.2 surfaced finding QB1: the CurrentLine wash
never appeared, and 9.1's Selection never actually rendered either.
Root cause: `Selection` and `CurrentLine` are the only two
per-WINDOW-state decorations; every other rendered family (StyleSpans,
diagnostics, inlay hints, minimap) is keyed to the shared BUFFER. The
producer emits both from the viewing frontend's own window
(`active_window_for(self.frontend_id)`). pmacs-gpu is a read-only
mirror with no input path — it never sends Key/cursor events, so its
own window's cursor stays pinned at 0 and its selection stays None.
Both decorations are therefore inert in pmacs-gpu: CurrentLine paints
a static line-0 wash (invisible at alpha 0.08) and Selection never
appears. What the user actually watches is the *editing* frontend's
(their TUI's) cursor — which is peer presence.
Fix is consumer-only — no producer or protocol change. The wire
already carries it: `InstanceMessage::PresenceUpdate { frontend_id,
buffer_id, cursor, selection }` is broadcast by the daemon to every
`multi_frontend` recipient, pmacs-gpu already negotiates
`multi_frontend: true`, and it was simply dropping the message at its
`_ => None` catch-all.
Q#5 (recorded in the framing doc): peer presence is the authoritative
cursor/selection source for a read-only mirror.
- New `peer_presences: HashMap<FrontendId, PeerPresence>` state,
cleared on BufferSnapshot (peer offsets are prior-buffer-relative).
- New `PresenceUpdate` arm stores per-peer (buffer_id, cursor,
selection) and requests a redraw.
- `peer_background_rects` replaces the old
`decoration_background_rects`: renders `CurrentLine` over the source
line holding each peer's cursor (`source_line_range`) and
`Selection` over each peer's selected range, both via the shared
`push_glyph_extent_rects` (the former inline glyph-overlap loop,
extracted). Own-window Selection/CurrentLine in `current_decorations`
are no longer drawn as backgrounds — they're inert for a read-only
mirror. Diagnostic (foreground) decorations are untouched.
- The producer keeps emitting own-window Selection/CurrentLine (9.1/
9.2) unchanged — correct and forward-looking for when pmacs-gpu
gains its own input in Phase B; simply unconsumed-for-backgrounds by
the mirror today.
Deferred within the stance (documented): per-peer stable colors (single
peer reuses the Selection/CurrentLine colors), peer caret glyph +
"user N" label, and own-vs-peer cursor merge once input lands.
New tests: `source_line_range_locates_enclosing_line` +
`source_line_range_handles_empty_and_leading_newline`. The peer
rect generation itself needs a laid-out buffer (font system) and is
covered by the manual probe.
Two pre-existing functions tipped past clippy's 100-line limit by the
additions (`State::new` 101, `apply_attach_message` 116, a per-variant
match dispatcher); both get `#[allow(clippy::too_many_lines)]`,
matching the precedent on `semantic_render::render_frame`.
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 (+2 source_line_range tests)
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2
Manual probe: daemon + TUI attach + pmacs-gpu attach. Move the cursor
in the TUI — pmacs-gpu's CurrentLine wash should track the TUI's line.
Select text in the TUI — the Selection wash should mirror it. Both
should now actually appear and follow the editing frontend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Retires the first half of Phase A's deferred finding A8 (background-
bearing decoration kinds couldn't render through cosmic-text's
foreground-only `Attrs`). `DecorationKind::Selection` now paints a
translucent blue rectangle under the selected glyphs in pmacs-gpu,
reusing the wgpu `QuadRenderer` that shipped for the minimap in
session 7.
The framing doc (`docs/pmacs-gpu-quad-backgrounds-framing.md`)
commits the load-bearing decisions before code lands: stance (α)
for Q#2 — single render pass, three draws in the order backgrounds
→ text → minimap — is what this commit implements. Q#1 (CurrentLine
source location, stance α: producer-side from
`core.active_window_for(self.frontend_id).cursor`) and Q#3 (per-line
cadence, stance β) are sketched for session 9.2; Q#4 defers search
backgrounds awaiting an upstream pmacs search feature.
Three components:
1. `decoration_kind_to_bg_color` helper, sibling of the existing
`decoration_kind_to_color`. Returns `Some([f32; 4])` RGBA for
Selection; `None` for CurrentLine (9.2), SearchMatch /
SearchMatchActive (deferred), and the four diagnostic kinds
(foreground-only). New unit tests assert disjoint total cover
between the two helpers across the eight kinds.
2. `State::decoration_background_rects` walks
`Buffer::layout_runs()`, finds glyphs whose `[start, end)`
overlaps each background-bearing decoration's `ByteRange`, and
produces one `MinimapRect` per laid-out visual line that
contributes glyphs. Multi-line selections fan out as N rects.
3. Render-order change in `State::render`: a `bg_buffer` is built
ahead of the minimap buffer and drawn first in the render pass
(before `text_renderer.render`), so selection fills sit under
the glyphs with the 0.30-alpha letting source color show through.
Minimap continues to draw last.
Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs-gpu unit: 13 (+2 new bg-color helper tests)
- pmacs lib: 1325, pmacs-protocol: 11
- m4_acceptance: 88, m11_5_semantic_acceptance (--features crdt): 2
Bet exercise so far: bet #1 (multi-line vertex decomposition) is
implicitly tested by the layout-run loop but waits on visual
validation for honest scoring. Bet #2 (overlap composition) is not
exercised in 9.1 — Selection is the only background kind, so no
overlaps with CurrentLine or future kinds. Bet #3 (cadence) is a
9.2 concern.
Manual probe: launch daemon + TUI attach + pmacs-gpu attach against
any file, select text in the TUI, verify the pmacs-gpu window paints
a translucent blue rectangle over the selected glyphs that tracks
selection extension. Multi-line selection should produce per-visual-
line rectangles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The clear-on-CrdtOp change broke the producer's incremental-update
contract. The producer ships dirty-range spans only on `full=false`
frames; the frontend is expected to retain non-dirty spans across
edits. Emptying both vectors meant the frontend ended up with only
the small dirty-range spans, missing the rest of the viewport — all
colors disappeared after an edit.
Reverting here. The proper fix lives in pmacs core (T M11.7):
producer must force `full=true` on generation transitions so the
frontend gets a complete replacement set on every text edit. Once
that lands, session-5's CrdtOp handler doesn't need to clear
anything — the next frame's `full=true` does it via
`replace_style_spans` / `replace_decorations`.
This reverts commit 49785c4. Returns the consumer behavior to
session-5's original "one-frame stale" artifact pending the core
fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Surfaced during session-5 manual validation (probe #3, the bet-#1
shape from the framing pass): editing at a diagnostic boundary
left stale color fragments visible against now-different text. The
session-4 PR documented this as a "one-frame stale" artifact, but
in practice the LSP re-analysis window stretches the wrong-color
period to 100ms–5s — human-perceptible and confusing.
Mechanism: CrdtOp updates `current_text` but `current_spans` /
`current_decorations` still index into pre-edit byte positions.
`reshape()` paints them at those stale positions against the new
text, producing colored fragments over wrong characters until the
producer ships an updated frame. Between CrdtOp arrival and
clangd's next publishDiagnostics (LSP debounce + re-analysis), no
Decorations frame ships at all (the producer's
`changed_intervals(prev, curr)` sees identical sets because clangd
hasn't republished yet).
Fix: drop both vectors in the CrdtOp arm before `set_text`. Tree-
sitter re-emits StyleSpans within ~one frame; LSP decorations
re-emit when clangd republishes. Cost = a brief uncolored window
per edit. Gain = no wrong-position color persists.
Manual revalidation (post-rebase on #44 + #45 + #46):
edit-at-boundary in the TUI now drops the old diagnostic color
cleanly. New diagnostic colors paint once clangd republishes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second Phase A session. pmacs-gpu now consumes
`InstanceMessage::Decorations` with the same M11.4 dirty-merge shape
as `StyleSpans`. Diagnostic kinds render as foreground color
overrides; background-needing kinds (selection, search match, current
line) accumulate in state but stay unpainted pending a quad pipeline.
What's wired
- `State.current_decorations: Vec<Decoration>`, sorted by
`range.start`, cleared on `BufferSnapshot` like `current_spans`.
- `apply_attach_message` gains a `Decorations` arm — `full=true` →
`replace_decorations`, `full=false` → `merge_decorations` (M11.4
clip/drop/split, structurally identical to `merge_style_spans`).
- `reshape()` rewritten as a sorted-boundary sweep over both spans
and decorations: every coverage edge becomes a chunk break.
Effective fg color = first matching decoration with a renderable
color, else span color, else default.
- `decoration_kind_to_color`: red error / yellow warning / blue info
/ dim hint; selection/search/current-line return `None`.
Session-5 findings (rule iii, both deferred)
- **M11.4 merge logic duplicated** between `StyleSpan` and
`Decoration`. Structural-but-minor; defer until a third instance
surfaces (peer-cursor decorations from `PresenceUpdate` are the
likely third point) so the generic shape is inducted from three
examples, not two.
- **Background-kind decorations need a wgpu quad pipeline**. glyphon
0.11 / cosmic-text 0.18 `Attrs` is foreground-only. Structural —
new render pass + composition story with text. Its own session,
not absorbed into Phase A.
Adversarial-verification framing
Probe #5 (active diagnostics + multi-frontend `PresenceUpdate`
overlap) — the diagnostics half is exercised; PresenceUpdate is its
own family and isn't consumed yet. Probe #3 (viewport-boundary
edges) gets re-tested: `merge_decorations` is the same code shape as
`merge_style_spans`, so an edge-case finding there would replicate.
Gates
`cargo fmt`; `cargo clippy --all-targets --workspace -D warnings`
clean; lib 1303 + protocol 11 = 1314; m4 83; m11_5 (--features crdt)
2.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First Phase A session. pmacs-gpu now sends FrontendEvent::Viewport
back to the daemon after BufferSnapshot lands, receives the resulting
InstanceMessage::StyleSpans frames, and renders the rope with
per-span colors via cosmic-text's set_rich_text.
What's wired:
- AttachClient gains a write-side Arc<Mutex<UnixStream>> and the
assigned FrontendId from Hello; new send_viewport method emits
FrontendEvent::Viewport. The mutex is over-cautious for our
single-threaded event loop but future-proofs against multi-window
emission.
- State adds current_buffer_id and current_spans (sorted by
range.start). BufferSnapshot bootstraps both, then the App's
user_event handler emits a follow-up Viewport via
AttachClient::send_viewport.
- StyleSpans handling distinguishes full vs incremental:
* full=true: replace_style_spans drops prior, takes segments as
authoritative for the declared viewport.
* full=false: merge_style_spans applies the M11.4 dirty-segment
rule — spans fully inside a dirty range drop; spans straddling
a dirty edge get clipped to outside the range (with the
straddles-both-edges case splitting into two); new spans append;
re-sort by start.
- reshape() walks current_text + current_spans, emits (substr,
Attrs) chunks at every span boundary (with .min(text_len) clamps
for safety against stale spans past EOF), calls set_rich_text.
- cell_color_to_glyphon converts cell::Color to glyphon::Color via
the standard xterm-style 256-color palette (16 ANSI + 6x6x6 cube +
24-step grayscale). Default → None so the renderer's default
Attrs color stays.
Adversarial verification scope (Phase A probes):
- #1 non-ASCII source: exercised through the UTF-16 col/byte
conversion already in pmacs's producer side; pmacs-gpu just renders
what the wire delivers. Non-ASCII files should show correct
styling at the right byte positions.
- #3 viewport-boundary tokens: the merge_style_spans path is exactly
bet #1 from the framing pass ('StyleSpans/Decorations dirty-segment
edges at viewport boundaries — headless-test-blind-spot probe').
Edits near a span edge exercise the clip-and-merge logic.
- #6 CRLF line endings: implicit — pmacs's rope uses byte offsets so
styling spans naturally include or exclude the \r as the producer
decided. pmacs-gpu doesn't special-case line endings.
Known limitation (session 4 acceptable artifact, documented in
set_text): CrdtOp + StyleSpans arrive separately. CrdtOp updates text;
StyleSpans for the new generation comes one tick later. Between the
two, current_spans points at pre-edit byte positions while the text
is post-edit — visually stale for one frame. The .min(text_len) clamp
in reshape() keeps it safe; the artifact is brief.
Headless test gap: there's no Rust-level test of merge_style_spans
or the rich-text segmentation. Phase A's framing intentionally
chose manual validation over headless tests for these paths (the
adversarial probes are visual). A Phase A audit doc lands at session
close with the predicted-vs-actual scoring; per-method unit tests
for the merge logic could land then if findings argue for them.
Gates: fmt; clippy --all-targets -D warnings clean across the whole
workspace; lib 1303 + pmacs-protocol 11 = 1314; m4_acceptance 83;
m11_5_semantic_acceptance --features crdt 2.
Manual validation pending — same daemon+TUI+pmacs-gpu setup as
session 3, now showing colored text in the GPU window.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced during manual validation: the pmacs-gpu window sits on
'(connecting...)' forever when attaching to a daemon built without
--features crdt. Handshake succeeds (negotiation reports semantic_render
+ crdt_replica as agreed by both sides), but the daemon's
crdt_replica default is cfg!(feature='crdt')=false in that build, so
send_buffer_snapshots() never fires and pmacs-gpu has nothing to
render.
Classified small under rule (iii). The structural answer (should the
daemon return a clearer signal when crdt_replica was negotiated but
isn't actually compiled in?) is genuine but deferred; for session 3
the failure mode is now documented inline at the build-AttachRequest
site so the next user to hit it recognizes the symptom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pmacs-gpu now has two run modes:
- no args: hello-world (session-2 behavior preserved)
- --attach <socket>: connect to a pmacs daemon, negotiate
semantic_render + crdt_replica, import BufferSnapshot into a local
loro replica, render the rope text. Live CrdtOp updates apply as
they arrive.
Architecture:
- pmacs-gpu/src/attach.rs (new): UnixStream connect + Hello /
AttachRequest handshake on the main thread; spawns a reader thread
that pumps decoded InstanceMessage frames through the winit
EventLoopProxy as AppEvent::Attach(AttachEvent::Message). Clean EOF
or transport errors surface as AttachEvent::Disconnected. Reader
thread holds the read half of the stream; AttachClient retains the
write half (unused yet — session 4 wires FrontendEvents back).
- pmacs-gpu/src/main.rs: ApplicationHandler<AppEvent> with a
user_event handler that dispatches Message variants. BufferSnapshot
builds a fresh LoroDoc, imports the snapshot bytes, extracts text
via doc.get_text('body').to_string(), and re-shapes the glyphon
buffer. CrdtOp passes the op bytes through doc.import (loro
accepts both shapes), re-extracts text, re-shapes. Other
InstanceMessage variants are intentionally ignored at session 3.
- Font size dropped from 48pt to 16pt now that we may render full
files (the hello-world 48pt was fine for one line, awful for code).
- Initial text is '(connecting...)' in attach mode, 'hello, pmacs' in
hello-world; attach failure falls back to '(attach failed; see
stderr)' so the window still opens.
One small finding logged in attach.rs's connect() doc: AttachRequest's
initial_size field is a CellSize (rows × cols), nominally
TUI-shaped. Sent as a placeholder (24×80) — a structural answer
('what does initial size mean for a pixel frontend?') belongs in its
own protocol thread, not session 3. Classified under rule (iii) as
deferred.
Container id for the loro text container ('body') hardcoded to match
pmacs::crdt::CrdtState — second finding worth pre-recording: the
container name is a wire-adjacent convention that isn't carried on
the wire itself. Both ends have to agree out-of-band. Not blocking
for session 3 but a structural smell for the producer arc. Logged
as deferred (rule iii structural; the answer is probably 'thread the
container id through BufferSnapshot' but it's not session-3 scope).
Gates: cargo fmt, cargo clippy --all-targets -D warnings (whole
workspace) clean; lib 1303 + pmacs-protocol 11 = 1314 unchanged;
m4_acceptance 83; m11_5_semantic_acceptance --features crdt 2.
Manual validation pending — agent environment is headless. User
walks through: start a pmacs daemon, run pmacs-gpu --attach <socket>,
confirm the window renders the daemon's file contents.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the pmacs-gpu binary crate to the workspace. wgpu 29.0 + winit
0.30 + glyphon 0.11 (cosmic-text 0.18 via re-export) + pollster +
env_logger; pmacs-protocol in the dep graph but not consumed yet
(session 3 wires the attach loop).
The binary opens an 800x200 window titled 'pmacs-gpu hello-world',
sets up wgpu against its surface, configures glyphon with the bundled
JetBrains Mono Regular, and renders 'hello, pmacs' once per redraw.
Close button or Escape exits. Resize re-configures the surface and
glyphon viewport. Surface acquisition matches wgpu 29's
CurrentSurfaceTexture enum (success/suboptimal render through; lost/
outdated re-configure; timeout/occluded skip the frame).
Bundled assets: pmacs-gpu/fonts/JetBrainsMono-Regular.ttf (268 KB)
and pmacs-gpu/fonts/OFL.txt. Font shipped as required by the SIL
Open Font License 1.1.
One finding surfaced during the move and absorbed under rule (iii)
of the framing pass (small / no structural change): the design doc
recorded JetBrains Mono as Apache 2.0; the actual license has been
OFL since the family's open-source release. Doc corrected in
docs/pmacs-gpu-design.md.
Gates: cargo fmt + cargo clippy --all-targets -D warnings clean for
the whole workspace; cargo test --lib still 1314 (pmacs main crate
untouched); m4_acceptance 83; m11_5_semantic_acceptance --features
crdt 2.
Visual confirmation pending — agent environment is headless, so
'window opens, text renders' is user-side validation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>