Skip to main content

lattice_ai/mcp/
install.rs

1//! BC.3b: the crate-owned `install(boot)` entry point.
2//!
3//! Boot-composition collapses claude-code's five formerly-scattered
4//! `editor_boot` sites (server spawn, ex-command registration, mode
5//! registration, the `ClaudeCodeServerHandle` service register, and the late
6//! `install_services` read/write wiring) into this single call against the
7//! generic [`SubsystemBoot`] surface. Called by the unified
8//! `lattice_ai::install` (the AG‑4 fold), so the host's Phase-B install list
9//! holds one line — `lattice_ai::install(&mut boot)` — and zero host internals
10//! (no `Editor::` method, no host `Effect`/`Action` variant): mode-ownership,
11//! and the property that keeps host churn flat as modes scale.
12
13use lattice_mode::SubsystemBoot;
14
15use lattice_agent::{EditorWriteRequest, make_handler};
16
17use crate::mcp::server::{self, ClaudeCodeServerHandle, ServerConfig};
18use crate::mcp::{commands, lockfile, modes};
19
20/// Wire the Claude Code IDE peer into the editor at boot.
21pub fn install(boot: &mut impl SubsystemBoot) {
22    // Spawn the IDE server supervisor (idle until `:claude-code-start`), reusing
23    // the shared async runtime + the generic event bus (the read cache
24    // subscribes to DocumentOpened/Closed/SelectionsChanged on it).
25    let handle = server::spawn(
26        default_config(),
27        boot.event_bus().clone(),
28        boot.runtime_handle(),
29    );
30
31    // `:claude-code-start` / `:claude-code-stop` (crate-owned ex-commands whose
32    // `apply` drives the handle directly) + `claude-code-mode`.
33    commands::register_claude_code_ex_commands(boot.commands_mut(), handle.clone());
34    modes::register_claude_code_modes(boot.modes_mut());
35    // I7 note: the `claude-code` modeline descriptor is registered lazily in
36    // `claude-code-mode::on_activate` (idempotent, last-write-wins), NOT here —
37    // this `install` runs in Phase B before the host registers the
38    // `ModelineServiceHandle` service, so the service isn't readable yet. By the
39    // time the mode activates (runtime) it is.
40
41    // I2 read tools: the generic buffer-store (trait accessor) + the LSP
42    // diagnostics handle (reached via the generic service lookup, so this crate
43    // needs no lattice-lsp accessor on the trait; the host registered it as a
44    // Phase-A service).
45    let buffer_store = Some(boot.buffer_store().clone());
46    let diagnostics = boot
47        .service::<lattice_lsp::modes::DiagnosticsQueryHandle>()
48        .map(|h| (*h).clone());
49
50    // I3 write tools: the generic inbound bus. `send` wakes the actor
51    // off-keystroke; the per-tick drain runs `make_handler` (maps each request
52    // to an `Effect` + resolves its oneshot). The drain's registration token
53    // rides `boot.into_registrations()` into the Editor for the program
54    // lifetime.
55    let writes = boot.inbound::<EditorWriteRequest, _>(make_handler(handle.read_cache()));
56
57    // I4 openDiff: the host registered the programmatic-diff bus (the openDiff
58    // producer side) as a Phase-A service so this crate names no host internals.
59    // Read it; `None` if absent → `openDiff` reports a graceful "not
60    // initialized". The host owns the matching receiver + drains it (the open is
61    // irreducibly `&mut Editor` + lattice-diff types).
62    let diff = boot
63        .service::<lattice_diff::ProgrammaticDiffBus>()
64        .map(|h| (*h).clone());
65
66    handle.install_services(buffer_store, diagnostics, writes, diff);
67
68    // Expose the handle so `claude-code-mode`'s `on_activate` (I5) reaches it.
69    boot.register_service::<ClaudeCodeServerHandle>(handle);
70}
71
72/// The default server config: workspace = cwd, lockfile dir = `~/.claude/ide`
73/// (temp-dir fallback). Built here so the host never names claude's config
74/// shape — adding/owning this is the crate's concern, not `editor_boot`'s.
75fn default_config() -> ServerConfig {
76    ServerConfig {
77        // PR.4: the project root, so claude's IDE peer sees the whole
78        // workspace rather than whichever directory the editor started
79        // in. Process-scoped like the ACP session above, hence the
80        // cwd-anchored form.
81        workspace_folders: lattice_core::project::root_from_cwd()
82            .map(|p| vec![p.display().to_string()])
83            .unwrap_or_default(),
84        lock_dir: lockfile::default_lock_dir().unwrap_or_else(std::env::temp_dir),
85    }
86}