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}