Skip to main content

zeph_commands/handlers/
agent_cmd.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Sub-agent management handler: `/agent`.
5
6use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12/// Manage sub-agents or dispatch `@mention` commands.
13///
14/// Delegates to `SubagentAccess::handle_agent_dispatch`, which handles both `/agent`
15/// subcommands and `@name` mentions. Returns `Continue` when the dispatch returns
16/// `None` (no agent matched an `@mention` — fall through to LLM processing).
17pub struct AgentCommand;
18
19impl CommandHandler<CommandContext<'_>> for AgentCommand {
20    fn name(&self) -> &'static str {
21        "/agent"
22    }
23
24    fn description(&self) -> &'static str {
25        "Manage sub-agents"
26    }
27
28    fn args_hint(&self) -> &'static str {
29        "[subcommand]"
30    }
31
32    fn category(&self) -> SlashCategory {
33        SlashCategory::Integration
34    }
35
36    fn requires_auth(&self) -> bool {
37        true
38    }
39
40    fn handle<'a>(
41        &'a self,
42        ctx: &'a mut CommandContext<'_>,
43        args: &'a str,
44    ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
45        use tracing::Instrument as _;
46        let span = tracing::info_span!("commands.agent.handle");
47        Box::pin(
48            async move {
49                let input = if args.is_empty() {
50                    "/agent".to_owned()
51                } else {
52                    format!("/agent {args}")
53                };
54                match ctx.agent.handle_agent_dispatch(&input).await? {
55                    Some(msg) => Ok(CommandOutput::Message(msg)),
56                    None => Ok(CommandOutput::Silent),
57                }
58            }
59            .instrument(span),
60        )
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::CommandRegistry;
68    use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
69    use crate::sink::NullSink;
70    use std::assert_matches;
71
72    #[test]
73    fn agent_cmd_name_and_description() {
74        assert_eq!(AgentCommand.name(), "/agent");
75        assert!(!AgentCommand.description().is_empty());
76    }
77
78    #[tokio::test]
79    async fn agent_dispatch_none_returns_silent() {
80        // NullAgent returns Ok(None), so the handler returns Silent.
81        let mut sink = NullSink;
82        let mut debug = MockDebug;
83        let mut messages = MockMessages;
84        let session = MockSession;
85        let mut agent = crate::NullAgent;
86        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
87        let out = AgentCommand.handle(&mut ctx, "").await.unwrap();
88        assert_matches!(out, CommandOutput::Silent);
89    }
90
91    #[tokio::test]
92    async fn agent_dispatch_with_args_returns_silent() {
93        let mut sink = NullSink;
94        let mut debug = MockDebug;
95        let mut messages = MockMessages;
96        let session = MockSession;
97        let mut agent = crate::NullAgent;
98        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
99        let out = AgentCommand.handle(&mut ctx, "list").await.unwrap();
100        assert_matches!(out, CommandOutput::Silent);
101    }
102
103    #[tokio::test]
104    async fn agent_dispatch_allowed_when_trusted() {
105        let mut sink = NullSink;
106        let mut debug = MockDebug;
107        let mut messages = MockMessages;
108        let session = MockSession;
109        let mut agent = crate::NullAgent;
110        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
111
112        let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
113        reg.register(AgentCommand);
114
115        let result = reg.dispatch(&mut ctx, "/agent list", true).await;
116        assert!(result.unwrap().is_ok());
117    }
118
119    #[tokio::test]
120    async fn agent_dispatch_rejected_when_untrusted() {
121        let mut sink = NullSink;
122        let mut debug = MockDebug;
123        let mut messages = MockMessages;
124        let session = MockSession;
125        let mut agent = crate::NullAgent;
126        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
127
128        let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
129        reg.register(AgentCommand);
130
131        let result = reg.dispatch(&mut ctx, "/agent list", false).await;
132        let err = result.unwrap().unwrap_err();
133        assert!(err.0.contains("trusted"));
134    }
135}