Skip to main content

lattice_ai/opencode/
commands.rs

1//! The `:opencode` ex-command — launch the opencode agent's native TUI in a
2//! terminal buffer with `opencode-mode` activated on it.
3//!
4//! Per `feedback_mode_owns_its_surface`, both the binding (the command name)
5//! and the body live here; the host action (spawning the terminal + activating
6//! the minor) is requested through the [`Effect`] vocabulary — the host
7//! boundary — not a bespoke channel, exactly like `:claude`.
8
9use lattice_agent::parse_no_args;
10use lattice_grammar::command::LatencyClass;
11use lattice_grammar::effect::Effect;
12use lattice_grammar::registry::{CommandRegistry, ExCommandSpec, SurfaceForm};
13use std::sync::Arc;
14
15use crate::opencode::modes::OpencodeMode;
16
17/// Register `:opencode` against `registry`. Called from [`super::install`].
18pub fn register_opencode_ex_commands(registry: &mut CommandRegistry) {
19    registry.register_ex_command(
20        "opencode-term",
21        "Launch the opencode agent's native TUI in a terminal buffer, with \
22         opencode-mode activated. NOTE: opencode's TUI (opentui) needs modern- \
23         terminal image/capability features lattice's emulator doesn't yet \
24         provide, so it may not render here -- use `:opencode` (the ACP \
25         conversation) instead. Kept for agents whose TUIs degrade gracefully.",
26        ExCommandSpec {
27            latency_class: LatencyClass::Reflex,
28            accepts_bang: false,
29            accepts_range: false,
30            parse_args: Arc::new(parse_no_args),
31            apply: Arc::new(|_ctx| {
32                Ok(Effect::SpawnTerminal {
33                    // PC.2: no override — spawn at the active buffer's
34                    // project root, which is what this always did.
35                    cwd: None,
36                    cmd_line: Some("opencode".to_string()),
37                    env: vec![],
38                    activate_minor: Some(OpencodeMode::mode_id().as_str().to_string()),
39                })
40            }),
41            args_schema: vec![],
42            surface_form: SurfaceForm::Keyword,
43        },
44    );
45}
46
47#[cfg(test)]
48mod tests {
49    #![allow(clippy::unwrap_used, clippy::panic)]
50    use super::*;
51    use lattice_grammar::args::Args;
52    use lattice_grammar::registry::ExCommandContext;
53    use lattice_grammar::{CancellationToken, Count, Register};
54
55    fn ctx() -> ExCommandContext {
56        ExCommandContext {
57            bang: false,
58            args: Args::None,
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    /// `:opencode-term` spawns the `opencode` TUI in a terminal and activates
78    /// `opencode-mode` on it (the terminal topology, like `:claude`).
79    #[test]
80    fn opencode_term_spawns_terminal_and_activates_mode() {
81        let mut registry = CommandRegistry::new();
82        register_opencode_ex_commands(&mut registry);
83        let id = registry
84            .id_by_name("opencode-term")
85            .expect("`:opencode-term` registered");
86        let spec = registry.ex_command_spec(id).expect("spec present");
87        match (spec.apply)(&ctx()).expect("apply ok") {
88            Effect::SpawnTerminal {
89                cmd_line,
90                activate_minor,
91                ..
92            } => {
93                assert_eq!(cmd_line.as_deref(), Some("opencode"));
94                assert_eq!(activate_minor.as_deref(), Some("opencode-mode"));
95            }
96            other => panic!("expected SpawnTerminal, got {other:?}"),
97        }
98    }
99}