Skip to main content

lattice_ai/pi/
commands.rs

1//! The `:pi` ex-command — launch the pi agent's native TUI in a
2//! terminal buffer with `pi-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 `:opencode`.
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::pi::modes::PiMode;
16
17/// Register `:pi` against `registry`. Called from [`super::install`].
18pub fn register_pi_ex_commands(registry: &mut CommandRegistry) {
19    registry.register_ex_command(
20        "pi",
21        "Launch the pi coding agent (pi.dev) in a terminal buffer, with \
22         pi-mode activated. Requires the `pi` CLI to be installed \
23         (curl -fsSL https://pi.dev/install.sh | sh).",
24        ExCommandSpec {
25            latency_class: LatencyClass::Reflex,
26            accepts_bang: false,
27            accepts_range: false,
28            parse_args: Arc::new(parse_no_args),
29            apply: Arc::new(|_ctx| {
30                Ok(Effect::SpawnTerminal {
31                    // PC.2: no override — spawn at the active buffer's
32                    // project root, which is what this always did.
33                    cwd: None,
34                    cmd_line: Some("pi".to_string()),
35                    env: vec![],
36                    activate_minor: Some(PiMode::mode_id().as_str().to_string()),
37                })
38            }),
39            args_schema: vec![],
40            surface_form: SurfaceForm::Keyword,
41        },
42    );
43}
44
45#[cfg(test)]
46mod tests {
47    #![allow(clippy::unwrap_used, clippy::panic)]
48    use super::*;
49    use lattice_grammar::args::Args;
50    use lattice_grammar::registry::ExCommandContext;
51    use lattice_grammar::{CancellationToken, Count, Register};
52
53    fn ctx() -> ExCommandContext {
54        ExCommandContext {
55            bang: false,
56            args: Args::None,
57            range: None,
58            register: Register::default(),
59            count: Count::default(),
60            buffer_id: lattice_core::BufferId::default(),
61            // OC.10 added these four so a PLUGIN ex-command could name the
62            // buffer its `Effect::ApplyEdit` targets. Every command in this
63            // module is transport-level — it starts an agent, opens a log,
64            // toggles a server — and reads none of them, so they carry their
65            // empty forms rather than a fabricated cursor into a buffer this
66            // test never builds.
67            cursor: Default::default(),
68            buffer: Default::default(),
69            path: None,
70            syntax: None,
71            cancel: CancellationToken::new(),
72        }
73    }
74
75    /// `:pi` spawns the `pi` TUI in a terminal and activates `pi-mode` on it
76    /// (the terminal topology, like `:opencode` and `:claude`).
77    #[test]
78    fn pi_spawns_terminal_and_activates_mode() {
79        let mut registry = CommandRegistry::new();
80        register_pi_ex_commands(&mut registry);
81        let id = registry.id_by_name("pi").expect("`:pi` registered");
82        let spec = registry.ex_command_spec(id).expect("spec present");
83        match (spec.apply)(&ctx()).expect("apply ok") {
84            Effect::SpawnTerminal {
85                cmd_line,
86                activate_minor,
87                ..
88            } => {
89                assert_eq!(cmd_line.as_deref(), Some("pi"));
90                assert_eq!(activate_minor.as_deref(), Some("pi-mode"));
91            }
92            other => panic!("expected SpawnTerminal, got {other:?}"),
93        }
94    }
95}