# Auto-indent on newline — framing (Arc 2, editing table stakes) Pressing RET in pmacs inserts a bare `\n`; every indented line means re-typing the indentation by hand. This adds the table-stakes behavior: RET carries the current line's indentation onto the new line, language-agnostic, as one undoable edit. Second-to-last Arc 2 item; auto-pairing follows as its own framing + PR. Roadmap: `docs/roadmap-2026-07.md` Arc 2 ("auto-indent on newline"). Revision 6 (PR #109 round 1): Q#AI8's invalidation is one helper invoked from all four edit paths — dispatch, direct notification, undo, redo — and the empty-anchor clear moved daemon-side, closing the optimistic case for BOTH frontends (the residual was never GPU-only: the TUI mirror tracks no selection state). The acceptance matrix now matches what the suites actually pin. Earlier revisions: success-gated Q#AI9, live-origin translation, M6.4 same-kind intercept contract, modal-context corrections. ## Ground truth (as of `efa41cb`) - **RET → `buffer.newline` → `ed.insert_char_over_region(10)`** (`builtin/keymaps/default.lua:52`, `builtin/commands/default.lua:110`): one `Replace` when a region is active (CUA type-over, one undo step), plain insert otherwise. The no-region path does **not** clear an empty selection, and `Window::region()` returns `None` when anchor == cursor while the selection object persists (`src/window.rs:244-255`). So a zero-length selection (e.g. `S-Left` at BOF) survives the edit, and the moment the edit advances the cursor away from the anchor the region is **already nonempty** — the very next self-insert type-overs the just-inserted text. The same no-region arm serves `buffer.self-insert` and `buffer.tab`, so ordinary typing has the identical bug today (`S-Left` at BOF, `x`, `y` → the `y` type-overs the `x`) — it is not RET-specific. `insert_char` catches the intercept rejection internally — status message, return `()` (`src/editor_core.rs:1681-1692`) — so its caller cannot observe failure; and `insert_char_over_region`'s doc comment already claims "any selection is cleared" while only the region arm does it (`src/editor_core.rs:1698`, `:1717`). - **Runtime modules are individually embedded** — each `builtin/runtime/*.lua` has an explicit `include_str!` + eval entry in `src/editor.rs` (comment.lua's at `src/editor.rs:336`). There is no discovery mechanism; a new module ships with a loader entry or it never loads. - **The two frontends' Enter paths diverge.** The TUI attach client round-trips Enter to the daemon keymap by design — `src/optimistic.rs:90-93` names "indentation, newline-with-indent" as the reason, locked by `classify_enter_and_tab_are_round_trip`. The GPU frontend treats plain Enter as optimistic-eligible (`optimistic_insert_text`, `pmacs-gpu/src/main.rs:1520-1530`) and ships a raw `"\n"` CRDT op that never touches the keymap — justified in its doc comment by "`buffer.newline` reduces to plain `insert_char(10)`", the exact premise this feature invalidates. The GPU already round-trips Enter when a selection is active, the completion popup is open, or the dispatcher is busy/stale. The selection gate reads `DecorationKind::Selection` decorations (`pmacs-gpu/src/main.rs:2054-2060`); an **empty** selection paints no decoration, so it does not gate — GPU optimistic typing proceeds over an empty anchor. - **Consequence of the GPU bypass: RET rebindings are dead in the GPU frontend.** The classic buffer list binds RET buffer-locally to `editor.buffer-list-visit` (`builtin/commands/default.lua:407-411`) through normal dispatch; unlike listview it is not marked for round-trip input, so GPU RET there inserts a raw newline instead of visiting. Any user rebind of RET is bypassed the same way. - **Dispatch supplies an empty active-mode list** — the keymap stack is resolved with `&[]` for modes (`src/editor.rs:710`), so mode-scope bindings do not resolve anywhere today, on any frontend. - **GPU test seams**: `optimistic_insert_text` is a free function with an existing in-crate unit test (`pmacs-gpu/src/main.rs:7058-7104`), and it is the sole gate at the top of `optimistic_crdt_insert` (`:2077-2079`). The eligibility/routing layer above it lives in private `App`/`State` methods (`:2050`, `:2077`) with no constructible unit seam — the winit handler's fall-through to `send_key` is not unit-reachable. `pmacs-gpu` declares **no cargo features** (`pmacs-gpu/Cargo.toml`); `crdt` is a root-package feature only. - **Modal contexts that consume Enter before the keymap**: isearch accept (`src/editor.rs:1831`), context-menu invoke (`src/editor.rs:1934`), completion-popup accept (`src/editor.rs:1984`), minibuffer accept (`src/minibuffer.rs:482`), query-replace prompt (`src/editor.rs:968`). These never reach the global keymap; a RET rebind cannot touch them. Acceptance pins them anyway. - **Search staleness is asymmetric across edit paths, and staleness is only half-honored by consumers.** Accepted isearch matches deliberately stay highlighted "until the next edit marks them stale" (`src/editor_core.rs:861-865`). `apply_active_edit` — the path today's `buffer.newline` takes — honors that via `SearchStore::mark_stale` (`src/editor_core.rs:1184-1193`), and highlight producers suppress stale matches. But `search_step` navigates stored ranges without checking `is_stale` (`src/editor_core.rs:844`), and `search_match_summary` exposes stale counts to the n/m prompt (`src/editor_core.rs:719`). `SearchStore::set` clears staleness (`src/search.rs:100`), so any pattern re-run refreshes. Meanwhile direct buffer edits notify through `notify_buffer_edit` (`src/editor_core.rs:1207`), which refreshes views/overlays only — it never marks search state stale. Its **three** callers: the CRDT-op apply path (`src/daemon.rs:2133`), the general Lua mutator path (`notify_buffer_edit_to_windows`, `src/lua_bindings/mod.rs:1390-1395`), and the LuaHost errors-buffer append (`src/lua.rs:442`). A pre-existing stale-highlight/stale-step bug for all three, which a Lua-implemented RET would inherit on the most common keystroke. - **A live search's origin is a raw byte offset.** `SearchSession` stores `origin: (BufferId, byte)` at `search_begin` (`src/editor_core.rs:740-753`); every recompute focuses from that unchanged offset (`src/editor_core.rs:803-810`), and cancel restores the cursor to it directly (`src/editor_core.rs:866`). No edit path translates it — an insert or delete strictly before the origin skews both the recompute focus and the cancel restore even when the match set is fresh. (`apply_active_edit` marks stale right there yet leaves the origin alone, so the other-frontend dispatch path carries the same pre-existing skew.) - **Direct buffer edits do not reconcile window state** — `notify_buffer_edit` adjusts no cursors and no selections. An intercept that expands a replace past the cursor can leave `cursor > buf:len()` and a dangling selection; nothing downstream clamps. The daemon's optimistic-CRDT arm shows the canonical repair: right-gravity cursor translation through the effective edit (`src/daemon.rs:2103-2130` — `pos < start` → unchanged; `pos > pre-edit end` → `pos - old_len + inserted_len`; within → `start + inserted_len`). Note the mutators' returned triple `(start, end, inserted_len)` has `end` = the **pre-edit** replaced-range end (`src/lua_bindings/mod.rs:1246`); the post-edit end is `start + inserted_len`. - **Intercepts may only move an edit, never change what it does.** M6.4 forbids kind-changing transforms and keeps the payload bytes immutable; same-kind position/range overrides are the entire surface (`src/lua_bindings/mod.rs:1061-1074`, `:1653-1657`). Consequences: an intercepted `insert` can only be **relocated** — the buffer always grows by the payload — while only an intercepted `replace` can **shrink** the buffer, by expanding its replaced range past the payload length. - **Intercepts can switch buffers and windows.** Phase 2 of `run_managed_edit` runs the intercept chain with the registry borrow released (`src/lua_bindings/mod.rs:1287-1324`); an intercept body may legally change the active window or buffer. Any post-edit fix-up that blindly targets "the active window" can corrupt unrelated state. `pmacs.window.current()` exposes the active window id to Lua (`src/lua_bindings/mod.rs:10617-10622`). - **No indent infrastructure exists.** No indent command; TAB inserts a literal `\t` (`buffer.tab`); there is no tabs-vs-spaces or width setting anywhere (config registry is a standing deferral); grammars drive highlighting only — no `indents.scm` in the repo. Hardcoded tab widths exist at **five** divergent sites: `TAB_WIDTH = 8` in four core renderers (`src/text_view.rs:35`, `src/highlight.rs:228`, `src/diag.rs:364`, `src/completion.rs:594`) and the GPU minimap's own leading-whitespace scan at width **4** (`pmacs-gpu/src/main.rs:5510`, `:5529-5531`; frontend-local, not reusable for editing). - **The comment.lua pattern covers the Lua mechanics**: line scans over `buf:slice` (`line_start_before`, `builtin/runtime/comment.lua:47-57` — there is no `buf:line()` API; the scan is the idiom), `ed.cursor()/region()/goto_byte()/ clear_selection()`, pcall'd mutators with the exact effective-edit triple check. `ed.goto_byte` clamps to buffer length. - `C-j` is bound nowhere in `builtin/`. ## Decisions ### Q#AI1 — Lua feature + four named Rust touches `builtin/runtime/indent.lua` (new), following the comment.lua module shape. Rust changes, each deliberate and small: 1. **Loader entry** in `src/editor.rs` — `include_str!` + eval after the comment.lua entry (`:336`). Without it the module never loads and RET would resolve to an undefined command. (Defining the command in `builtin/commands/default.lua` was the alternative; a feature with helpers gets its own module per the comment.lua precedent.) 2. **Search-stale substrate fix** (Q#AI8): mark stale in the shared notify path, make the two stale-blind consumers fail closed, and translate the live origin. 3. **GPU eligibility fix**: remove the `ProtocolKey::Enter` arm from `optimistic_insert_text` so plain Enter round-trips through the daemon keymap, as the TUI already does. The arm's own documented justification (byte-identical to what the daemon would do) stops being true the moment RET means newline-and-indent. Tab keeps its arm: `buffer.tab` is still a plain `insert_char(9)`; the doc comment is rewritten Tab-only. 4. **Empty-selection core fix** (Q#AI9): the no-region arm of `insert_char_over_region` clears a lingering selection. ### Q#AI2 — Command + binding One new command, `edit.newline-and-indent` (the Emacs name), and the default keymap's RET line changes to it. `buffer.newline` keeps its command definition and plain-newline role — the escape hatch via `M-x` or a rebind — unchanged, with one deliberate exception: its empty-selection behavior improves through the shared primitive (the Q#AI9 fix). No new default chord for plain newline in v1: the natural candidate `C-j` is LF itself — the same terminal-ambiguity class as the C-/ undo note in the comment-toggle framing — so it's a named deferral, not a casual bind. ### Q#AI3 — Indent = verbatim copy, clipped at the split point On the line containing the split point: indent = bytes[line_start .. min(first_non_ws, split_point)] where whitespace is space/tab only. Insert `"\n" .. indent` as ONE edit; cursor lands after the indent. - **Verbatim copy** (not width math): with no tabs-vs-spaces or width config in the editor, reproducing the existing bytes is the only policy that cannot be wrong about the file's convention. Tabs, spaces, and mixed runs all round-trip untouched. - **Clip at the split point**: splitting *inside* the leading whitespace would otherwise double-indent the carried text. Clipping preserves the carried text's total indentation exactly — `··|··foo` → `··` / `····foo` (split after 2 of 4 spaces: new line still starts at column 4). - **No language awareness in v1**: no extra indent after `{`, no dedent of `}`. Electric indent needs a per-language width opinion pmacs has no surface for (deferral, below). - Whitespace-only lines copy what's before the split point; the abandoned line keeps its trailing whitespace (cleanup is a named deferral, not an accident). ### Q#AI4 — Region type-over preserved; selection always cleared Region active → one `buf:replace(start, end, "\n" .. indent)`, indent computed on the line containing the region start (clipped at region start); cursor after the indent. Matches `insert_char_over_region`'s single-`Replace` / one-undo-step / one-CRDT-op contract that the GPU's selection round-trip relies on. **The selection is cleared after every successful edit, region or not.** A zero-length selection (anchor == cursor) reports no region, so the edit takes the no-region path — but the edit itself advances the cursor off the anchor, making the region nonempty **immediately**: the very next self-insert type-overs the newline (`S-Left` at BOF, RET, `x` → the `x` replaces the newline). Unconditional `clear_selection()` closes that for this command. (Q#AI9 fixes the same lingering-anchor bug in the core command family; this command edits via `buf:insert`/`buf:replace`, not `insert_char_over_region`, so it must clear its own.) ### Q#AI5 — Edit discipline: comment.lua contract + window-state safety Snapshot first, edit second, guarded fix-up third: 1. **Snapshot** `pmacs.window.current()` and the target buffer id before the mutator runs. 2. `pcall`'d mutator; a rejecting intercept → status message, no edit, no throw, no dangling state. 3. **Guard**: intercepts run with the registry borrow released and may switch the active window or buffer (ground truth). If the active window or its buffer no longer matches the snapshot, skip all fix-up — report only; touching whatever window is now active would corrupt unrelated state. 4. **Cursor repair, one formula for both paths**: translate the pre-edit cursor through the *effective* edit with the established right-gravity shape (`src/daemon.rs:2103-2130`): `pos < start` → unchanged; `pos > pre-edit end` → `pos - old_len + inserted_len`; within the replaced range → `start + inserted_len`. Then `goto_byte` (which clamps). For the clean insert-at-cursor case this lands exactly at `start + inserted_len` — the normal after-the-indent position — so there is no special-cased "clean" placement to drift from the repaired one. Never jump to a fixed edit endpoint: an intercept that relocates the edit elsewhere in the buffer must not teleport the cursor there. 5. The effective triple is compared EXACTLY against the request; on deviation → status *"newline-and-indent altered by buffer intercept"*; the interceptor's **positional** result stands — kind and payload are immutable under the M6.4 contract (ground truth). Concretely: the plain path's `buf:insert` can only be relocated, so the buffer always grows and the `"\n" .. indent` payload lands wherever the intercept moved it; only the region path's `buf:replace` can shrink the buffer, via a same-kind range expansion. This is a named deviation from the comment.lua precedent (which skips fix-up entirely): skipping here would bless `cursor > buf:len()` whenever an expanded replace shrinks the buffer past the cursor. 6. `clear_selection()` (Q#AI4), under the same context guard. ### Q#AI6 — Consequences of the GPU change, named - Plain Enter in the GPU costs one daemon round-trip before echo — parity with every TUI keypress and with the GPU's own selection/popup/busy Enter today. - **Global and buffer-local RET rebindings become effective in the GPU frontend.** Concretely: buffer-list RET now visits the selected buffer instead of inserting a raw newline into the list (today's behavior — the optimistic path bypasses the buffer-local binding). This is an incidental bug fix, named and tested, not a side effect. **Mode-scope bindings are excluded from this claim**: dispatch resolves the keymap stack with an empty mode list (`src/editor.rs:710`), so they stay unresolved on every frontend until the mode system is wired. - `this_command` after Enter in the GPU becomes `edit.newline-and-indent` (was `buffer.self-insert` via the exact-decode CRDT classification) — now consistent across frontends; kill chains break across a newline in both (already true in TUI). - GPU unit tests locking Enter's optimistic eligibility flip to lock the round-trip. ### Q#AI7 — Chain/hook plumbing: nothing to build Keybound RET rotates the command boundary and `buffer.after-edit` fires from dispatch's revision check; `M-x edit.newline-and-indent` gets both via `invoke_interactive`. Same Arc 2 substrate as comment-toggle; the acceptance suite asserts the hook fires once anyway. ### Q#AI8 — Search staleness: mark it, honor it, and keep the origin true Three parts, all required: 1. **Mark**: one helper, `search_invalidate_for_edit` (mark stale + translate the origin), invoked from **all four edit paths**: `apply_active_edit` (dispatch), `notify_buffer_edit` (applied CRDT ops `src/daemon.rs:2133`, general Lua mutator edits `src/lua_bindings/mod.rs:1390-1395`, and the errors-buffer append `src/lua.rs:442` — normally a no-op, unless the *errors* buffer itself carries accepted search state, in which case marking it stale is exactly right), and — round-1 finding — `undo` / `redo`, which receive precise `Edit` values but previously invalidated nothing. Generated-buffer rebuilds via `rebuild_views_for` remain a named lower-frequency bypass (deferral). 2. **Honor (fail closed)**: `SearchStore::step` returns `None` while stale — C-s / `search.next` stops navigating byte ranges that no longer exist instead of teleporting the cursor to them — and `search_match_summary` reports `(None, 0)` while stale, so the TUI/GPU n/m prompt cannot show counts for suppressed highlights. The consumers then match what the highlight producers already do. A stale **live** search un-sticks on the next pattern keystroke: the re-run calls `SearchStore::set`, which clears staleness (`src/search.rs:100`). Auto-recomputing from the stored query on step (instead of failing closed) is a named deferral — nicer UX, separate change. 3. **Translate the live origin**: a small `EditorCore` helper right-gravity-translates `SearchSession::origin` through the effective edit, invoked alongside `mark_stale` in **both** `apply_active_edit` and `notify_buffer_edit` (the formula is the `src/daemon.rs:2103-2130` shape; the dispatch path has the same pre-existing skew, and the two call sites already mirror each other for `mark_stale`). Without it, a fresh recompute focuses from a skewed offset and cancel restores the cursor to the wrong place whenever an external edit lands before the origin. Together these close a pre-existing bug family — lingering highlights, stale stepping/counts, and skewed origins after any direct Lua edit or optimistic GPU edit — that a Lua-implemented RET would otherwise put on the most common keystroke in the editor. ### Q#AI9 — Empty-selection type-over: fix the core arm, name the GPU residual The lingering-anchor bug is not RET's alone (ground truth): the no-region arm of `insert_char_over_region` leaves an empty selection armed, so ordinary typing already type-overs its own previous keystroke (`S-Left` at BOF, `x`, `y` → the `y` replaces the `x`). **In scope**: `insert_char` returns success — today it catches the intercept rejection and returns `()` (ground truth), so the caller cannot distinguish a landed edit from a rejected one — and the no-region arm clears the lingering selection **only on success**. Clearing unconditionally would mutate state on a *failed* self-insert/Tab/plain-newline, which the rejecting-intercept contract forbids. This fixes `buffer.self-insert`, `buffer.tab`, and the retained `buffer.newline` escape hatch for all daemon-dispatched input, on both frontends' round-trip paths, and makes `insert_char_over_region`'s existing "any selection is cleared" doc claim true. **Also in scope (round 1: the residual was never GPU-only)**: the TUI attach mirror tracks buffers and cursors but no selection state (`src/buffer_mirror.rs:113`), and its optimistic gate checks cursor freshness and EOL only (`src/optimistic.rs:250`) — so both frontends' optimistic paths could re-arm the type-over. The fix lives in the daemon's CRDT source arm (`handle_remote_crdt_op`): before applying the source cursor update, a selection whose anchor equals the pre-edit cursor — i.e. one that was EMPTY — is cleared. Nonempty selections stand untouched. **Out of scope, named**: the TUI optimistic gate performs no type-over check at all — a NONEMPTY selection ending at EOL optimistically inserts instead of consuming the region (the GPU gates on Selection decorations, which nonempty selections do paint, so it round-trips correctly there). That is a pre-existing TUI gate gap, deferred with the substrate reconciliation work. ## Bets 1. **Verbatim copy is the right v1 policy** — no tabs-vs-spaces complaints are possible when we only ever reproduce what's already on the line. 2. **GPU Enter round-trip latency is imperceptible** — it matches the TUI's every keypress and the GPU's existing non-optimistic paths. 3. **Clip-at-split matches muscle memory** — mid-indent splits preserving total indentation is what vi/Emacs users expect; nobody files "my line double-indented". 4. **Fail-closed staleness has no workflow regressions** — a stale step no-op (until the pattern re-runs) surprises nobody, because the highlights it would have navigated are already suppressed; nothing legitimate consumes stale offsets once step and summary are guarded. 5. **Clearing a lingering empty selection on the core insert arm breaks nothing** — no workflow depends on an anchor surviving plain typing; mainstream editors deactivate the mark on edit. ## Deferred (named) - Language-aware indent (electric `{`/`}`, tree-sitter `indents.scm`) — blocked on width/style config, which is blocked on the config registry deferral. - TAB as reindent (`indent-for-tab-command`) — TAB stays a literal tab, and stays GPU-optimistic. - Doc-comment continuation on newline — re-deferred from the comment-toggle framing; needs in-comment detection (grammar/comment-span work); its own framing after auto-pairing. - Strip the abandoned line's trailing whitespace on split. - A plain-newline default chord (`C-j` is LF — terminal-ambiguous). - `C-o` open-line-with-indent. - Substrate-level window-state reconciliation for direct buffer edits (cursor clamp / selection repair in the notify path, rather than per-command — including windows other than the acting one), and aligning comment.lua's transformed-intercept fix-up with Q#AI5's translate-and-clamp discipline. - **TUI optimistic type-over gate gap** (Q#AI9 round 1): the TUI attach gate consults no selection state, so a NONEMPTY selection ending at EOL optimistically inserts instead of type-over. (The empty-anchor half was fixed daemon-side in round 1.) - **Generated-buffer rebuilds** (`rebuild_views_for`) bypass search invalidation — a lower-frequency Q#AI8 gap, named not handled. - Auto-recompute of a stale search from the stored query on step (Q#AI8 fails closed instead). - Unifying the five hardcoded tab-width sites (4× core `TAB_WIDTH=8`, GPU minimap `4`) behind a real setting — config-registry work. - Mode-scope keybinding resolution (dispatch passes `&[]` today) — the mode system's wiring, not this feature's. - A constructible unit seam for the GPU routing layer above `optimistic_insert_text` (private `App`/`State` today). ## Acceptance (`tests/auto_indent_acceptance.rs`, dispatch-driven) - RET at EOL of a space-indented line: new line carries the indent, cursor after it; tab-indented and mixed `\t··` lines round-trip verbatim. - Mid-line split: `····foo|bar` → `····foo` / `····bar`, cursor before `bar`. - Split inside leading whitespace: `··|··foo` → `··` / `····foo` (clip rule — no double indent). - Zero-indent line and empty buffer: byte-identical to old `buffer.newline`. - Whitespace-only line: new line copies it; abandoned line keeps it. - Region active: ONE `Replace` — region replaced by `"\n" .. indent`, selection cleared; a single `buffer.undo` restores exactly. - **Zero-length selection, command level**: `S-Left` at BOF, RET, `x` → `x` inserts plainly, the newline survives (selection cleared unconditionally). **Core level (Q#AI9)**: `S-Left` at BOF, `x`, `y` → the `y` does not replace the `x`; likewise via `M-x buffer.newline`. **Failure regression**: empty selection armed, self-insert rejected by an intercept → the anchor REMAINS (no state mutation on a failed edit). The GPU-optimistic variant is the named residual, not claimed here. - One undo step in the plain case too. - Intercept discipline: rejecting intercept → status, buffer unchanged, no throw; transforming intercept → reported, positional result stands. **Relocating transform** (plain path: the insert's `pos` moved — the only transform an insert admits) → payload inserted at the new site, cursor translated, NOT teleported. **Shrinking transform** (region path: the replace's range expanded past the payload, shrinking the buffer below the cursor) → cursor right-gravity-translated and clamped within `buf:len()`, selection cleared — the tests assert validity, not immobility. **Context-switching intercept** (switches window or buffer mid-edit) → fix-up skipped; this proves the *new* context is untouched — the original window's validity after such an intercept belongs to the substrate-reconciliation deferral, and the test does not claim it. - **Search staleness** (Q#AI8; acceptance + lib split, per what each seam can reach): - Acceptance: isearch, accept, RET → post-accept `search_step` no-op; the same via a direct `buf:insert` (notify path). - Lib (editor_core): edit during **active** isearch → step no-op and summary `(None, 0)`, then the next pattern keystroke refreshes and stepping resumes; origin translation through **inserts and deletes on both edit paths** (dispatch and notify), pinning recompute focus and cancel restore; **undo and redo** stale accepted matches and translate the live origin. - Lib (search store): stale `step` fails closed; a fresh `set` un-sticks it. - Lib (daemon, `--features crdt`): the optimistic source arm clears an EMPTY anchor and leaves a NONEMPTY selection alone. - `after-edit` fires exactly once per RET (keybound and `M-x`). - Kill-chain break: `C-k`, RET, `C-k` → two ring entries. - Modal contexts: isearch accept and minibuffer accept pinned here (plus buffer-list RET below). Query-replace and completion-popup RET are pinned by `tests/query_replace_acceptance.rs` and `tests/completion_popup_acceptance.rs`; context-menu RET is pinned by `editor::tests::menu_enter_invokes_command_and_closes`. Those tests run in the gates and are not duplicated here. - Giant minified line (64 KiB, unindented and indented): splits correctly — pins the forward-chunked indent scan's behavior (boundedness is by construction). - Buffer-list RET still visits via dispatch (now also reachable from the GPU frontend). - `this_command()` after RET is `edit.newline-and-indent`. - **GPU coverage, two named seams (not "end to end")**: - In-crate (`pmacs-gpu`): the existing `optimistic_insert_text_covers_plain_chars_enter_and_tab` test flips — Enter returns `None`, Tab still optimistic. This is the sole gate at the top of `optimistic_crdt_insert` (`:2079`), so classifier-`None` forces the handler's round-trip branch; the handler itself has no constructible unit seam (named deferral). - Root package (`--features crdt`): a synthetic attached replica plus the root TUI optimistic orchestrator drive pending optimistic self-inserts followed by Enter on an indented line, asserting the daemon dispatches `edit.newline-and-indent` and the resulting multi-byte CRDT op reaches the replica with correct final text — the daemon side of the wire path the GPU takes once it round-trips.