//! Stop command: cancel the in-flight LLM turn for the current room's chat //! session. //! //! `{bot_name} stop` (or `halt`/`abort`) cancels the actively-running Claude //! Code turn for this room via [`crate::chat::dispatcher::ChatDispatcher`] //! and clears any coalesced/pending messages. Unlike [`super::reset`] and //! [`crate::chat::compact`], which match on the first word and ignore //! trailing text, this requires an EXACT match (after mention-stripping) — //! an instruction like "stop X and do Y" must not be misread as an abort. use crate::chat::util::strip_bot_mention; /// A parsed stop/halt/abort command. #[derive(Debug, PartialEq)] pub struct StopCommand; /// Parse a stop command from a raw message body. /// /// Strips the bot mention prefix and requires the *entire* remaining, /// trimmed message to equal `stop`, `halt`, or `abort` (case-insensitive). /// Returns `None` for anything else, including a recognised word followed by /// trailing text (e.g. "stop the deployment"). pub fn extract_stop_command( message: &str, bot_name: &str, bot_user_id: &str, ) -> Option { let stripped = strip_bot_mention(message, bot_name, bot_user_id); let trimmed = stripped.trim(); if trimmed.eq_ignore_ascii_case("stop") || trimmed.eq_ignore_ascii_case("halt") || trimmed.eq_ignore_ascii_case("abort") { Some(StopCommand) } else { None } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn extract_bare_stop() { let cmd = extract_stop_command("stop", "Timmy", "@timmy:home.local"); assert_eq!(cmd, Some(StopCommand)); } #[test] fn extract_with_display_name() { let cmd = extract_stop_command("Timmy stop", "Timmy", "@timmy:home.local"); assert_eq!(cmd, Some(StopCommand)); } #[test] fn extract_with_full_user_id() { let cmd = extract_stop_command("@timmy:home.local stop", "Timmy", "@timmy:home.local"); assert_eq!(cmd, Some(StopCommand)); } #[test] fn extract_halt_synonym() { let cmd = extract_stop_command("Timmy halt", "Timmy", "@timmy:home.local"); assert_eq!(cmd, Some(StopCommand)); } #[test] fn extract_abort_synonym() { let cmd = extract_stop_command("abort", "Timmy", "@timmy:home.local"); assert_eq!(cmd, Some(StopCommand)); } #[test] fn extract_case_insensitive() { let cmd = extract_stop_command("Timmy STOP", "Timmy", "@timmy:home.local"); assert_eq!(cmd, Some(StopCommand)); } #[test] fn extract_non_stop_returns_none() { let cmd = extract_stop_command("Timmy help", "Timmy", "@timmy:home.local"); assert_eq!(cmd, None); } #[test] fn extract_with_trailing_text_returns_none() { // "stop X and do Y" is an instruction, not an abort request. let cmd = extract_stop_command("Timmy stop X and do Y", "Timmy", "@timmy:home.local"); assert_eq!(cmd, None); } #[test] fn extract_stop_followed_by_words_returns_none() { let cmd = extract_stop_command("stop the deployment please", "Timmy", "@timmy:home.local"); assert_eq!(cmd, None); } }