1#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum RouteResult {
26 SlashCommand(SlashCommand),
28 NaturalLanguage(String),
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SlashCommand {
35 Help,
37 Tools,
39 ToolDescribe { name: String },
41 TapeSearch { query: String },
43 TapeInfo,
45 Anchors,
47 Handoff { name: Option<String> },
49 Quit,
51 Shell { command: String },
53}
54
55#[must_use]
60pub fn route(input: &str) -> RouteResult {
61 let trimmed = input.trim();
62
63 if !trimmed.starts_with('/') {
64 return RouteResult::NaturalLanguage(trimmed.to_string());
65 }
66
67 let rest = &trimmed[1..];
69
70 let (cmd, args) = rest
72 .split_once(|c: char| c.is_ascii_whitespace())
73 .map_or((rest, ""), |(c, a)| (c, a.trim()));
74
75 match cmd {
76 "help" | "h" => RouteResult::SlashCommand(SlashCommand::Help),
77 "tools" => RouteResult::SlashCommand(SlashCommand::Tools),
78 "tool.describe" => {
79 RouteResult::SlashCommand(SlashCommand::ToolDescribe { name: args.to_string() })
80 }
81 "tape.search" => {
82 RouteResult::SlashCommand(SlashCommand::TapeSearch { query: args.to_string() })
83 }
84 "tape.info" => RouteResult::SlashCommand(SlashCommand::TapeInfo),
85 "anchors" => RouteResult::SlashCommand(SlashCommand::Anchors),
86 "handoff" => RouteResult::SlashCommand(SlashCommand::Handoff {
87 name: if args.is_empty() { None } else { Some(args.to_string()) },
88 }),
89 "quit" | "exit" => RouteResult::SlashCommand(SlashCommand::Quit),
90 _ => {
91 RouteResult::SlashCommand(SlashCommand::Shell { command: rest.to_string() })
93 }
94 }
95}
96
97#[must_use]
99pub fn help_text() -> String {
100 "\
101Available commands:
102 /help Show this help message
103 /tools List all registered tools
104 /tool.describe NAME Show full schema for a tool
105 /tape.search QUERY Search conversation history
106 /tape.info Show tape statistics
107 /anchors List all tape anchors
108 /handoff [NAME] Reset context window (create handoff point)
109 /quit Exit the session
110
111Natural language input (without /) goes to the AI model."
112 .to_string()
113}
114
115#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn natural_language_passthrough() {
123 let result = route("hello world");
124 assert_eq!(result, RouteResult::NaturalLanguage("hello world".to_string()));
125 }
126
127 #[test]
128 fn natural_language_trims_whitespace() {
129 let result = route(" hello ");
130 assert_eq!(result, RouteResult::NaturalLanguage("hello".to_string()));
131 }
132
133 #[test]
134 fn help_command() {
135 assert_eq!(route("/help"), RouteResult::SlashCommand(SlashCommand::Help));
136 assert_eq!(route("/h"), RouteResult::SlashCommand(SlashCommand::Help));
137 }
138
139 #[test]
140 fn tools_command() {
141 assert_eq!(route("/tools"), RouteResult::SlashCommand(SlashCommand::Tools));
142 }
143
144 #[test]
145 fn tool_describe_command() {
146 assert_eq!(
147 route("/tool.describe file.read"),
148 RouteResult::SlashCommand(SlashCommand::ToolDescribe { name: "file.read".to_string() })
149 );
150 }
151
152 #[test]
153 fn tape_search_command() {
154 assert_eq!(
155 route("/tape.search error handling"),
156 RouteResult::SlashCommand(SlashCommand::TapeSearch {
157 query: "error handling".to_string()
158 })
159 );
160 }
161
162 #[test]
163 fn handoff_with_name() {
164 assert_eq!(
165 route("/handoff phase-2"),
166 RouteResult::SlashCommand(SlashCommand::Handoff { name: Some("phase-2".to_string()) })
167 );
168 }
169
170 #[test]
171 fn handoff_without_name() {
172 assert_eq!(
173 route("/handoff"),
174 RouteResult::SlashCommand(SlashCommand::Handoff { name: None })
175 );
176 }
177
178 #[test]
179 fn quit_and_exit_commands() {
180 assert_eq!(route("/quit"), RouteResult::SlashCommand(SlashCommand::Quit));
181 assert_eq!(route("/exit"), RouteResult::SlashCommand(SlashCommand::Quit));
182 }
183
184 #[test]
185 fn unknown_command_becomes_shell() {
186 assert_eq!(
187 route("/git status"),
188 RouteResult::SlashCommand(SlashCommand::Shell { command: "git status".to_string() })
189 );
190 }
191
192 #[test]
193 fn shell_command_preserves_full_text() {
194 assert_eq!(
195 route("/ls -la /tmp"),
196 RouteResult::SlashCommand(SlashCommand::Shell { command: "ls -la /tmp".to_string() })
197 );
198 }
199}