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