#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RouteResult {
SlashCommand(SlashCommand),
NaturalLanguage(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlashCommand {
Help,
Tools,
ToolDescribe { name: String },
TapeSearch { query: String },
TapeInfo,
Anchors,
Handoff { name: Option<String> },
Usage,
Quit,
Shell { command: String },
}
#[must_use]
pub fn route(input: &str) -> RouteResult {
let trimmed = input.trim();
if !trimmed.starts_with('/') {
return RouteResult::NaturalLanguage(trimmed.to_string());
}
let rest = &trimmed[1..];
let (cmd, args) = rest
.split_once(|c: char| c.is_ascii_whitespace())
.map_or((rest, ""), |(c, a)| (c, a.trim()));
match cmd {
"help" | "h" => RouteResult::SlashCommand(SlashCommand::Help),
"tools" => RouteResult::SlashCommand(SlashCommand::Tools),
"tool.describe" => {
RouteResult::SlashCommand(SlashCommand::ToolDescribe { name: args.to_string() })
}
"tape.search" => {
RouteResult::SlashCommand(SlashCommand::TapeSearch { query: args.to_string() })
}
"tape.info" => RouteResult::SlashCommand(SlashCommand::TapeInfo),
"anchors" => RouteResult::SlashCommand(SlashCommand::Anchors),
"handoff" => RouteResult::SlashCommand(SlashCommand::Handoff {
name: if args.is_empty() { None } else { Some(args.to_string()) },
}),
"usage" => RouteResult::SlashCommand(SlashCommand::Usage),
"quit" | "exit" => RouteResult::SlashCommand(SlashCommand::Quit),
_ => {
RouteResult::SlashCommand(SlashCommand::Shell { command: rest.to_string() })
}
}
}
#[must_use]
pub fn help_text() -> String {
"\
Available commands:
/help Show this help message
/tools List all registered tools
/tool.describe NAME Show full schema for a tool
/tape.search QUERY Search conversation history
/tape.info Show tape statistics
/anchors List all tape anchors
/handoff [NAME] Reset context window (create handoff point)
/usage Show cumulative session token usage
/quit Exit the session
Natural language input (without /) goes to the AI model."
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn natural_language_passthrough() {
let result = route("hello world");
assert_eq!(result, RouteResult::NaturalLanguage("hello world".to_string()));
}
#[test]
fn natural_language_trims_whitespace() {
let result = route(" hello ");
assert_eq!(result, RouteResult::NaturalLanguage("hello".to_string()));
}
#[test]
fn help_command() {
assert_eq!(route("/help"), RouteResult::SlashCommand(SlashCommand::Help));
assert_eq!(route("/h"), RouteResult::SlashCommand(SlashCommand::Help));
}
#[test]
fn tools_command() {
assert_eq!(route("/tools"), RouteResult::SlashCommand(SlashCommand::Tools));
}
#[test]
fn tool_describe_command() {
assert_eq!(
route("/tool.describe file.read"),
RouteResult::SlashCommand(SlashCommand::ToolDescribe { name: "file.read".to_string() })
);
}
#[test]
fn tape_search_command() {
assert_eq!(
route("/tape.search error handling"),
RouteResult::SlashCommand(SlashCommand::TapeSearch {
query: "error handling".to_string()
})
);
}
#[test]
fn handoff_with_name() {
assert_eq!(
route("/handoff phase-2"),
RouteResult::SlashCommand(SlashCommand::Handoff { name: Some("phase-2".to_string()) })
);
}
#[test]
fn handoff_without_name() {
assert_eq!(
route("/handoff"),
RouteResult::SlashCommand(SlashCommand::Handoff { name: None })
);
}
#[test]
fn usage_command() {
assert_eq!(route("/usage"), RouteResult::SlashCommand(SlashCommand::Usage));
}
#[test]
fn quit_and_exit_commands() {
assert_eq!(route("/quit"), RouteResult::SlashCommand(SlashCommand::Quit));
assert_eq!(route("/exit"), RouteResult::SlashCommand(SlashCommand::Quit));
}
#[test]
fn unknown_command_becomes_shell() {
assert_eq!(
route("/git status"),
RouteResult::SlashCommand(SlashCommand::Shell { command: "git status".to_string() })
);
}
#[test]
fn shell_command_preserves_full_text() {
assert_eq!(
route("/ls -la /tmp"),
RouteResult::SlashCommand(SlashCommand::Shell { command: "ls -la /tmp".to_string() })
);
}
}