1use crate::agent::extension::{
2 AutocompleteItem, CommandHandler, CommandResult, Extension, SlashCommand,
3};
4use std::borrow::Cow;
5use std::sync::{Arc, Mutex};
6
7pub struct CommandsExtension {
11 available_models: Vec<String>,
13 pub session_info: Arc<Mutex<Option<SessionInfoInternal>>>,
15}
16
17#[derive(Debug, Clone)]
19pub struct SessionInfoInternal {
20 pub session_id: String,
21 pub file_path: Option<std::path::PathBuf>,
22 pub name: Option<String>,
23 pub message_count: usize,
24}
25
26impl CommandsExtension {
27 pub fn new(available_models: Vec<String>) -> Self {
28 Self {
29 available_models,
30 session_info: Arc::new(Mutex::new(None)),
31 }
32 }
33
34 pub fn set_session_info(&self, info: SessionInfoInternal) {
36 if let Ok(mut guard) = self.session_info.lock() {
37 *guard = Some(info);
38 }
39 }
40}
41
42impl Extension for CommandsExtension {
43 fn name(&self) -> Cow<'static, str> {
44 "commands".into()
45 }
46
47 fn commands(&self) -> Vec<SlashCommand> {
48 vec![
49 SlashCommand {
50 name: "quit".to_string(),
51 description: "Exit rab".to_string(),
52 handler: Box::new(QuitCommand),
53 },
54 SlashCommand {
55 name: "model".to_string(),
56 description: "Switch model".to_string(),
57 handler: Box::new(ModelCommand {
58 available_models: self.available_models.clone(),
59 }),
60 },
61 SlashCommand {
62 name: "hotkeys".to_string(),
63 description: "Show keyboard shortcuts".to_string(),
64 handler: Box::new(HotkeysCommand),
65 },
66 SlashCommand {
67 name: "reload".to_string(),
68 description: "Reload settings and auth from disk".to_string(),
69 handler: Box::new(ReloadCommand),
70 },
71 SlashCommand {
72 name: "new".to_string(),
73 description: "Start a new session (clear conversation)".to_string(),
74 handler: Box::new(NewCommand),
75 },
76 SlashCommand {
77 name: "resume".to_string(),
78 description: "Resume a different session".to_string(),
79 handler: Box::new(ResumeCommand),
80 },
81 SlashCommand {
82 name: "session".to_string(),
83 description: "Show session info".to_string(),
84 handler: Box::new(SessionInfoCommand {
85 info: self.session_info.clone(),
86 }),
87 },
88 SlashCommand {
89 name: "name".to_string(),
90 description: "Set session display name".to_string(),
91 handler: Box::new(NameCommand),
92 },
93 ]
94 }
95}
96
97struct QuitCommand;
100
101impl CommandHandler for QuitCommand {
102 fn execute(&self, _args: &str) -> anyhow::Result<CommandResult> {
103 Ok(CommandResult::Quit)
104 }
105}
106
107struct ModelCommand {
110 available_models: Vec<String>,
111}
112
113impl CommandHandler for ModelCommand {
114 fn execute(&self, args: &str) -> anyhow::Result<CommandResult> {
115 let model = args.trim();
116 if model.is_empty() {
117 let list = self.available_models.join(", ");
118 Ok(CommandResult::Info(format!(
119 "Available models: {}\nUsage: /model <name>",
120 list
121 )))
122 } else {
123 if self.available_models.iter().any(|m| m == model) {
125 Ok(CommandResult::ModelChanged(model.to_string()))
126 } else {
127 Ok(CommandResult::Info(format!(
128 "Unknown model: {}. Available: {}",
129 model,
130 self.available_models.join(", ")
131 )))
132 }
133 }
134 }
135
136 fn argument_completions(&self, prefix: &str) -> Vec<AutocompleteItem> {
137 let lower = prefix.to_lowercase();
138 self.available_models
139 .iter()
140 .filter(|m| m.to_lowercase().contains(&lower))
141 .map(|m| AutocompleteItem {
142 value: m.clone(),
143 label: m.clone(),
144 description: None,
145 })
146 .collect()
147 }
148}
149
150struct HotkeysCommand;
153
154impl CommandHandler for HotkeysCommand {
155 fn execute(&self, _args: &str) -> anyhow::Result<CommandResult> {
156 Ok(CommandResult::ShowHelp)
157 }
158}
159
160struct ReloadCommand;
163
164impl CommandHandler for ReloadCommand {
165 fn execute(&self, _args: &str) -> anyhow::Result<CommandResult> {
166 Ok(CommandResult::Reloaded)
167 }
168}
169
170struct NewCommand;
173
174impl CommandHandler for NewCommand {
175 fn execute(&self, _args: &str) -> anyhow::Result<CommandResult> {
176 Ok(CommandResult::NewSession)
177 }
178}
179
180struct ResumeCommand;
183
184impl CommandHandler for ResumeCommand {
185 fn execute(&self, _args: &str) -> anyhow::Result<CommandResult> {
186 Ok(CommandResult::OpenSessionSelector)
187 }
188}
189
190struct SessionInfoCommand {
193 info: Arc<Mutex<Option<SessionInfoInternal>>>,
194}
195
196impl CommandHandler for SessionInfoCommand {
197 fn execute(&self, _args: &str) -> anyhow::Result<CommandResult> {
198 let info = self.info.lock().unwrap();
199 match info.as_ref() {
200 Some(si) => Ok(CommandResult::SessionInfo {
201 session_id: si.session_id.clone(),
202 file_path: si.file_path.clone(),
203 name: si.name.clone(),
204 message_count: si.message_count,
205 }),
206 None => Ok(CommandResult::Info(
207 "No active session (use --no-session?)".to_string(),
208 )),
209 }
210 }
211}
212
213struct NameCommand;
216
217impl CommandHandler for NameCommand {
218 fn execute(&self, args: &str) -> anyhow::Result<CommandResult> {
219 let name = args.trim();
220 if name.is_empty() {
221 return Ok(CommandResult::Info(
222 "Usage: /name <name> — set session display name".to_string(),
223 ));
224 }
225 Ok(CommandResult::SessionNamed {
226 name: name.to_string(),
227 })
228 }
229}