Skip to main content

lattice_ai/
commands.rs

1//! The transport-neutral `:ai-log` ex-command.
2//!
3//! `:ai-log` is always registered ([`register_ai_log_command`], no `#[cfg]`):
4//! the log substrate lives in the `lattice-agent` port (AG‑3), so the command
5//! is meaningful for whichever transport(s) are compiled in. It captures no
6//! handle: opening the per-process `*ai:<provider>:<index>*` buffer needs host
7//! buffer-open machinery, so it emits `Effect::OpenAiLog` and the host resolves
8//! it (0 known sessions echo a hint, 1 opens directly, >1 raises a picker) --
9//! exactly how `:lsp-server-log` is wired.
10//!
11//! The ACP lifecycle commands (`:opencode` / `:ai-prompt` / `:ai-stop`) live in
12//! [`crate::acp::commands`], gated with the rest of the ACP transport.
13
14use lattice_grammar::args::Args;
15use lattice_grammar::command::LatencyClass;
16use lattice_grammar::effect::Effect;
17use lattice_grammar::registry::{CommandRegistry, ExCommandSpec, SurfaceForm};
18use std::sync::Arc;
19
20/// Register the transport-neutral `:ai-log [provider]` command. Captures NO
21/// handle: the host reads the `AiLogger` service to enumerate sessions and
22/// open the `*ai:<provider>:<index>*` buffer. The crate owns the BINDING + the
23/// emission here (mode-ownership); the host owns only the generic buffer open.
24/// Mirrors `:lsp-server-log`. Always available (both transports log through the
25/// shared port substrate).
26pub fn register_ai_log_command(registry: &mut CommandRegistry) {
27    registry.register_ex_command(
28        "ai-log",
29        "Open the AI agent log buffer (picker when several sessions exist).",
30        ExCommandSpec {
31            latency_class: LatencyClass::Reflex,
32            accepts_bang: false,
33            accepts_range: false,
34            parse_args: Arc::new(lattice_agent::parse_rest_as_text),
35            apply: Arc::new(move |ctx| {
36                let session = match &ctx.args {
37                    Args::String(t) if !t.is_empty() => Some(t.clone()),
38                    _ => None,
39                };
40                Ok(Effect::OpenAiLog { session })
41            }),
42            args_schema: vec![],
43            surface_form: SurfaceForm::Keyword,
44        },
45    );
46}
47
48#[cfg(test)]
49mod tests {
50    #![allow(clippy::unwrap_used, clippy::panic)]
51    use super::*;
52    use lattice_grammar::registry::ExCommandContext;
53    use lattice_grammar::{CancellationToken, Count, Register};
54
55    fn ctx_with(args: Args) -> ExCommandContext {
56        ExCommandContext {
57            bang: false,
58            args,
59            range: None,
60            register: Register::default(),
61            count: Count::default(),
62            buffer_id: lattice_core::BufferId::default(),
63            // OC.10 added these four so a PLUGIN ex-command could name the
64            // buffer its `Effect::ApplyEdit` targets. Every command in this
65            // module is transport-level — it starts an agent, opens a log,
66            // toggles a server — and reads none of them, so they carry their
67            // empty forms rather than a fabricated cursor into a buffer this
68            // test never builds.
69            cursor: Default::default(),
70            buffer: Default::default(),
71            path: None,
72            syntax: None,
73            cancel: CancellationToken::new(),
74        }
75    }
76
77    // `:ai-log` is transport-neutral -- registered without a handle and tested
78    // in every feature combination (acp, mcp, both, neither).
79    #[test]
80    fn ai_log_no_arg_opens_without_prefilter() {
81        let mut registry = CommandRegistry::new();
82        register_ai_log_command(&mut registry);
83
84        let id = registry
85            .id_by_name("ai-log")
86            .expect("`:ai-log` is registered");
87        let spec = registry.ex_command_spec(id).expect("spec present");
88        let effect = (spec.apply)(&ctx_with(Args::None)).expect("apply ok");
89
90        match effect {
91            Effect::OpenAiLog { session } => assert_eq!(session, None),
92            other => panic!("expected OpenAiLog, got {other:?}"),
93        }
94    }
95
96    #[test]
97    fn ai_log_with_arg_carries_provider_prefilter() {
98        let mut registry = CommandRegistry::new();
99        register_ai_log_command(&mut registry);
100
101        let id = registry
102            .id_by_name("ai-log")
103            .expect("`:ai-log` is registered");
104        let spec = registry.ex_command_spec(id).expect("spec present");
105        let effect =
106            (spec.apply)(&ctx_with(Args::String("opencode".to_string()))).expect("apply ok");
107
108        match effect {
109            Effect::OpenAiLog { session } => assert_eq!(session.as_deref(), Some("opencode")),
110            other => panic!("expected OpenAiLog, got {other:?}"),
111        }
112    }
113}