//! Bot command I/O — the ONLY place in `service/bot_command/` that may call //! transport handlers, load stores, spawn tasks, or interact with the agent //! pool. //! //! Every function here is a thin adapter over the underlying matrix/timer/htop //! handlers. No argument parsing or business logic lives here — that belongs in //! `parse.rs` or `mod.rs`. use crate::agents::AgentPool; use std::path::Path; use std::sync::Arc; use super::parse::{AssignArgs, StartArgs}; /// Call the Matrix `assign` handler with pre-validated arguments. pub(super) async fn call_assign( args: &AssignArgs, project_root: &Path, agents: &Arc, ) -> String { crate::chat::transport::matrix::assign::handle_assign( "web-ui", &args.number, &args.model, project_root, agents, ) .await } /// Call the Matrix `start` handler with pre-validated arguments. pub(super) async fn call_start( args: &StartArgs, project_root: &Path, agents: &Arc, ) -> String { crate::chat::transport::matrix::start::handle_start( "web-ui", &args.number, args.hint.as_deref(), project_root, agents, ) .await } /// Call the Matrix `delete` handler with a pre-validated story number. pub(super) async fn call_delete( number: &str, project_root: &Path, agents: &Arc, ) -> String { crate::chat::transport::matrix::delete::handle_delete("web-ui", number, project_root, agents) .await } /// Call the Matrix `rmtree` handler with a pre-validated story number. pub(super) async fn call_rmtree( number: &str, project_root: &Path, agents: &Arc, ) -> String { crate::chat::transport::matrix::rmtree::handle_rmtree("web-ui", number, project_root, agents) .await } /// Call the Matrix `rebuild` handler. pub(super) async fn call_rebuild(project_root: &Path, agents: &Arc) -> String { crate::chat::transport::matrix::rebuild::handle_rebuild("web-ui", project_root, agents).await } /// Parse and execute a `timer` command. /// /// Returns `Err` with a usage string if the timer arguments cannot be parsed. pub(super) async fn call_timer(args: &str, project_root: &Path) -> Result { let synthetic = format!("__web_ui__ timer {args}"); let timer_cmd = match crate::chat::timer::extract_timer_command( &synthetic, "__web_ui__", "@__web_ui__:localhost", ) { Some(cmd) => cmd, None => { return Err( "Usage: `/timer list`, `/timer `, or `/timer cancel `" .to_string(), ); } }; let store = crate::chat::timer::TimerStore::load(project_root.join(".huskies").join("timers.json")); Ok(crate::chat::timer::handle_timer_command(timer_cmd, &store, project_root).await) } /// Build an `htop` snapshot for the web UI. /// /// The web UI uses one-shot HTTP requests, so live-updating sessions are not /// supported. `htop stop` returns a helpful explanation instead of an error. pub(super) fn call_htop(args: &str, agents: &Arc) -> String { use crate::chat::transport::matrix::htop::{HtopCommand, build_htop_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), } } /// Dispatch through the synchronous command registry. /// /// Returns `Some(response)` if the command keyword is registered, or `None` /// if the keyword is unknown. pub(super) fn call_sync( cmd: &str, args: &str, project_root: &Path, agents: &Arc, ) -> Option { use crate::chat::commands::CommandDispatch; use std::collections::HashSet; use std::sync::Mutex; let ambient_rooms: Arc>> = Arc::new(Mutex::new(HashSet::new())); let bot_name = "__web_ui__"; let bot_user_id = "@__web_ui__:localhost"; let room_id = "__web_ui__"; let dispatch = CommandDispatch { bot_name, bot_user_id, project_root, agents, ambient_rooms: &ambient_rooms, room_id, }; // Build a synthetic bot-addressed message so the registry parses it // identically to messages from chat transports. let synthetic = if args.is_empty() { format!("{bot_name} {cmd}") } else { format!("{bot_name} {cmd} {args}") }; crate::chat::commands::try_handle_command(&dispatch, &synthetic) }