zeph_commands/handlers/
model.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct ModelCommand;
18
19impl CommandHandler<CommandContext<'_>> for ModelCommand {
20 fn name(&self) -> &'static str {
21 "/model"
22 }
23
24 fn description(&self) -> &'static str {
25 "Show or switch the active model"
26 }
27
28 fn args_hint(&self) -> &'static str {
29 "[id|refresh]"
30 }
31
32 fn category(&self) -> SlashCategory {
33 SlashCategory::Configuration
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.model.handle");
47 Box::pin(
48 async move {
49 let result = ctx.agent.handle_model(args).await;
50 Ok(CommandOutput::message_or_silent(result))
51 }
52 .instrument(span),
53 )
54 }
55}
56
57pub struct ProviderCommand;
62
63impl CommandHandler<CommandContext<'_>> for ProviderCommand {
64 fn name(&self) -> &'static str {
65 "/provider"
66 }
67
68 fn description(&self) -> &'static str {
69 "List configured providers or switch to one by name"
70 }
71
72 fn args_hint(&self) -> &'static str {
73 "[name|status]"
74 }
75
76 fn category(&self) -> SlashCategory {
77 SlashCategory::Configuration
78 }
79
80 fn requires_auth(&self) -> bool {
81 true
82 }
83
84 fn handle<'a>(
85 &'a self,
86 ctx: &'a mut CommandContext<'_>,
87 args: &'a str,
88 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
89 use tracing::Instrument as _;
90 let span = tracing::info_span!("commands.provider.handle");
91 Box::pin(
92 async move {
93 let result = ctx.agent.handle_provider(args).await;
94 Ok(CommandOutput::message_or_silent(result))
95 }
96 .instrument(span),
97 )
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
105 use crate::sink::NullSink;
106 use std::assert_matches;
107
108 #[test]
109 fn model_name_and_description() {
110 assert_eq!(ModelCommand.name(), "/model");
111 assert!(!ModelCommand.description().is_empty());
112 }
113
114 #[test]
115 fn provider_name_and_description() {
116 assert_eq!(ProviderCommand.name(), "/provider");
117 assert!(!ProviderCommand.description().is_empty());
118 }
119
120 #[tokio::test]
121 async fn model_returns_silent_when_agent_returns_empty() {
122 let mut sink = NullSink;
123 let mut debug = MockDebug;
124 let mut messages = MockMessages;
125 let session = MockSession;
126 let mut agent = crate::NullAgent;
127 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
128 let out = ModelCommand.handle(&mut ctx, "").await.unwrap();
129 assert_matches!(out, CommandOutput::Silent);
130 }
131
132 #[tokio::test]
133 async fn provider_returns_silent_when_agent_returns_empty() {
134 let mut sink = NullSink;
135 let mut debug = MockDebug;
136 let mut messages = MockMessages;
137 let session = MockSession;
138 let mut agent = crate::NullAgent;
139 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
140 let out = ProviderCommand.handle(&mut ctx, "status").await.unwrap();
141 assert_matches!(out, CommandOutput::Silent);
142 }
143}