PointerKind::TripleDown — the cheap additive bump shape returns:
PROTOCOL_VERSION 7, SUPPORTED [6, 7], the new variant kept off
pre-v7 wires by a frontend send-gate that downgrades it to the
plain Down a third click produced before. The GPU's click history
deepens to a chain count (1 → Down, 2 → DoubleDown, 3 →
TripleDown, then restart). Daemon side, select_line_at_cursor
selects the line including its trailing newline, so consecutive
triple-click lines abut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dispatch_pointer consults the mods it has carried since v5: a Down
with SHIFT keeps the existing anchor (or, with no selection,
anchors at the pre-click cursor) and only moves the cursor — the
universal extend convention. Zero wire change. Frontend-side, a
Shift-click neither advances nor inherits the multi-click chain,
so two Shift-clicks can't become a word select.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mode-line counts (a0bd4d7) took the diag-store mutex before the
window loop and held it through overlay rendering. DiagnosticView —
attached as a window overlay the moment a file with an LSP opens —
locks the same mutex in its render, and std's Mutex is not
reentrant: the daemon's main loop deadlocked on the first frame
after C-x C-f, unresponsive even to SIGINT (parked in futex_wait,
confirmed on the live process). No render test attached a
diagnostic overlay, which is how it slipped through.
The lock is now scoped to the per-window summary computation, after
overlays have rendered and released it. Regression test renders the
full paint_frame path with a real DiagnosticView attached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
M4.6 follow-up piece 1: the mode line's right segment now shows
error/warning counts for the window's buffer, computed from the
shared diag store at paint time. Counts are suppressed while the
URI's diagnostics are stale (mid-edit, pre-publish) so the readout
never describes text that no longer exists. Info/hint severities
stay off the mode line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per docs/pmacs-gpu-mouse-framing.md (resolves the deferred Q#B5):
a pixel frontend cannot express the daemon's cell coordinates —
inline adornments shift visual columns invisibly to cell space and
the design contract forbids hit-test round trips — so the frontend
hit-tests locally and ships source-byte gestures.
- protocol v5: FrontendEvent::Pointer { buffer_id, byte, kind, mods }
with PointerKind { Down, Drag, Up, DoubleDown }. Double-click
detection is frontend-side (only it knows pixel proximity).
SUPPORTED_PROTOCOL_VERSIONS gains 5; the send gate runs in the
frontend (an older instance cannot decode the variant).
- daemon: dispatch_pointer replays the existing mouse gesture
semantics in byte space against the semantic session's window —
Down places + anchors, Drag grows, Up collapses an empty click,
DoubleDown selects the word. Routed by the authenticated source
(CrdtOp/Viewport trust rule); hit bytes clamp + snap to UTF-8
boundaries (a hit can race an in-flight edit).
- word_range_at fix (pre-existing CUA bug the new test surfaced):
double-clicking a word's FIRST character selected the previous
word too — backward_word from pos sees the non-word char behind
the hit and crosses over; walk from pos + ch_len instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- S-<arrows>/S-<home>/S-<end> (+C-S word/paragraph variants) extend a
selection; the TUI grid paints it reverse-video; double-click
selects the word at point.
- Backspace / Delete consume the active region (delete_region first,
falling back to single-codepoint semantics).
- Typing replaces the region: buffer.self-insert / newline / tab
delete_region before inserting. pmacs-gpu cooperates by
round-tripping keys while an own-window selection is active, so
the region-aware commands run instead of a raw optimistic op.
- tests/cua_region_acceptance.rs drives the real dispatch path:
select -> BS/DEL/char/Enter, plus the no-region fallbacks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The attach-mode optimistic-apply layer (M10.10) classifies any
plain-char keystroke as `Insert(c)` and applies it directly to the
local CRDT mirror, bypassing the daemon's keymap dispatcher. The
documented limitation ("the optimistic layer doesn't track keymap-
prefix state") also covered the minibuffer-active case, which
surfaced during session-5 manual validation: characters typed into a
`C-x C-f` prompt were optimistically inserted into the previously-
active document instead of routed to the minibuffer.
The fix is a daemon→frontend wire signal indicating whether the
daemon's *next* key event would be intercepted (minibuffer or pending
prefix) vs would self-insert. The frontend gates the optimistic-apply
path on this; when not idle, every keystroke round-trips as
`FrontendEvent::Key`.
Protocol changes (pmacs-protocol):
- `PROTOCOL_VERSION` 3 → 4; `SUPPORTED_PROTOCOL_VERSIONS` adds 4.
- New `InstanceMessage::DispatchIdle { idle: bool }`.
Daemon (`src/editor.rs`, `src/daemon.rs`):
- `EditorState::dispatch_idle()` — true iff `dispatcher.pending`
empty AND `minibuffer.is_active() == false`.
- Per-tick emission: `last_dispatch_idle_sent: HashMap<FrontendId,
bool>` tracks the last-broadcast value per session; emission fires
on first frame after attach (absent entry) and on transitions.
- Gated on `crdt_replica` AND `negotiated_protocol_version >= 4` so
older peers don't hard-error on the unknown variant. Same gating
shape as the M10.5 CrdtOp and M11.1 SemanticFrame bumps.
Frontend (`src/attach.rs`):
- New `dispatch_idle: bool` (cfg `crdt`); default `false`
(pessimistic — optimistic apply only activates after the daemon
explicitly says idle).
- DispatchIdle messages consumed in the drain loop; they don't
participate in `present_messages` batches.
- Optimistic-apply branch gated on `dispatch_idle`. When false, the
branch returns false (forces fallthrough to the round-trip
`forward_event` path).
Tests:
- `editor::tests::dispatch_idle_*` — fresh, prefix-pending, prefix-
resolved, minibuffer-open/cancelled.
- `protocol::tests::dispatch_idle_round_trips_through_postcard` —
wire encoding both polarities.
- `protocol::tests::protocol_version_is_four_for_dispatch_idle` +
`supported_protocol_versions_includes_one_through_four` — pin the
new version constants.
Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1474 (+5 from 1469 baseline) with crdt; 1312 (+4) without;
m4 83; m11_5 (--features crdt) 2.
Acknowledged remaining gap: plain-char Lua bindings (e.g. binding
`q` to a command) still surface optimistic-apply divergence —
optimistic doesn't know "is this char bound to a non-self-insert
command in the current keymap." Rare in practice; revisit if anyone
hits it. Documented at session-5 finding time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Closes the visible "C++ has no syntax coloring in the grid TUI" gap.
Sibling of SyntaxHighlightView: a View impl that paints LSP semantic
tokens as cell styles, attached for buffers with no bundled
tree-sitter grammar. Same policy A (one styling authority per buffer)
the semantic-frontend producer arc enforces, applied to the grid
renderer the user actually uses today.
Mechanics: every render re-derives the buffer's URI from
buf.file_path() and pulls (encoding, legend) via the existing
LspManager::semantic_style_context plus tokens via for_uri. Per
visible line, tokens are converted from LSP encoding units to byte
ranges via char_to_byte, then to display columns via the existing
byte_range_to_display_cols (UTF-8 + tab aware). Theme::lookup
resolves token type names through the same dotted-prefix mechanism
the tree-sitter capture names use, so "function", "variable",
"type", "keyword" land on the existing theme vocabulary with no new
style names. Default-styled spans skip the per-cell loop, matching
SyntaxHighlightView's short-circuit.
Wiring: pmacs.lsp._attach_style binding pushes the overlay on the
active window (mirrors pmacs.parse._attach_highlight). install_lsp
and make_lsp_manager take SharedSyntaxRegistry so the binding can
hand the LspStyleView the shared ThemeHandle; editor.rs caller
updated. builtin/runtime/lsp.lua's attach_buffer attaches the view
when pmacs.parse.language_for_path returns nil (grammar-less
signal), dedup'd via a styled_buffers set that mirrors syntax.lua's
highlighted_buffers.
Test: lsp_style_view_paints_cells_from_semantic_tokens — seeds an
Initialized fake LSP client (using the cfg(test) helper from the
producer arc) on a /tmp/x.cpp buffer with one token, asserts the
expected cells are styled per the theme face and the cell just past
the token range is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The async bridge settles awaiters inside `tick_lsp`/`tick_mcp` by
posting to the message bus; `tick_async` drains that bus and resumes
the parked coroutine. With `tick_async` running *first* (historical
accretion from M3.3, predating processes/LSP/MCP), every LSP/MCP
`:await()` resumption was deferred a full frame: the response
absorbed in frame N's `tick_lsp` wasn't observed until frame N+1's
`tick_async` (~33ms structural floor @ 60Hz, plus a render frame).
Reordering both production loops (`editor::run` and the daemon loop)
to `processes → lsp → mcp → async` makes settle→resume happen in the
same frame, halving the floor to one frame. The only documented
ordering invariant — `tick_processes → tick_lsp → tick_mcp` for
same-batch supervisor I/O — is preserved; settle (bus post) and
resume (bus drain) are bus-decoupled, so the move cannot regress
correctness in either direction.
Acceptance tests open-code their own per-test tick orders and never
drive `editor::run`, so none covered production ordering. Added
`m4_5_await_resolves_same_frame_as_response_absorbed`, which drives
the exact production order and asserts the awaited request resolves
in the same frame its response is absorbed (absorbed_cycle ==
done_cycle); it fails if anyone reverts to `tick_async`-first.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1223/0; m4_acceptance 59/0; m9_1 18/0; m8_1/m8_9/m8_10 green
(SP-7 outline-aggregate "one async tick" pin unaffected).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the editor-blocking `poll_until` tick-loop in the LSP UX
with the M9.1 external-settle pattern: each `textDocument/*` request
registers a pending entry via `AsyncRuntime::register_external` and
returns the job id; the JSON-RPC response (or a server-teardown /
cancel / timeout) settles it, resuming a `Handle:await()` coroutine.
No worker thread is occupied for the round-trip.
Hybrid result delivery (operator decision): the response is absorbed
into the typed stores *and* carried through the Handle. The
completion popup and diagnostics gutter keep reading the stores
untouched; request/response command code awaits the value directly.
Core (src/lsp.rs):
- `LspManager` gains `runtime: SharedAsyncRuntime` (threaded through
`make_lsp_manager` / editor.rs, mirroring `make_mcp_manager`) plus
a `(server, request_id)` -> PendingExternal awaiter table parallel
to `pending_routes`.
- `request_*` return the async `JobId` (`= u64`, signature
unchanged; no caller consumed the old JSON-RPC id).
- `handle_response` settles every non-cancelled awaiter ok/failed
alongside store absorption; null result still wakes await with nil.
- Awaiters drain-cancelled at all three `pending_routes` purge sites
(restart generation flip / terminal exit / forget) so a coroutine
cannot park on a server that went away.
- Per-tick sweep: per-awaiter cancellation (Handle:cancel() or
supersede via a stable `lsp:{method}:{sid}:{uri}` key), with
`$/cancelRequest` + `cancelled_rids` on abandonment to drop the
cancel/response race silently. Mirrors mcp.rs.
- Per-request timeout (default 10s, `pmacs.lsp.set_request_timeout_ms`):
an alive-but-silent server fails the await instead of hanging.
Lua surface:
- `_request_*_raw` job-id bindings (mirror `pmacs.mcp._send_request_raw`).
- builtin/runtime/lsp.lua: Handle wrappers + the four commands
rewritten to spawn `pmacs.async` coroutines that `:await()`;
`poll_until` removed. Server-gone / error surface as structured
await failures in the modeline.
Tests:
- pmacs_fake_lsp: `error` / `silent` modes for deterministic
failure-path coverage.
- 5 end-to-end await-path tests (success+store, server-error->failed,
server-stop->cancelled, timeout->failed, supersede->cancelled).
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1223/0; m4_acceptance 58/0; m9_1_acceptance 18/0 (MCP unaffected).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pulls a set of planned V0.2 prerequisites forward to ship in v1.0,
plus the clean audit-review round over that work.
V0.2-prereq implementations (documented promotions, not M11
surprises; operator raised the v1.0 public-API ceiling to absorb
them — see V0.2-PREREQUISITES.md "v1.0 pull-forward"):
- CC-1: `bypass_intercept` opts on buffer insert/delete/replace —
skips the Lua intercept chain only; preserves the same-buffer
re-entry guard, undo/dirty bookkeeping, view notifications, and
CRDT broadcast queueing.
- CC-2: `pmacs.buffer.on_removed(buf, cb)` + idempotent `:remove()`
handle; buffer-local keymaps pruned on removal. Fires for both
`pmacs.buffer.remove` and `.kill` (incl. interactive C-x k);
callback errors logged to *errors* without failing the removal.
- SP-4: `pmacs.buffer.from_file`.
- SP-5: `pmacs.fs.watch` (polling; `:cancel()`/`:is_cancelled()`).
- SP-7: `pmacs.async.yield_to_next_tick` (worker-free next-tick
yield); outline-aggregate repaint now uses it instead of
workers.sleep(0):await(), pinning propagation to one async tick.
- SP-1: `pmacs.editor.move_to_line` (0-based, clamps out-of-range).
- SP-6: `pmacs.outline.query` published by pmacs-outline.
- SP-3: audit rule 15 `reach-around-require-field` (Info).
- CC-3: runtime API-availability documented (docs-only).
Clean audit-review round (M10.11 framing stop-condition pass):
- Finding 1 (fixed): clippy needless_raw_string_hashes blocked
`clippy -D warnings` on both lanes; raw-string delimiter fixed.
- Finding 2 (fixed): fs_watch acceptance test was racy — the
`pending == 1` gate could not distinguish the in-flight baseline
stat from the steady-state poll sleep, so under load the mutation
raced the baseline (~1/3 fail in the default lane). Rewritten to
re-emit a distinct change each pump iteration; 6/6 on the
previously-failing invocation.
- Finding 3 (fixed): documented fs.watch's async-baseline startup
window and size+mtime-granularity detection limit.
- Finding 4 / SP-8 (logged, non-blocking, out of diff): a
pre-existing PTY-lifecycle test timing flake under severe CPU
oversubscription; src/process.rs untouched here.
CC-1's opts-extension-counts question resolved explicitly
(consistent treatment: counted; ceiling raised to fit).
Gate at normal load, both lanes: fmt clean; clippy --all-targets
-D warnings clean; non-crdt lib 1223/0; crdt lib 1377/0;
m8_1/m8_9/m8_10 green.
Not in scope here: v1.0 CHANGELOG body, version bump, the M10.11
Finding-4 (reattach undo) user-facing artifact, and the recorded
two-laptop manual acceptance — tracked as the remaining v1.0 steps.
.gitignore: M*-FRAMING.md added to the internal-only block for
consistency with the M*-AUDIT.md / M*-SHIP-GATE.md siblings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the optimistic local-edit-application layer on top of the M10 CRDT
foundation: frontend-side rope replica with local edit application,
daemon-authoritative broadcast, and bidirectional cursor reconciliation.
Keystrokes feel instantaneous because the local replica answers next-render
queries before the daemon round-trip completes, while the daemon remains
the single source of truth for conflict resolution and broadcast to remote
replicas.
Architecture beats:
- BufferMirror (src/buffer_mirror.rs) holds a per-frontend rope replica
with explicit cursor-staleness tracking. Every event that may move the
active cursor or swap the active buffer marks the mirror stale; the
next CursorByte from the daemon clears it.
- CrdtOpOrigin {OptimisticReplica(FrontendId), DaemonKey} routes broadcast.
OptimisticReplica skips re-application on the originating frontend
(already applied locally); DaemonKey broadcasts to all replicas including
source -- covers Lua-driven and generated-buffer edits that bypass the
optimistic path.
- Generated buffers (*help*, *workers*, *pmacs-instance*, *errors*) funnel
apply_edit output through queue_daemon_origin_crdt_op so post-attach
CRDT upgrades don't drop their edits.
- forbid(unsafe_code) preserved throughout; loro 1.12 added as the CRDT
engine.
Audit posture: M10.10 shipped through six post-audit review rounds with
twenty-eight cumulative findings, most categorized as "incomplete
application of a prior round's mechanism." The audit doc records
grep-driven exhaustiveness as the standing countermeasure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the Model Context Protocol (MCP) integration as a transport binding,
not a built-in feature. Six Lua functions plus userdata methods expose
the substance of three MCP feature areas (resources, tools, prompts), a
notification dispatcher, and a non-trivial AI-assistance example package
that meets the architectural ship gate (spec/pmacs-spec.tex:1572): zero
direct calls into the Rust core, zero special-cased MCP handling outside
the public API, source under 2000 lines of Lua.
The M9.5 -> M9.6 -> M9.7 -> M9.8 layered composition validates the claim
"AI is a transport binding, not a feature" -- pmacs-mcp-ai composes with
pmacs-mcp-prompts.render and inherits notification handling transitively
through M9.7's package, demonstrating that the AI domain is a layer
above MCP, not a thread woven through the core.
Subtask shape:
M9.1 stdio transport + initialize handshake + restart policy
M9.2 resources with in-flight + settled cache and per-uri invalidation
M9.3 tools with isError-vs-JSON-RPC-error semantics + cancellation
M9.4 prompts with required-argument validation
M9.5 notification dispatcher (on_notification, off_notification)
M9.6 tools-as-commands fixture package + 12 audit findings disposed
M9.7 prompts-as-result-buffers fixture package + tree-sitter-md grammar
M9.8 AI-assistance fixture package (363+ LoC; 17/17 acceptance tests)
M9.9 formal package audit -- PASS on all three criteria
M9.10 release: TRANSITION-M9.md + MCP-for-package-authors guide
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>