diff --git a/server/src/http/bot_command.rs b/server/src/http/bot_command.rs index 30f12897..7081f27a 100644 --- a/server/src/http/bot_command.rs +++ b/server/src/http/bot_command.rs @@ -82,6 +82,7 @@ async fn dispatch_command( "delete" => dispatch_delete(args, project_root, agents).await, "rebuild" => dispatch_rebuild(project_root, agents).await, "timer" => dispatch_timer(args, project_root).await, + "htop" => dispatch_htop(args, agents).await, // All other commands go through the synchronous command registry. _ => dispatch_sync(cmd, args, project_root, agents), } @@ -230,6 +231,34 @@ async fn dispatch_timer(args: &str, project_root: &std::path::Path) -> String { crate::chat::timer::handle_timer_command(timer_cmd, &store, project_root).await } +/// Handle the `htop` command from the web UI. +/// +/// The web UI uses a one-shot HTTP request, so live updates are not possible +/// here. Returns a static snapshot of the process dashboard. For `htop stop`, +/// returns a helpful message (no persistent session state exists in the web UI). +async fn dispatch_htop(args: &str, agents: &Arc) -> String { + use crate::chat::transport::matrix::htop::{HtopCommand, build_htop_message}; + + // Re-use the existing parser by constructing a synthetic message. + let synthetic = if args.is_empty() { + "__web_ui__ htop".to_string() + } else { + format!("__web_ui__ htop {args}") + }; + + match crate::chat::transport::matrix::htop::extract_htop_command( + &synthetic, + "__web_ui__", + "@__web_ui__:localhost", + ) { + Some(HtopCommand::Stop) => "No active htop session in the web UI. \ + Live sessions are only supported in chat transports (Matrix, Slack, Discord)." + .to_string(), + Some(HtopCommand::Start { duration_secs }) => build_htop_message(agents, 0, duration_secs), + None => build_htop_message(agents, 0, 300), + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -349,6 +378,78 @@ mod tests { ); } + // -- htop (web-UI slash-command path) ------------------------------------ + + #[tokio::test] + async fn htop_returns_dashboard_not_unknown_command() { + let dir = TempDir::new().unwrap(); + let api = test_api(&dir); + let body = BotCommandRequest { + command: "htop".to_string(), + args: String::new(), + }; + let result = api.run_command(Json(body)).await; + assert!(result.is_ok()); + let resp = result.unwrap().0; + assert!( + !resp.response.contains("Unknown command"), + "htop should not return 'Unknown command': {}", + resp.response + ); + assert!( + resp.response.contains("htop"), + "htop response should contain 'htop': {}", + resp.response + ); + } + + #[tokio::test] + async fn htop_with_duration_returns_dashboard() { + let dir = TempDir::new().unwrap(); + let api = test_api(&dir); + let body = BotCommandRequest { + command: "htop".to_string(), + args: "10m".to_string(), + }; + let result = api.run_command(Json(body)).await; + assert!(result.is_ok()); + let resp = result.unwrap().0; + assert!( + !resp.response.contains("Unknown command"), + "htop 10m should not return 'Unknown command': {}", + resp.response + ); + } + + #[tokio::test] + async fn htop_stop_returns_response_not_unknown_command() { + let dir = TempDir::new().unwrap(); + let api = test_api(&dir); + let body = BotCommandRequest { + command: "htop".to_string(), + args: "stop".to_string(), + }; + let result = api.run_command(Json(body)).await; + assert!(result.is_ok()); + let resp = result.unwrap().0; + assert!( + !resp.response.contains("Unknown command"), + "htop stop should not return 'Unknown command': {}", + resp.response + ); + } + + // -- htop bot-command path (regression: htop must remain in command registry) -- + + #[test] + fn htop_is_registered_in_bot_command_registry() { + let commands = crate::chat::commands::commands(); + assert!( + commands.iter().any(|c| c.name == "htop"), + "htop must be registered in the bot command registry so /help lists it" + ); + } + #[tokio::test] async fn run_command_requires_project_root() { // Create a context with no project root set.