Skip to main content

lattice_diff/
programmatic.rs

1//! Programmatic diff requests — the host-drained "open a diff and await the
2//! user's verdict" capability that [`DiffSession::bind_completion`] was built
3//! for.
4//!
5//! I4 (Claude Code IDE peer, `openDiff`): an off-thread producer (the IDE
6//! peer's WebSocket task) sends a [`ProgrammaticDiffRequest`] over the
7//! host-drained inbound bus
8//! ([`lattice_mode::inbound::make_inbound_raw`](lattice_mode::inbound::make_inbound_raw)),
9//! whose `send` wakes the editor; the host drains it on the actor thread, opens
10//! a side-by-side diff (the baseline file vs the proposed text), and
11//! `bind_completion`s the request's [`response`](ProgrammaticDiffRequest::response)
12//! oneshot to the session. The producer awaits `response` directly — when the
13//! user resolves the diff (`:diff-accept` / `:diff-reject`, or a close-tab
14//! cancel that drops the session), the existing teardown
15//! ([`DiffSession::take_completion`] in `tear_down_single_diff_session`) fires
16//! the bound [`DiffOutcome`] back.
17//!
18//! The type lives here, NOT in the IDE-peer crate, on purpose: the host must
19//! drain it and the open is irreducibly `&mut Editor` + lattice-diff types, so
20//! keeping the request a *diff-subsystem* type means the host references no
21//! IDE-peer internals (preserving the BC.3b invariant that the host carries
22//! zero claude-code internals beyond one `install` line), and a second consumer
23//! — an LSP `WorkspaceEdit` preview, a magit-style plugin — reuses the same bus.
24//! See `StaticSource`'s doc comment, which already names these consumers.
25//!
26//! [`DiffSession::bind_completion`]: crate::subsystem::DiffSession::bind_completion
27//! [`DiffSession::take_completion`]: crate::subsystem::DiffSession::take_completion
28
29use std::path::PathBuf;
30
31use lattice_mode::inbound::InboundBus;
32use tokio::sync::oneshot;
33
34use crate::subsystem::DiffOutcome;
35
36/// A request to open an interactive side-by-side diff and block (on the
37/// producer side) until the user Keeps or Rejects it.
38///
39/// `old_file_path` is the baseline — its on-disk content fills the read-only
40/// left side. `new_contents` is the proposed text (the editable right side),
41/// carrying `new_file_path` so an Accept can save it. `response` resolves with
42/// the user's [`DiffOutcome`] when the diff is torn down.
43#[derive(Debug)]
44pub struct ProgrammaticDiffRequest {
45    /// Baseline file path; its on-disk content is the left (read-only) side.
46    pub old_file_path: PathBuf,
47    /// The path the proposed content carries (the right side's buffer path); an
48    /// Accept writes the right side here. Usually equals `old_file_path`.
49    pub new_file_path: PathBuf,
50    /// The proposed text — the editable right side.
51    pub new_contents: String,
52    /// Display label for the diff (the agent's tab name). Presentation only —
53    /// the teardown is keyed on [`origin_session`](Self::origin_session), NOT
54    /// this label (a diff may show as a tab today, a window/split tomorrow).
55    pub tab_name: String,
56    /// D-fix.6: the originating session — the IDE-peer connection id that
57    /// produced this diff. The host stores it with the opened diff so a
58    /// session-scoped close (a `close_tab` / `closeAllDiffTabs` from THAT
59    /// connection) tears down only the diffs that connection opened, never
60    /// another session's. `0` means "no originating session" (a non-IDE
61    /// producer — an LSP `WorkspaceEdit` preview, a plugin); such diffs are
62    /// matched by no connection's close.
63    pub origin_session: u64,
64    /// Resolved with the user's verdict when the diff is torn down. A dropped
65    /// receiver (the producer gave up) makes the teardown's `send` a no-op; a
66    /// dropped sender (the session was cancelled without an explicit outcome)
67    /// surfaces to the producer's `await` as a recv error it maps to a reject.
68    pub response: oneshot::Sender<DiffOutcome>,
69}
70
71/// The host-drained inbound bus carrying [`ProgrammaticDiffRequest`]s.
72///
73/// A clone is registered as a boot service so a producer subsystem (the IDE
74/// peer) reads it via `boot.service::<ProgrammaticDiffBus>()` and `send`s
75/// requests; the wake is baked into [`InboundBus::send`] (paramount #4 — the
76/// editor learns of the request off-keystroke without a keypress). The host
77/// holds the matching receiver on the `Editor` and drains it per tick.
78pub type ProgrammaticDiffBus = InboundBus<ProgrammaticDiffRequest>;