zeph_commands/handlers/
status.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct StatusCommand;
14
15impl CommandHandler<CommandContext<'_>> for StatusCommand {
16 fn name(&self) -> &'static str {
17 "/status"
18 }
19
20 fn description(&self) -> &'static str {
21 "Show current session status (provider, model, tokens, uptime)"
22 }
23
24 fn category(&self) -> SlashCategory {
25 SlashCategory::Debugging
26 }
27
28 fn requires_auth(&self) -> bool {
29 true
30 }
31
32 fn handle<'a>(
33 &'a self,
34 ctx: &'a mut CommandContext<'_>,
35 _args: &'a str,
36 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
37 use tracing::Instrument as _;
38 let span = tracing::info_span!("commands.status.handle");
39 Box::pin(
40 async move {
41 let result = ctx.agent.session_status().await?;
42 Ok(CommandOutput::Message(result))
43 }
44 .instrument(span),
45 )
46 }
47}
48
49pub struct GuardrailCommand;
51
52impl CommandHandler<CommandContext<'_>> for GuardrailCommand {
53 fn name(&self) -> &'static str {
54 "/guardrail"
55 }
56
57 fn description(&self) -> &'static str {
58 "Show guardrail status (provider, model, action, timeout, stats)"
59 }
60
61 fn category(&self) -> SlashCategory {
62 SlashCategory::Debugging
63 }
64
65 fn feature_gate(&self) -> Option<&'static str> {
66 Some("guardrail")
67 }
68
69 fn requires_auth(&self) -> bool {
70 true
71 }
72
73 fn handle<'a>(
74 &'a self,
75 ctx: &'a mut CommandContext<'_>,
76 _args: &'a str,
77 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
78 use tracing::Instrument as _;
79 let span = tracing::info_span!("commands.guardrail.handle");
80 Box::pin(
81 async move {
82 let result = ctx.agent.guardrail_status();
83 Ok(CommandOutput::Message(result))
84 }
85 .instrument(span),
86 )
87 }
88}
89
90pub struct FocusCommand;
92
93impl CommandHandler<CommandContext<'_>> for FocusCommand {
94 fn name(&self) -> &'static str {
95 "/focus"
96 }
97
98 fn description(&self) -> &'static str {
99 "Show Focus Agent status (active session, knowledge block size)"
100 }
101
102 fn category(&self) -> SlashCategory {
103 SlashCategory::Advanced
104 }
105
106 fn feature_gate(&self) -> Option<&'static str> {
107 Some("context-compression")
108 }
109
110 fn requires_auth(&self) -> bool {
111 true
112 }
113
114 fn handle<'a>(
115 &'a self,
116 ctx: &'a mut CommandContext<'_>,
117 _args: &'a str,
118 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
119 use tracing::Instrument as _;
120 let span = tracing::info_span!("commands.focus.handle");
121 Box::pin(
122 async move {
123 let result = ctx.agent.focus_status();
124 Ok(CommandOutput::Message(result))
125 }
126 .instrument(span),
127 )
128 }
129}
130
131pub struct SideQuestCommand;
133
134impl CommandHandler<CommandContext<'_>> for SideQuestCommand {
135 fn name(&self) -> &'static str {
136 "/sidequest"
137 }
138
139 fn description(&self) -> &'static str {
140 "Show SideQuest eviction stats (passes run, tokens freed)"
141 }
142
143 fn category(&self) -> SlashCategory {
144 SlashCategory::Advanced
145 }
146
147 fn feature_gate(&self) -> Option<&'static str> {
148 Some("context-compression")
149 }
150
151 fn requires_auth(&self) -> bool {
152 true
153 }
154
155 fn handle<'a>(
156 &'a self,
157 ctx: &'a mut CommandContext<'_>,
158 _args: &'a str,
159 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
160 use tracing::Instrument as _;
161 let span = tracing::info_span!("commands.sidequest.handle");
162 Box::pin(
163 async move {
164 let result = ctx.agent.sidequest_status();
165 Ok(CommandOutput::Message(result))
166 }
167 .instrument(span),
168 )
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
176 use crate::sink::NullSink;
177 use std::assert_matches;
178
179 #[test]
180 fn status_name_and_description() {
181 assert_eq!(StatusCommand.name(), "/status");
182 assert!(!StatusCommand.description().is_empty());
183 }
184
185 #[test]
186 fn guardrail_name_and_description() {
187 assert_eq!(GuardrailCommand.name(), "/guardrail");
188 assert!(!GuardrailCommand.description().is_empty());
189 }
190
191 #[test]
192 fn focus_name_and_description() {
193 assert_eq!(FocusCommand.name(), "/focus");
194 assert!(!FocusCommand.description().is_empty());
195 }
196
197 #[test]
198 fn sidequest_name_and_description() {
199 assert_eq!(SideQuestCommand.name(), "/sidequest");
200 assert!(!SideQuestCommand.description().is_empty());
201 }
202
203 #[tokio::test]
204 async fn status_returns_message() {
205 let mut sink = NullSink;
206 let mut debug = MockDebug;
207 let mut messages = MockMessages;
208 let session = MockSession;
209 let mut agent = crate::NullAgent;
210 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
211 let out = StatusCommand.handle(&mut ctx, "").await.unwrap();
212 assert_matches!(out, CommandOutput::Message(_));
213 }
214
215 #[tokio::test]
216 async fn guardrail_returns_message() {
217 let mut sink = NullSink;
218 let mut debug = MockDebug;
219 let mut messages = MockMessages;
220 let session = MockSession;
221 let mut agent = crate::NullAgent;
222 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
223 let out = GuardrailCommand.handle(&mut ctx, "").await.unwrap();
224 assert_matches!(out, CommandOutput::Message(_));
225 }
226
227 #[tokio::test]
228 async fn focus_returns_message() {
229 let mut sink = NullSink;
230 let mut debug = MockDebug;
231 let mut messages = MockMessages;
232 let session = MockSession;
233 let mut agent = crate::NullAgent;
234 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
235 let out = FocusCommand.handle(&mut ctx, "").await.unwrap();
236 assert_matches!(out, CommandOutput::Message(_));
237 }
238
239 #[tokio::test]
240 async fn sidequest_returns_message() {
241 let mut sink = NullSink;
242 let mut debug = MockDebug;
243 let mut messages = MockMessages;
244 let session = MockSession;
245 let mut agent = crate::NullAgent;
246 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
247 let out = SideQuestCommand.handle(&mut ctx, "").await.unwrap();
248 assert_matches!(out, CommandOutput::Message(_));
249 }
250}