Skip to main content

zeph_commands/handlers/
status.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Status display handlers: `/status`, `/guardrail`, `/focus`, `/sidequest`.
5
6use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12/// Display the current session status (provider, model, tokens, uptime, etc.).
13pub 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
49/// Display guardrail configuration and runtime statistics.
50pub 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
90/// Display Focus Agent status (active session, knowledge block size).
91pub 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
131/// Display `SideQuest` eviction statistics (passes run, tokens freed).
132pub 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}