huskies: merge 1213 story Chat "stop" command that immediately aborts the in-flight LLM turn

This commit is contained in:
Huskies Agent
2026-07-18 11:46:52 +00:00
parent b91e2d53ff
commit cebe9e2737
7 changed files with 367 additions and 35 deletions
+103
View File
@@ -0,0 +1,103 @@
//! 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<StopCommand> {
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);
}
}