159 lines
4.9 KiB
Rust
159 lines
4.9 KiB
Rust
|
|
//! 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<AgentPool>,
|
||
|
|
) -> 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<AgentPool>,
|
||
|
|
) -> 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<AgentPool>,
|
||
|
|
) -> 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<AgentPool>,
|
||
|
|
) -> 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<AgentPool>) -> 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<String, String> {
|
||
|
|
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 <number> <HH:MM>`, or `/timer cancel <number>`"
|
||
|
|
.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<AgentPool>) -> 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<AgentPool>,
|
||
|
|
) -> Option<String> {
|
||
|
|
use crate::chat::commands::CommandDispatch;
|
||
|
|
use std::collections::HashSet;
|
||
|
|
use std::sync::Mutex;
|
||
|
|
|
||
|
|
let ambient_rooms: Arc<Mutex<HashSet<String>>> = 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)
|
||
|
|
}
|