1pub mod registry;
2
3use crate::tui::core_commands::{CoreCommandType as CoreCommand, SlashCommandError};
4use crate::tui::custom_commands::CustomCommand;
5use std::fmt;
6use std::str::FromStr;
7use strum::{Display, EnumIter, IntoEnumIterator};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum TuiCommandError {
13 #[error("Unknown command: {0}")]
14 UnknownCommand(String),
15 #[error(transparent)]
16 CoreParseError(#[from] SlashCommandError),
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum TuiCommand {
22 New,
24 ReloadFiles,
26 Theme(Option<String>),
28 Auth,
30 Help(Option<String>),
32 EditingMode(Option<String>),
34 Mcp,
36 Workspace(Option<String>),
38 Custom(CustomCommand),
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Display)]
45#[strum(serialize_all = "kebab-case")]
46pub enum TuiCommandType {
47 New,
48 ReloadFiles,
49 Theme,
50 Auth,
51 Help,
52 EditingMode,
53 Mcp,
54 Workspace,
55}
56
57impl TuiCommandType {
58 pub fn command_name(&self) -> String {
59 match self {
60 TuiCommandType::New => self.to_string(),
61 TuiCommandType::ReloadFiles => self.to_string(),
62 TuiCommandType::Theme => self.to_string(),
63 TuiCommandType::Auth => self.to_string(),
64 TuiCommandType::Help => self.to_string(),
65 TuiCommandType::EditingMode => self.to_string(),
66 TuiCommandType::Mcp => self.to_string(),
67 TuiCommandType::Workspace => self.to_string(),
68 }
69 }
70
71 pub fn description(&self) -> &'static str {
72 match self {
73 TuiCommandType::New => "Start a new conversation session",
74 TuiCommandType::ReloadFiles => "Reload file cache in the TUI",
75 TuiCommandType::Theme => "Change or list available themes",
76 TuiCommandType::Auth => "Manage authentication settings",
77 TuiCommandType::Help => "Show help information",
78 TuiCommandType::EditingMode => "Switch between editing modes (simple/vim)",
79 TuiCommandType::Mcp => "Show MCP server connection status",
80 TuiCommandType::Workspace => "Show workspace status",
81 }
82 }
83
84 pub fn usage(&self) -> String {
85 match self {
86 TuiCommandType::New => format!("/{}", self.command_name()),
87 TuiCommandType::ReloadFiles => format!("/{}", self.command_name()),
88 TuiCommandType::Theme => format!("/{} [theme_name]", self.command_name()),
89 TuiCommandType::Auth => format!("/{}", self.command_name()),
90 TuiCommandType::Help => format!("/{} [command]", self.command_name()),
91 TuiCommandType::EditingMode => format!("/{} [simple|vim]", self.command_name()),
92 TuiCommandType::Mcp => format!("/{}", self.command_name()),
93 TuiCommandType::Workspace => format!("/{} [workspace_id]", self.command_name()),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Display)]
99#[strum(serialize_all = "kebab-case")]
100pub enum CoreCommandType {
101 Model,
102 Agent,
103 Compact,
104}
105
106impl CoreCommandType {
107 pub fn command_name(&self) -> String {
108 match self {
109 CoreCommandType::Model => self.to_string(),
110 CoreCommandType::Agent => self.to_string(),
111 CoreCommandType::Compact => self.to_string(),
112 }
113 }
114
115 pub fn description(&self) -> &'static str {
116 match self {
117 CoreCommandType::Model => "Show or change the current model",
118 CoreCommandType::Agent => "Switch primary agent mode (normal/plan/yolo)",
119 CoreCommandType::Compact => "Summarize the current conversation",
120 }
121 }
122
123 pub fn usage(&self) -> String {
124 match self {
125 CoreCommandType::Model => format!("/{} [model_name]", self.command_name()),
126 CoreCommandType::Agent => format!("/{} <mode>", self.command_name()),
127 CoreCommandType::Compact => format!("/{}", self.command_name()),
128 }
129 }
130
131 pub fn to_core_command(&self, args: &[&str]) -> Option<CoreCommand> {
132 match self {
133 CoreCommandType::Model => {
134 let target = if args.is_empty() {
135 None
136 } else {
137 Some(args.join(" "))
138 };
139 Some(CoreCommand::Model { target })
140 }
141 CoreCommandType::Agent => {
142 let target = if args.is_empty() {
143 None
144 } else {
145 Some(args.join(" "))
146 };
147 Some(CoreCommand::Agent { target })
148 }
149 CoreCommandType::Compact => Some(CoreCommand::Compact),
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
156pub enum AppCommand {
157 Tui(TuiCommand),
159 Core(CoreCommand),
161}
162
163impl TuiCommand {
164 fn parse_without_slash(command: &str) -> Result<Self, TuiCommandError> {
166 let parts: Vec<&str> = command.split_whitespace().collect();
167 let cmd_name = parts.first().copied().unwrap_or("");
168
169 for cmd_type in TuiCommandType::iter() {
170 if cmd_name == cmd_type.command_name() {
171 return match cmd_type {
172 TuiCommandType::New => Ok(TuiCommand::New),
173 TuiCommandType::ReloadFiles => Ok(TuiCommand::ReloadFiles),
174 TuiCommandType::Theme => {
175 let theme_name = parts.get(1).map(|s| (*s).to_string());
176 Ok(TuiCommand::Theme(theme_name))
177 }
178 TuiCommandType::Auth => Ok(TuiCommand::Auth),
179 TuiCommandType::Help => {
180 let command_name = parts.get(1).map(|s| (*s).to_string());
181 Ok(TuiCommand::Help(command_name))
182 }
183 TuiCommandType::EditingMode => {
184 let mode_name = parts.get(1).map(|s| (*s).to_string());
185 Ok(TuiCommand::EditingMode(mode_name))
186 }
187 TuiCommandType::Mcp => Ok(TuiCommand::Mcp),
188 TuiCommandType::Workspace => {
189 let workspace_id = parts.get(1).map(|s| (*s).to_string());
190 Ok(TuiCommand::Workspace(workspace_id))
191 }
192 };
193 }
194 }
195
196 Err(TuiCommandError::UnknownCommand(command.to_string()))
197 }
198
199 pub fn as_command_str(&self) -> String {
200 match self {
201 TuiCommand::New => TuiCommandType::New.command_name().clone(),
202 TuiCommand::ReloadFiles => TuiCommandType::ReloadFiles.command_name().clone(),
203 TuiCommand::Theme(None) => TuiCommandType::Theme.command_name().clone(),
204 TuiCommand::Theme(Some(name)) => {
205 format!("{} {}", TuiCommandType::Theme.command_name(), name)
206 }
207 TuiCommand::Auth => TuiCommandType::Auth.command_name().clone(),
208 TuiCommand::Help(None) => TuiCommandType::Help.command_name().clone(),
209 TuiCommand::Help(Some(cmd)) => {
210 format!("{} {}", TuiCommandType::Help.command_name(), cmd)
211 }
212 TuiCommand::EditingMode(None) => TuiCommandType::EditingMode.command_name().clone(),
213 TuiCommand::EditingMode(Some(mode)) => {
214 format!("{} {}", TuiCommandType::EditingMode.command_name(), mode)
215 }
216 TuiCommand::Mcp => TuiCommandType::Mcp.command_name().clone(),
217 TuiCommand::Workspace(None) => TuiCommandType::Workspace.command_name().clone(),
218 TuiCommand::Workspace(Some(workspace_id)) => {
219 format!(
220 "{} {}",
221 TuiCommandType::Workspace.command_name(),
222 workspace_id
223 )
224 }
225 TuiCommand::Custom(cmd) => cmd.name().to_string(),
226 }
227 }
228}
229
230impl AppCommand {
231 pub fn parse(input: &str) -> Result<Self, TuiCommandError> {
233 let command = input.trim();
235 let command = command.strip_prefix('/').unwrap_or(command);
236
237 let parts: Vec<&str> = command.split_whitespace().collect();
238 let cmd_name = parts.first().copied().unwrap_or("");
239
240 for tui_type in TuiCommandType::iter() {
242 if cmd_name == tui_type.command_name() {
243 return TuiCommand::parse_without_slash(command).map(AppCommand::Tui);
244 }
245 }
246
247 for core_type in CoreCommandType::iter() {
249 if cmd_name == core_type.command_name() {
250 let args: Vec<&str> = parts.into_iter().skip(1).collect();
251 if let Some(core_cmd) = core_type.to_core_command(&args) {
252 return Ok(AppCommand::Core(core_cmd));
253 }
254 return Err(TuiCommandError::UnknownCommand(command.to_string()));
255 }
256 }
257
258 if cmd_name == "mode" {
259 let args: Vec<&str> = parts.into_iter().skip(1).collect();
260 let target = if args.is_empty() {
261 None
262 } else {
263 Some(args.join(" "))
264 };
265 return Ok(AppCommand::Core(CoreCommand::Agent { target }));
266 }
267
268 Err(TuiCommandError::UnknownCommand(command.to_string()))
271 }
272
273 pub fn as_command_str(&self) -> String {
275 match self {
276 AppCommand::Tui(tui_cmd) => format!("/{}", tui_cmd.as_command_str()),
277 AppCommand::Core(core_cmd) => core_cmd.to_string(),
278 }
279 }
280}
281
282impl fmt::Display for TuiCommand {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 write!(f, "/{}", self.as_command_str())
285 }
286}
287
288impl fmt::Display for AppCommand {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 write!(f, "{}", self.as_command_str())
291 }
292}
293
294impl FromStr for AppCommand {
295 type Err = TuiCommandError;
296
297 fn from_str(s: &str) -> Result<Self, Self::Err> {
298 Self::parse(s)
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_parse_tui_commands() {
308 assert!(matches!(
309 AppCommand::parse("/reload-files").unwrap(),
310 AppCommand::Tui(TuiCommand::ReloadFiles)
311 ));
312 assert!(matches!(
313 AppCommand::parse("/theme").unwrap(),
314 AppCommand::Tui(TuiCommand::Theme(None))
315 ));
316 assert!(matches!(
317 AppCommand::parse("/theme gruvbox").unwrap(),
318 AppCommand::Tui(TuiCommand::Theme(Some(_)))
319 ));
320 assert!(matches!(
321 AppCommand::parse("/mcp").unwrap(),
322 AppCommand::Tui(TuiCommand::Mcp)
323 ));
324 assert!(matches!(
325 AppCommand::parse("/workspace").unwrap(),
326 AppCommand::Tui(TuiCommand::Workspace(None))
327 ));
328 }
329
330 #[test]
331 fn test_parse_core_commands() {
332 assert!(matches!(
333 AppCommand::parse("/help").unwrap(),
334 AppCommand::Tui(TuiCommand::Help(None))
335 ));
336 assert!(matches!(
337 AppCommand::parse("/model opus").unwrap(),
338 AppCommand::Core(CoreCommand::Model { .. })
339 ));
340 assert!(matches!(
341 AppCommand::parse("/compact").unwrap(),
342 AppCommand::Core(CoreCommand::Compact)
343 ));
344 assert!(matches!(
345 AppCommand::parse("/agent plan").unwrap(),
346 AppCommand::Core(CoreCommand::Agent { .. })
347 ));
348 assert!(matches!(
349 AppCommand::parse("/mode yolo").unwrap(),
350 AppCommand::Core(CoreCommand::Agent { .. })
351 ));
352 }
353
354 #[test]
355 fn test_parse_new_command() {
356 assert!(matches!(
357 AppCommand::parse("/new").unwrap(),
358 AppCommand::Tui(TuiCommand::New)
359 ));
360 }
361
362 #[test]
363 fn test_display() {
364 assert_eq!(
365 AppCommand::Tui(TuiCommand::ReloadFiles).to_string(),
366 "/reload-files"
367 );
368 assert_eq!(AppCommand::Tui(TuiCommand::Help(None)).to_string(), "/help");
369 }
370
371 #[test]
372 fn test_error_formatting() {
373 let err = AppCommand::parse("/unknown-tui-cmd").unwrap_err();
375 assert_eq!(err.to_string(), "Unknown command: unknown-tui-cmd");
376 }
377
378 #[test]
379 fn test_tui_command_from_str() {
380 let cmd = "/reload-files".parse::<AppCommand>().unwrap();
381 assert!(matches!(cmd, AppCommand::Tui(TuiCommand::ReloadFiles)));
382 }
383}