Commit Graph

7 Commits

Author SHA1 Message Date
Levi Neuwirth 72799f771a protocol v6: per-severity diagnostic underline colors (SGR 58)
M4.6 follow-up piece 2. `Style` gains `underline_color: Color`
(Default = follow the text color) so a diagnostic squiggle can be
red/yellow/cyan/gray without clobbering the syntax color of the
text it underlines — exactly why error_style() left its 'red'
unwired until now.

The wire consequence: Style rides inside Cell / CellDelta /
Snapshot / StyleSpans, so this is the protocol's first
encoding-breaking change. PROTOCOL_VERSION 5 → 6 and
SUPPORTED_PROTOCOL_VERSIONS narrows to [6]: postcard is not
self-describing, so no per-session send gate can keep a v5 peer
decoding v6 cells — a mismatched pair now fails the handshake with
a clean VersionMismatch instead of garbling mid-session. Version
policy tests rewritten to pin the new contract.

Surface wiring:
- diag.rs: per-severity underline_color (indexed 1/3/6/8).
- frontend.rs: kitty-style CSI 4:N for Double/Curly/Dotted/Dashed
  (previously flattened to plain SGR 4) + SGR 58:5/58:2 emission.
- ansi.rs: parse SGR 58/59 with the 38/48 extended-color grammar.
- overlay.rs merge_styles: non-default-wins, like fg/bg/underline.
- lua_bindings.rs: underline_color on Lua style tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:16:42 -04:00
Levi Neuwirth 7ec314ad78
T M11.6 — DispatchIdle signal closes optimistic-apply blindness (#45)
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>
2026-05-21 13:49:48 +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 1c5169b8f8 M11.1: semantic-frontend protocol scaffolding (wire + capability)
First milestone of the M11 semantic-frontend arc
(docs/semantic-frontend-protocol.md). Wire-format scaffolding only —
no producer or consumer; mechanically identical to the M10.5 CRDT
wire declaration, and non-breaking by the same slice-membership +
per-session-filter argument.

- PROTOCOL_VERSION 2 -> 3; SUPPORTED_PROTOCOL_VERSIONS [1,2,3]. v0.1
  and v1.0 binaries keep connecting unchanged (membership, not
  strict equality).
- semantic_render capability bit on FrontendCapabilities,
  InstanceCapabilities, NegotiatedCapabilities (#[serde(default)];
  instance default false until the M11.2 projection seam).
  negotiate_capabilities AND-combines it and enforces the
  semantic_render => crdt_replica dependency as a CapabilityMismatch
  (a semantic session is also a text replica), never a silent
  degrade. PMACS_INSTANCE_SEMANTIC_RENDER env override added.
- InstanceMessage SemanticFrame family: StyleSpans, Decorations,
  InlineAdornments, BlockAdornments, FoldState, ResourceOffer.
  FrontendEvent::Viewport. Supporting types: ByteRange, StyleSpan,
  Decoration/DecorationKind, InlineAdornment/AdornmentPlacement/
  AdornmentContent, BlockAdornment, ResourceBody. All byte-anchored;
  no pixels cross the contract boundary.
- Grid TUI (frontend.rs) and daemon apply_event drop the new family
  silently — the "declared, not yet wired" posture CrdtOp held
  between M10.5 and M10.8. Stale v1.0 version-pin tests updated to
  the v1.1 truth; negotiation matrix + postcard round-trips added.

Lib + integration suites green on both the default and crdt feature
flavors; clippy -D warnings clean on both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:39:07 -04:00
Levi Neuwirth 45be65b026 M10.10 ship gate
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>
2026-05-13 16:28:46 -04:00
Levi Neuwirth c8d0d67615 Fix PTY final-output drain race 2026-05-04 09:44:30 -04:00
Levi Neuwirth 4da4b09d5d Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00