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}