From 98ef140a84e47fc72218767a4ad2c2c68a47cd52 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 29 May 2026 12:38:09 -0400 Subject: [PATCH] =?UTF-8?q?B1=20fix=20=E2=80=94=20route=20semantic-fronten?= =?UTF-8?q?d=20Key=20events=20into=20the=20editor=20core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual validation found "nothing occurs" when typing in pmacs-gpu. Root cause is daemon-side, not consumer-side: the dispatcher's catch-all arm only called `apply_event` (→ `dispatch_key`) when the source frontend had a `RenderState` — i.e. a grid frontend. A semantic frontend like pmacs-gpu has only a `SemanticRenderState`, so its `Key`/`Mouse`/etc. events hit the `else` branch and were silently dropped (the long-standing "M11.5 scope" posture). So pmacs-gpu's keys never reached the keymap; the cursor never moved. This contradicts the Phase B framing's "consumer-only" claim: the Explore fact-check verified `apply_event` → `dispatch_key` (true for grid frontends) but not that the dispatcher gates that call on `render_state`, so semantic-frontend keys never reach `apply_event`. Exactly the gap visual validation exists to catch. Fix: when the source has no `render_state` but is a registered semantic session, route its input through a new `apply_semantic_input_event` — `Key` → `dispatch_key`, `Mouse` → `dispatch_mouse` — the same core path the TUI uses. No grid state is needed (the editor core owns the cursor/buffer/commands); the resulting motion/edit flows back to pmacs-gpu as `CursorByte` / `CrdtOp`. Regression test `semantic_frontend_key_event_reaches_the_core`: a printable `Key` from a semantic frontend self-inserts and advances its window cursor (0→1). Before the fix the dispatcher dropped it. Gates green: - cargo fmt --all -- --check - cargo clippy --all-targets --workspace -- -D warnings (default + crdt) - pmacs lib 1334; crdt daemon tests pass - m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2 Still awaiting visual confirmation (caret tracks arrow keys in a running pmacs-gpu) before merge, per the framing's process rule. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/daemon.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 1f928a0..43d8eea 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1354,15 +1354,22 @@ fn handle_dispatcher_event( if let Some(render_state) = render_states.get_mut(&source) { apply_event(editor, event, &mut term_size, render_state); term_sizes.insert(source, term_size); + } else if semantic_states.contains_key(&source) { + // Phase B (session B1) — a semantic (grid-less) + // session has no `RenderState`, but its keyboard + // input still drives the shared editor core. The + // input events that don't need grid state + // (`Key`, `Mouse`) dispatch through the same + // `dispatch_key` / `dispatch_mouse` path the TUI + // uses; the resulting cursor move / edit flows + // back as `CursorByte` / `CrdtOp`. (Earlier this + // arm dropped these events — the "M11.5 scope" + // posture — which is why typing in pmacs-gpu did + // nothing before B1.) + apply_semantic_input_event(editor, event, term_size); } else { - // T M11.2 — a semantic (grid-less) session has - // no `RenderState`. Key/Mouse/Paste/Focus - // command handling for semantic frontends is - // M11.5 scope; until then these events are - // dropped rather than panicking the - // dispatcher on the absent grid state. debug_assert!( - semantic_states.contains_key(&source), + false, "fid with neither a render_state nor a semantic_state \ sent a frontend event" ); @@ -1897,6 +1904,30 @@ fn build_presence_snapshot(editor: &EditorState, frontend_id: FrontendId) -> Pre } } +/// Dispatch a semantic (grid-less) frontend's input event into the +/// shared editor core (Phase B, session B1). Mirrors the `Key` / `Mouse` +/// arms of [`apply_event`] but takes no `RenderState` — a semantic +/// frontend lays out locally, so the only state these events touch is +/// the editor core (cursor, buffer, commands), which `dispatch_key` / +/// `dispatch_mouse` operate on directly. `Resize` / `Paste` / `Focus` +/// have no grid-less effect yet and are dropped; `Viewport` / `CrdtOp` +/// are handled in their own dispatcher arms and never reach here. +#[allow(clippy::needless_pass_by_value)] // consumes the event, mirroring `apply_event`. +fn apply_semantic_input_event(editor: &mut EditorState, ev: FrontendEvent, term_size: CellSize) { + match ev { + FrontendEvent::Key(pmacs_key) => { + if let Some(ct_key) = key_to_crossterm(&pmacs_key) { + editor.dispatch_key(pmacs_key.frontend_id, ct_key); + } + } + FrontendEvent::Mouse(pmacs_mouse) => { + let ct_mouse = mouse_to_crossterm(&pmacs_mouse); + editor.dispatch_mouse(pmacs_mouse.frontend_id, ct_mouse, term_size); + } + _ => {} + } +} + // Takes `ev` by value because it semantically consumes the event; // the caller pulls events out of the channel one at a time and never // needs to look at them again. @@ -2100,4 +2131,55 @@ mod tests { "buffer.after-edit must fire when handle_remote_crdt_op produces a text Edit" ); } + + /// Session B1 regression: a `Key` event from a *semantic* + /// (grid-less) frontend must reach the editor core. Before B1 the + /// dispatcher's catch-all only called `apply_event` when the + /// frontend had a `RenderState`, so a semantic frontend's keys were + /// silently dropped — typing in pmacs-gpu did nothing. The routing + /// now goes through `apply_semantic_input_event`; a printable char + /// must self-insert at the frontend's window cursor. + #[cfg(feature = "crdt")] + #[test] + fn semantic_frontend_key_event_reaches_the_core() { + use crate::editor::EditorState; + use crate::protocol::FrontendId; + use pmacs_protocol::{Key, KeyEvent, Modifiers}; + + let mut editor = EditorState::new(); + let fid = FrontendId(99); + let view = build_fresh_frontend_view(&mut editor); + editor.core.borrow_mut().register_frontend_view(fid, view); + + let before = editor + .core + .borrow() + .active_window_for(fid) + .expect("fid window") + .cursor; + + apply_semantic_input_event( + &mut editor, + FrontendEvent::Key(KeyEvent { + frontend_id: fid, + key: Key::Char('X'), + mods: Modifiers::NONE, + timestamp_ns: 0, + }), + CellSize::new(24, 80), + ); + + let after = editor + .core + .borrow() + .active_window_for(fid) + .expect("fid window") + .cursor; + assert_eq!( + after, + before + 1, + "a semantic frontend's printable Key must self-insert and advance its window cursor \ + (pre-B1 the dispatcher dropped it)" + ); + } }