1use std::future::Future;
7use std::pin::Pin;
8
9use crate::CommandHandler;
10use crate::context::CommandContext;
11use crate::{CommandError, CommandOutput, SlashCategory};
12
13pub struct ExitCommand;
19
20impl CommandHandler<CommandContext<'_>> for ExitCommand {
21 fn name(&self) -> &'static str {
22 "/exit"
23 }
24
25 fn description(&self) -> &'static str {
26 "Exit the agent (also: /quit)"
27 }
28
29 fn category(&self) -> SlashCategory {
30 SlashCategory::Session
31 }
32
33 fn handle<'a>(
34 &'a self,
35 ctx: &'a mut CommandContext<'_>,
36 _args: &'a str,
37 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
38 Box::pin(async move {
39 if ctx.session.supports_exit() {
40 Ok(CommandOutput::Exit)
41 } else {
42 ctx.sink
43 .send("/exit is not supported in this channel.")
44 .await?;
45 Ok(CommandOutput::Continue)
46 }
47 })
48 }
49}
50
51pub struct QuitCommand;
53
54impl CommandHandler<CommandContext<'_>> for QuitCommand {
55 fn name(&self) -> &'static str {
56 "/quit"
57 }
58
59 fn description(&self) -> &'static str {
60 "Exit the agent (alias for /exit)"
61 }
62
63 fn category(&self) -> SlashCategory {
64 SlashCategory::Session
65 }
66
67 fn handle<'a>(
68 &'a self,
69 ctx: &'a mut CommandContext<'_>,
70 _args: &'a str,
71 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
72 Box::pin(async move {
73 if ctx.session.supports_exit() {
74 Ok(CommandOutput::Exit)
75 } else {
76 ctx.sink
77 .send("/exit is not supported in this channel.")
78 .await?;
79 Ok(CommandOutput::Continue)
80 }
81 })
82 }
83}
84
85pub struct ClearCommand;
90
91impl CommandHandler<CommandContext<'_>> for ClearCommand {
92 fn name(&self) -> &'static str {
93 "/clear"
94 }
95
96 fn description(&self) -> &'static str {
97 "Clear conversation history"
98 }
99
100 fn category(&self) -> SlashCategory {
101 SlashCategory::Session
102 }
103
104 fn handle<'a>(
105 &'a self,
106 ctx: &'a mut CommandContext<'_>,
107 _args: &'a str,
108 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
109 Box::pin(async move {
110 ctx.messages.clear_history();
111 Ok(CommandOutput::Silent)
112 })
113 }
114}
115
116pub struct ResetCommand;
118
119impl CommandHandler<CommandContext<'_>> for ResetCommand {
120 fn name(&self) -> &'static str {
121 "/reset"
122 }
123
124 fn description(&self) -> &'static str {
125 "Reset conversation history (alias for /clear, replies with confirmation)"
126 }
127
128 fn category(&self) -> SlashCategory {
129 SlashCategory::Session
130 }
131
132 fn handle<'a>(
133 &'a self,
134 ctx: &'a mut CommandContext<'_>,
135 _args: &'a str,
136 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
137 Box::pin(async move {
138 ctx.messages.clear_history();
139 Ok(CommandOutput::Message(
140 "Conversation history reset.".to_owned(),
141 ))
142 })
143 }
144}
145
146pub struct ClearQueueCommand;
148
149impl CommandHandler<CommandContext<'_>> for ClearQueueCommand {
150 fn name(&self) -> &'static str {
151 "/clear-queue"
152 }
153
154 fn description(&self) -> &'static str {
155 "Discard queued messages"
156 }
157
158 fn category(&self) -> SlashCategory {
159 SlashCategory::Session
160 }
161
162 fn handle<'a>(
163 &'a self,
164 ctx: &'a mut CommandContext<'_>,
165 _args: &'a str,
166 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
167 Box::pin(async move {
168 let n = ctx.messages.drain_queue();
169 let _ = ctx.sink.send_queue_count(0).await;
171 Ok(CommandOutput::Message(format!(
172 "Cleared {n} queued messages."
173 )))
174 })
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::CommandRegistry;
182 use crate::context::CommandContext;
183 use crate::sink::ChannelSink;
184 use crate::traits::debug::DebugAccess;
185 use crate::traits::messages::MessageAccess;
186 use crate::traits::session::SessionAccess;
187 use std::future::Future;
188 use std::pin::Pin;
189
190 struct MockSink {
193 sent: Vec<String>,
194 }
195
196 impl ChannelSink for MockSink {
197 fn send<'a>(
198 &'a mut self,
199 msg: &'a str,
200 ) -> Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'a>> {
201 self.sent.push(msg.to_owned());
202 Box::pin(async { Ok(()) })
203 }
204
205 fn flush_chunks<'a>(
206 &'a mut self,
207 ) -> Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'a>> {
208 Box::pin(async { Ok(()) })
209 }
210
211 fn send_queue_count<'a>(
212 &'a mut self,
213 _count: usize,
214 ) -> Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'a>> {
215 Box::pin(async { Ok(()) })
216 }
217
218 fn supports_exit(&self) -> bool {
219 false
220 }
221 }
222
223 struct MockDebug;
224
225 impl DebugAccess for MockDebug {
226 fn log_status(&self) -> String {
227 String::new()
228 }
229
230 fn read_log_tail<'a>(
231 &'a self,
232 _n: usize,
233 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
234 Box::pin(async { None })
235 }
236
237 fn scrub(&self, text: &str) -> String {
238 text.to_owned()
239 }
240
241 fn dump_status(&self) -> Option<String> {
242 None
243 }
244
245 fn dump_format_name(&self) -> String {
246 "raw".to_owned()
247 }
248
249 fn enable_dump(&mut self, _dir: &str) -> Result<String, CommandError> {
250 Ok("/tmp".to_owned())
251 }
252
253 fn set_dump_format(&mut self, _name: &str) -> Result<(), CommandError> {
254 Ok(())
255 }
256 }
257
258 struct MockMessages {
259 pub cleared: bool,
260 pub queue: usize,
261 }
262
263 impl MessageAccess for MockMessages {
264 fn clear_history(&mut self) {
265 self.cleared = true;
266 }
267
268 fn queue_len(&self) -> usize {
269 self.queue
270 }
271
272 fn drain_queue(&mut self) -> usize {
273 let n = self.queue;
274 self.queue = 0;
275 n
276 }
277
278 fn notify_queue_count<'a>(
279 &'a mut self,
280 _count: usize,
281 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
282 Box::pin(async {})
283 }
284 }
285
286 struct MockSession {
287 supports_exit: bool,
288 }
289
290 impl SessionAccess for MockSession {
291 fn supports_exit(&self) -> bool {
292 self.supports_exit
293 }
294 }
295
296 fn make_ctx<'a>(
297 sink: &'a mut MockSink,
298 debug: &'a mut MockDebug,
299 messages: &'a mut MockMessages,
300 session: &'a MockSession,
301 agent: &'a mut crate::NullAgent,
302 ) -> CommandContext<'a> {
303 CommandContext {
304 sink,
305 debug,
306 messages,
307 session: session as &dyn SessionAccess,
308 agent,
309 }
310 }
311
312 #[tokio::test]
315 async fn exit_returns_exit_when_supported() {
316 let mut sink = MockSink { sent: vec![] };
317 let mut debug = MockDebug;
318 let mut messages = MockMessages {
319 cleared: false,
320 queue: 0,
321 };
322 let session = MockSession {
323 supports_exit: true,
324 };
325 let mut agent = crate::NullAgent;
326 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
327 let out = ExitCommand.handle(&mut ctx, "").await.unwrap();
328 assert!(matches!(out, CommandOutput::Exit));
329 }
330
331 #[tokio::test]
332 async fn exit_sends_message_when_not_supported() {
333 let mut sink = MockSink { sent: vec![] };
334 let mut debug = MockDebug;
335 let mut messages = MockMessages {
336 cleared: false,
337 queue: 0,
338 };
339 let session = MockSession {
340 supports_exit: false,
341 };
342 let mut agent = crate::NullAgent;
343 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
344 let out = ExitCommand.handle(&mut ctx, "").await.unwrap();
345 assert!(matches!(out, CommandOutput::Continue));
346 assert!(!sink.sent.is_empty());
347 }
348
349 #[tokio::test]
350 async fn clear_clears_history() {
351 let mut sink = MockSink { sent: vec![] };
352 let mut debug = MockDebug;
353 let mut messages = MockMessages {
354 cleared: false,
355 queue: 0,
356 };
357 let session = MockSession {
358 supports_exit: false,
359 };
360 let out = {
361 let mut agent = crate::NullAgent;
362 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
363 ClearCommand.handle(&mut ctx, "").await.unwrap()
364 };
365 assert!(matches!(out, CommandOutput::Silent));
366 assert!(messages.cleared);
367 }
368
369 #[tokio::test]
370 async fn reset_clears_and_confirms() {
371 let mut sink = MockSink { sent: vec![] };
372 let mut debug = MockDebug;
373 let mut messages = MockMessages {
374 cleared: false,
375 queue: 0,
376 };
377 let session = MockSession {
378 supports_exit: false,
379 };
380 let out = {
381 let mut agent = crate::NullAgent;
382 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
383 ResetCommand.handle(&mut ctx, "").await.unwrap()
384 };
385 let CommandOutput::Message(msg) = out else {
386 panic!("expected Message")
387 };
388 assert!(msg.contains("reset"));
389 assert!(messages.cleared);
390 }
391
392 #[tokio::test]
393 async fn clear_queue_drains_and_reports() {
394 let mut sink = MockSink { sent: vec![] };
395 let mut debug = MockDebug;
396 let mut messages = MockMessages {
397 cleared: false,
398 queue: 3,
399 };
400 let session = MockSession {
401 supports_exit: false,
402 };
403 let out = {
404 let mut agent = crate::NullAgent;
405 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
406 ClearQueueCommand.handle(&mut ctx, "").await.unwrap()
407 };
408 let CommandOutput::Message(msg) = out else {
409 panic!("expected Message")
410 };
411 assert!(msg.contains('3'));
412 assert_eq!(messages.queue, 0);
413 }
414
415 #[test]
416 fn registry_finds_all_session_commands() {
417 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
418 reg.register(ExitCommand);
419 reg.register(QuitCommand);
420 reg.register(ClearCommand);
421 reg.register(ResetCommand);
422 reg.register(ClearQueueCommand);
423
424 assert!(reg.find_handler("/exit").is_some());
425 assert!(reg.find_handler("/quit").is_some());
426 assert!(reg.find_handler("/clear").is_some());
427 assert!(reg.find_handler("/reset").is_some());
428 assert!(reg.find_handler("/clear-queue").is_some());
429 }
430}