opendev_repl/commands/
builtin.rs1use opendev_config::ModelRegistry;
6use opendev_runtime::AutonomyLevel;
7
8use crate::repl::{OperationMode, ReplState};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum CommandOutcome {
13 Handled,
15 Exit,
17 Unknown,
19}
20
21pub struct BuiltinCommands;
23
24impl BuiltinCommands {
25 pub fn new() -> Self {
27 Self
28 }
29
30 pub fn dispatch(&self, cmd: &str, args: &str, state: &mut ReplState) -> CommandOutcome {
34 match cmd {
35 "/help" => {
36 self.handle_help(args);
37 CommandOutcome::Handled
38 }
39 "/exit" | "/quit" => CommandOutcome::Exit,
40 "/clear" => {
41 self.handle_clear(state);
42 CommandOutcome::Handled
43 }
44 "/mode" => {
45 self.handle_mode(args, state);
46 CommandOutcome::Handled
47 }
48 "/compact" => {
49 self.handle_compact(state);
50 CommandOutcome::Handled
51 }
52 "/models" => {
53 self.handle_models();
54 CommandOutcome::Handled
55 }
56 "/mcp" => {
57 self.handle_mcp(args);
58 CommandOutcome::Handled
59 }
60 "/agents" => {
61 self.handle_agents(args);
62 CommandOutcome::Handled
63 }
64 "/skills" => {
65 self.handle_skills(args);
66 CommandOutcome::Handled
67 }
68 "/plugins" => {
69 self.handle_plugins(args);
70 CommandOutcome::Handled
71 }
72 "/session-models" => {
73 self.handle_session_models(args);
74 CommandOutcome::Handled
75 }
76 "/autonomy" => {
77 self.handle_autonomy(args, state);
78 CommandOutcome::Handled
79 }
80 "/status" => {
81 self.handle_status(state);
82 CommandOutcome::Handled
83 }
84 "/init" => {
85 self.handle_init(args, state);
86 CommandOutcome::Handled
87 }
88 "/sound" => {
89 self.handle_sound();
90 CommandOutcome::Handled
91 }
92 _ => CommandOutcome::Unknown,
93 }
94 }
95
96 fn handle_help(&self, _args: &str) {
97 println!("Available commands:");
98 println!(" /help Show this help message");
99 println!(" /exit, /quit Exit the REPL");
100 println!(" /clear Clear conversation history");
101 println!(" /mode [plan|normal] Switch operation mode");
102 println!(" /autonomy [manual|semi-auto|auto] Set approval level");
103 println!(" /status Show current status");
104 println!(" /compact Compact conversation context");
105 println!(" /models Show model picker (from models.dev registry)");
106 println!(" /mcp <subcommand> Manage MCP servers");
107 println!(" /agents <args> Manage agents");
108 println!(" /skills <args> Manage skills");
109 println!(" /plugins <args> Manage plugins");
110 println!(" /session-models Session model management");
111 println!(" /sound Play test notification sound");
112 println!(" /init Initialize codebase context");
113 }
114
115 fn handle_clear(&self, state: &mut ReplState) {
116 state.messages_cleared = true;
117 println!("Conversation cleared.");
118 }
119
120 fn handle_mode(&self, args: &str, state: &mut ReplState) {
121 let target = args.trim().to_lowercase();
122 match target.as_str() {
123 "plan" => {
124 state.mode = OperationMode::Plan;
125 println!("Switched to Plan mode (read-only tools).");
126 }
127 "normal" | "" => {
128 state.mode = OperationMode::Normal;
129 println!("Switched to Normal mode (full tool access).");
130 }
131 _ => {
132 println!("Usage: /mode [plan|normal]");
133 }
134 }
135 }
136
137 fn handle_compact(&self, state: &mut ReplState) {
138 state.compact_requested = true;
139 println!("Context compaction triggered.");
140 }
141
142 fn handle_models(&self) {
143 self.handle_model_picker(None);
144 }
145
146 pub fn handle_model_picker(
152 &self,
153 cache_dir: Option<&std::path::Path>,
154 ) -> Vec<(String, String)> {
155 let cache = cache_dir.map(std::path::PathBuf::from).unwrap_or_else(|| {
156 dirs::home_dir()
157 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
158 .join(".opendev")
159 .join("cache")
160 });
161
162 let registry = ModelRegistry::load_from_cache(&cache);
163
164 if registry.providers.is_empty() {
165 println!("No models available. Run `opendev setup` or check network connectivity.");
166 return Vec::new();
167 }
168
169 let models = registry.list_all_models(None, None);
170
171 if models.is_empty() {
172 println!("No models found in registry.");
173 return Vec::new();
174 }
175
176 println!("Available models:");
177 println!();
178
179 let mut entries: Vec<(String, String)> = Vec::new();
180 for (i, (provider_id, model)) in models.iter().enumerate() {
181 let num = i + 1;
182 let pricing = model.format_pricing();
183 let ctx = if model.context_length >= 1_000_000 {
184 format!("{}M ctx", model.context_length / 1_000_000)
185 } else if model.context_length >= 1_000 {
186 format!("{}K ctx", model.context_length / 1_000)
187 } else {
188 format!("{} ctx", model.context_length)
189 };
190 let caps = if model.capabilities.is_empty() {
191 String::new()
192 } else {
193 format!(" [{}]", model.capabilities.join(", "))
194 };
195
196 println!(
197 " {num:>3}. {name} ({provider}) - {ctx}, {pricing}{caps}",
198 name = model.name,
199 provider = model.provider,
200 );
201
202 entries.push((provider_id.to_string(), model.id.clone()));
203 }
204
205 println!();
206 println!("Use /session-models set model <name> to change the active model.");
207
208 entries
209 }
210
211 fn handle_mcp(&self, args: &str) {
212 let parts: Vec<&str> = args.trim().splitn(2, ' ').collect();
213 let subcommand = parts.first().copied().unwrap_or("");
214 let sub_args = parts.get(1).copied().unwrap_or("");
215
216 match subcommand {
217 "" | "list" => {
218 println!("MCP Servers:");
219 println!(" (none configured)");
220 println!();
221 println!("Use /mcp add <name> <command> to register a server.");
222 }
223 "add" => {
224 if sub_args.is_empty() {
225 println!("Usage: /mcp add <name> <command> [args...]");
226 } else {
227 let name = sub_args.split_whitespace().next().unwrap_or(sub_args);
228 println!(
229 "MCP server '{}' registered (restart required to activate).",
230 name
231 );
232 }
233 }
234 "remove" => {
235 if sub_args.is_empty() {
236 println!("Usage: /mcp remove <name>");
237 } else {
238 println!("MCP server '{}' removed.", sub_args.trim());
239 }
240 }
241 "enable" => {
242 if sub_args.is_empty() {
243 println!("Usage: /mcp enable <name>");
244 } else {
245 println!("MCP server '{}' enabled.", sub_args.trim());
246 }
247 }
248 "disable" => {
249 if sub_args.is_empty() {
250 println!("Usage: /mcp disable <name>");
251 } else {
252 println!("MCP server '{}' disabled.", sub_args.trim());
253 }
254 }
255 _ => {
256 println!("Unknown MCP subcommand: {}", subcommand);
257 println!("Usage: /mcp [list|add|remove|enable|disable] ...");
258 }
259 }
260 }
261
262 fn handle_agents(&self, args: &str) {
263 let subcommand = args.split_whitespace().next().unwrap_or("list");
264 match subcommand {
265 "list" | "" => {
266 println!("Available agents:");
267 println!(" - Explore Explore and understand codebase structure");
268 println!(" - Planner Create and refine implementation plans");
269 println!(" - Ask-User Request clarification from the user");
270 }
271 _ => {
272 println!("Usage: /agents [list]");
273 }
274 }
275 }
276
277 fn handle_skills(&self, args: &str) {
278 let subcommand = args.split_whitespace().next().unwrap_or("list");
279 match subcommand {
280 "list" | "" => {
281 println!("Built-in skills:");
282 println!(" - commit Git commit best practices");
283 println!(" - review-pr Pull request review guidelines");
284 println!(" - create-pr Pull request creation workflow");
285 println!();
286 println!("Use /skills to invoke a skill by name.");
287 }
288 _ => {
289 println!("Usage: /skills [list]");
290 }
291 }
292 }
293
294 fn handle_plugins(&self, args: &str) {
295 let parts: Vec<&str> = args.trim().splitn(2, ' ').collect();
296 let subcommand = parts.first().copied().unwrap_or("");
297 let sub_args = parts.get(1).copied().unwrap_or("");
298
299 match subcommand {
300 "" | "list" => {
301 println!("Plugins: (none installed)");
302 println!("Use /plugins install <name> to add plugins.");
303 }
304 "install" => {
305 if sub_args.is_empty() {
306 println!("Usage: /plugins install <name>");
307 } else {
308 println!("Installing plugin '{}'...", sub_args.trim());
309 println!("Plugin installation not yet connected to marketplace.");
310 }
311 }
312 "remove" => {
313 if sub_args.is_empty() {
314 println!("Usage: /plugins remove <name>");
315 } else {
316 println!(
317 "Plugin '{}' not found in installed plugins.",
318 sub_args.trim()
319 );
320 }
321 }
322 _ => {
323 println!("Unknown plugins subcommand: {}", subcommand);
324 println!("Usage: /plugins [list|install|remove] ...");
325 }
326 }
327 }
328
329 fn handle_session_models(&self, args: &str) {
330 let parts: Vec<&str> = args.trim().splitn(2, ' ').collect();
331 let subcommand = parts.first().copied().unwrap_or("");
332 let sub_args = parts.get(1).copied().unwrap_or("");
333
334 match subcommand {
335 "" | "show" => {
336 println!("No session model overrides set.");
337 println!();
338 println!("Available slots: model, model_vlm");
339 println!(
340 "Use /session-models set <slot> <value> to override a model for this session."
341 );
342 }
343 "set" => {
344 let set_parts: Vec<&str> = sub_args.splitn(2, ' ').collect();
345 if set_parts.len() < 2 {
346 println!("Usage: /session-models set <slot> <model-name>");
347 } else {
348 let slot = set_parts[0];
349 let value = set_parts[1];
350 let valid_slots =
351 ["model", "model_provider", "model_vlm", "model_vlm_provider"];
352 if valid_slots.contains(&slot) {
353 println!("Session override: {} = {}", slot, value);
354 } else {
355 println!("Unknown slot: {}", slot);
356 println!("Valid slots: {}", valid_slots.join(", "));
357 }
358 }
359 }
360 "clear" => {
361 println!("Session model overrides cleared.");
362 }
363 _ => {
364 println!("Unknown session-models subcommand: {}", subcommand);
365 println!("Usage: /session-models [show|set|clear] ...");
366 }
367 }
368 }
369
370 fn handle_autonomy(&self, args: &str, state: &mut ReplState) {
371 let target = args.trim();
372 if target.is_empty() {
373 println!("Autonomy level: {}", state.autonomy_level);
374 println!("Usage: /autonomy [manual|semi-auto|auto]");
375 return;
376 }
377 match AutonomyLevel::from_str_loose(target) {
378 Some(level) => {
379 state.autonomy_level = level;
380 let detail = match level {
381 AutonomyLevel::Manual => "(all commands require approval)",
382 AutonomyLevel::SemiAuto => "(safe commands auto-approved)",
383 AutonomyLevel::Auto => "(all commands auto-approved)",
384 };
385 println!("Autonomy level set to: {} {}", level, detail);
386 }
387 None => {
388 println!("Invalid autonomy level: {}", target);
389 println!("Valid levels: manual, semi-auto, auto");
390 }
391 }
392 }
393
394 fn handle_status(&self, state: &ReplState) {
395 println!("Current status:");
396 println!(" Mode: {}", state.mode);
397 println!(" Autonomy: {}", state.autonomy_level);
398 }
399
400 fn handle_sound(&self) {
401 opendev_runtime::play_finish_sound();
402 println!("Playing test sound...");
403 }
404
405 fn handle_init(&self, args: &str, state: &mut ReplState) {
406 let prompt = opendev_agents::prompts::embedded::build_init_prompt(args);
407 state.init_prompt = Some(prompt);
408 println!("Generating AGENTS.md...");
409 }
410}
411
412impl Default for BuiltinCommands {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418#[cfg(test)]
419#[path = "builtin_tests.rs"]
420mod tests;