Skip to main content

lattice_ai/acp/
install.rs

1//! ACP transport boot wiring.
2//!
3//! Spawns the supervisor (owns the provider child process, the ACP connection,
4//! and the active session for the program's lifetime), wires the
5//! `:opencode` / `:ai-prompt` / `:ai-stop` ex-commands, and registers the
6//! `AiClientHandle` service. Called by the crate-root `install` (behind
7//! `#[cfg(feature = "acp")]`), which owns the transport-neutral log substrate.
8
9use std::sync::Arc;
10
11use lattice_agent::AiLogger;
12use lattice_mode::SubsystemBoot;
13
14use crate::acp::commands::register_ai_ex_commands;
15use crate::acp::conversation::ConversationStore;
16use crate::acp::conversation_mode::AiConversationMode;
17use crate::acp::handle::AiClientHandle;
18
19/// Wire the ACP (Agent Client Protocol) transport into the editor at boot. The
20/// `logger` is the port-level `AiLogger` the supervisor streams *trace* records
21/// into; the crate-root `install` already registered it as a service.
22pub fn install(boot: &mut impl SubsystemBoot, logger: &AiLogger) {
23    // The structured conversation store: the supervisor folds agent *conversation*
24    // output into it and publishes `ConversationUpdated` on the event bus so the
25    // `ai-conversation` mode (AU-2) can live-tail. Registered as a service so the
26    // mode can read snapshots.
27    let bus = boot.event_bus().clone();
28    let conv_store = ConversationStore::new(Arc::new(move |event| bus.publish_typed(event)));
29
30    // The `ai-conversation` major mode backs the `*ai:opencode*` buffer; it
31    // reads the ConversationStore service and live-tails via ConversationUpdated.
32    boot.modes_mut()
33        .register(AiConversationMode::new())
34        .expect("ai-conversation-mode register");
35
36    // PU-B: the `ai-permission` major mode backs the `*ai-permission*` popup
37    // menu; its `on_activate` reads the ConversationStore and projects the
38    // oldest pending request.
39    boot.modes_mut()
40        .register(crate::acp::permission_mode::AiPermissionMode::new())
41        .expect("ai-permission-mode register");
42
43    // AU‑4: the host-registered programmatic-diff bus (the `review_diff`
44    // producer side), shared with MCP's `openDiff`. `None` if absent → edit
45    // permissions can't be reviewed and are denied (graceful). The host owns
46    // the receiver and resolves verdicts on `:diff-accept` / `:diff-reject`.
47    let diff_bus = boot
48        .service::<lattice_diff::ProgrammaticDiffBus>()
49        .map(|h| (*h).clone());
50
51    // Spawn the supervisor with a logger clone (trace) + the conversation store
52    // + the diff bus (edit review).
53    let handle = AiClientHandle::spawn(
54        boot.runtime_handle(),
55        logger.clone(),
56        conv_store.clone(),
57        diff_bus,
58    );
59
60    // Crate-owned ex-commands: `:opencode` / `:ai-prompt` / `:ai-stop`.
61    register_ai_ex_commands(boot.commands_mut(), handle.clone());
62
63    // AU‑3: the modal-input action commands (`action:ai-conv-*`) the mode's
64    // keymap binds. Declaration here (mode owns its surface); the handler
65    // bodies live in `AiConversationMode::action_handlers`.
66    crate::acp::conversation_mode::register_ai_conversation_actions(boot.commands_mut());
67
68    // PU-B: the `ai-permission` menu's action commands (`action:ai-perm-*`).
69    crate::acp::permission_mode::register_ai_permission_actions(boot.commands_mut());
70
71    // PU-B.3: the permission-menu auto-open coordinator (menu-open gate +
72    // Esc-deferred set), shared by `ai-permission-mode` and the auto-open tick
73    // callback below. Registered as a service (the `Arc<X>` handle convention).
74    let coordinator: crate::acp::permission_mode::PermissionMenuCoordinatorHandle =
75        Arc::new(crate::acp::permission_mode::PermissionMenuCoordinator::new());
76    boot.register_service::<crate::acp::permission_mode::PermissionMenuCoordinatorHandle>(
77        coordinator.clone(),
78    );
79
80    // PU-B.3: the auto-open tick callback. `run_tick_pending` runs it on the
81    // actor's `async_landed` wake (below) — no keystroke — so a permission that
82    // becomes pending while the user is idle opens the menu on its own. The body
83    // is mode-owned (`permission_mode::auto_open_tick`); the host only runs the
84    // closure + applies the returned `Effect::OpenPopup`.
85    {
86        let conv_store = conv_store.clone();
87        let coordinator = coordinator.clone();
88        boot.tick_callback(Box::new(move || {
89            crate::acp::permission_mode::auto_open_tick(&conv_store, &coordinator)
90        }));
91    }
92
93    // Services: `AiClientHandle` for a future modeline/UI; `ConversationStore`
94    // for the `ai-conversation` mode's projection.
95    boot.register_service::<AiClientHandle>(handle);
96    boot.register_service::<ConversationStore>(conv_store);
97
98    // Repaint wake: the `ai-conversation` drain fires `ConversationProjected`
99    // AFTER each re-projection edit lands. Wake the editor actor on it so a
100    // streamed agent response repaints (and the prompt-focus tick callback runs)
101    // without needing a keystroke. Sequenced after the edit — via this event,
102    // not `ConversationUpdated` — so the wake never paints stale content.
103    boot.wake_on_event::<crate::acp::conversation::ConversationProjected>();
104
105    // PU-B.3: also wake on `ConversationUpdated` (fired by the supervisor's
106    // `push_permission_request`) so the auto-open tick callback runs the instant
107    // a permission goes pending — even when the `*ai:opencode*` buffer isn't
108    // focused, so its `ConversationProjected` drain isn't the one waking us.
109    boot.wake_on_event::<crate::acp::conversation::ConversationUpdated>();
110}