1#[derive(Debug, Clone, Copy)]
12pub struct SlashCommand {
13 pub name: &'static str,
15 pub aliases: &'static [&'static str],
18 pub description: &'static str,
20 pub arg_hint: Option<&'static str>,
23 pub group: SlashCommandGroup,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SlashCommandGroup {
29 Everyday,
30 ModelContext,
31 SafetyRecovery,
32 Integrations,
33 AdvancedRuntime,
34}
35
36impl SlashCommandGroup {
37 pub fn title(self) -> &'static str {
38 match self {
39 SlashCommandGroup::Everyday => "Everyday",
40 SlashCommandGroup::ModelContext => "Model and context",
41 SlashCommandGroup::SafetyRecovery => "Safety and recovery",
42 SlashCommandGroup::Integrations => "Integrations",
43 SlashCommandGroup::AdvancedRuntime => "Advanced runtime",
44 }
45 }
46}
47
48pub const COMMAND_GROUPS: &[SlashCommandGroup] = &[
49 SlashCommandGroup::Everyday,
50 SlashCommandGroup::ModelContext,
51 SlashCommandGroup::SafetyRecovery,
52 SlashCommandGroup::Integrations,
53 SlashCommandGroup::AdvancedRuntime,
54];
55
56pub const KEYBINDINGS: &[(&str, &str)] = &[
60 ("Enter", "Submit the prompt"),
61 ("Ctrl+J", "Insert a newline (multi-line input)"),
62 ("Esc", "Interrupt the current turn"),
63 ("Up / Down", "Browse input history"),
64 ("PageUp / PageDown", "Scroll the transcript"),
65 ("Shift+Up / Shift+Down", "Scroll the transcript one line"),
66 ("End", "Jump to the newest message"),
67 ("Ctrl+V", "Paste (including images)"),
68 ("Ctrl+O", "Compose the prompt in $VISUAL/$EDITOR"),
69 ("Ctrl+B", "Background a running command"),
70 ("Ctrl+T", "Expand or collapse the task checklist"),
71 ("Alt+T", "Cycle reasoning depth"),
72 ("Shift+Tab", "Cycle the safety mode (plan is one of them)"),
73 ("Ctrl+C", "Cancel, then exit"),
74 ("Ctrl+D", "Quit"),
75];
76
77pub const COMMAND_REGISTRY: &[SlashCommand] = &[
80 SlashCommand {
81 name: "model",
82 aliases: &[],
83 description: "Open the model picker, or switch directly with a name",
84 arg_hint: Some("[name]"),
85 group: SlashCommandGroup::ModelContext,
86 },
87 SlashCommand {
88 name: "reasoning",
89 aliases: &[],
90 description: "Set reasoning depth (none, minimal, low, medium, high, max, xhigh)",
91 arg_hint: Some("[level]"),
92 group: SlashCommandGroup::ModelContext,
93 },
94 SlashCommand {
95 name: "visible-reasoning",
96 aliases: &["visiblereasoning"],
97 description: "Show, hide, or toggle reasoning blocks in the transcript",
98 arg_hint: Some("[on|off|toggle]"),
99 group: SlashCommandGroup::ModelContext,
100 },
101 SlashCommand {
102 name: "clear",
103 aliases: &[],
104 description: "Clear chat history",
105 arg_hint: None,
106 group: SlashCommandGroup::Everyday,
107 },
108 SlashCommand {
109 name: "save",
110 aliases: &[],
111 description: "Save current conversation",
112 arg_hint: Some("[name]"),
113 group: SlashCommandGroup::Everyday,
114 },
115 SlashCommand {
116 name: "load",
117 aliases: &[],
118 description: "Load a conversation",
119 arg_hint: Some("[name]"),
120 group: SlashCommandGroup::Everyday,
121 },
122 SlashCommand {
123 name: "list",
124 aliases: &[],
125 description: "List saved conversations",
126 arg_hint: None,
127 group: SlashCommandGroup::Everyday,
128 },
129 SlashCommand {
130 name: "todos",
131 aliases: &["todo"],
132 description: "Show or edit the task checklist",
133 arg_hint: Some("[add <subject>|rm <id>|done <id>|clear]"),
134 group: SlashCommandGroup::Everyday,
135 },
136 SlashCommand {
137 name: "scratchpad",
138 aliases: &[],
139 description: "Show the session scratch directory and its contents",
140 arg_hint: None,
141 group: SlashCommandGroup::Everyday,
142 },
143 SlashCommand {
144 name: "usage",
145 aliases: &[],
146 description: "Show provider token usage and session totals",
147 arg_hint: None,
148 group: SlashCommandGroup::ModelContext,
149 },
150 SlashCommand {
151 name: "context",
152 aliases: &[],
153 description: "Show context window/budget; set Ollama num_ctx (Ollama auto-fits to VRAM)",
154 arg_hint: Some("[n|auto|max|offload on|off]"),
155 group: SlashCommandGroup::ModelContext,
156 },
157 SlashCommand {
158 name: "compact",
159 aliases: &["compress", "summarize"],
160 description: "Compact conversation context with optional focus instructions",
161 arg_hint: Some("[instructions]"),
162 group: SlashCommandGroup::ModelContext,
163 },
164 SlashCommand {
165 name: "memory",
166 aliases: &["memories"],
167 description: "List durable memories saved across sessions",
168 arg_hint: None,
169 group: SlashCommandGroup::ModelContext,
170 },
171 SlashCommand {
172 name: "remember",
173 aliases: &[],
174 description: "Save a fact to private durable memory",
175 arg_hint: Some("<fact>"),
176 group: SlashCommandGroup::ModelContext,
177 },
178 SlashCommand {
179 name: "forget",
180 aliases: &[],
181 description: "Delete a saved memory by name",
182 arg_hint: Some("<name>"),
183 group: SlashCommandGroup::ModelContext,
184 },
185 SlashCommand {
186 name: "consolidate-memory",
187 aliases: &["memory-consolidate", "prune-memory"],
188 description: "Prune duplicate or obsolete memories (model-assisted, reversible)",
189 arg_hint: None,
190 group: SlashCommandGroup::ModelContext,
191 },
192 SlashCommand {
193 name: "doctor",
194 aliases: &[],
195 description: "Show in-session readiness, model, safety, and instruction status",
196 arg_hint: None,
197 group: SlashCommandGroup::Everyday,
198 },
199 SlashCommand {
200 name: "tasks",
201 aliases: &[],
202 description: "List durable runtime tasks",
203 arg_hint: None,
204 group: SlashCommandGroup::AdvancedRuntime,
205 },
206 SlashCommand {
207 name: "task",
208 aliases: &[],
209 description: "Show a durable runtime task",
210 arg_hint: Some("<id>"),
211 group: SlashCommandGroup::AdvancedRuntime,
212 },
213 SlashCommand {
214 name: "pause",
215 aliases: &[],
216 description: "Pause a durable task by marking it blocked",
217 arg_hint: Some("<task-id>"),
218 group: SlashCommandGroup::AdvancedRuntime,
219 },
220 SlashCommand {
221 name: "resume",
222 aliases: &[],
223 description: "Resume a durable task by marking it running",
224 arg_hint: Some("<task-id>"),
225 group: SlashCommandGroup::AdvancedRuntime,
226 },
227 SlashCommand {
228 name: "cancel",
229 aliases: &[],
230 description: "Cancel the active turn or a durable task",
231 arg_hint: Some("[task-id]"),
232 group: SlashCommandGroup::Everyday,
233 },
234 SlashCommand {
235 name: "handoff",
236 aliases: &[],
237 description: "Write a handoff report for the current or named task",
238 arg_hint: Some("[task-id]"),
239 group: SlashCommandGroup::Everyday,
240 },
241 SlashCommand {
242 name: "report",
243 aliases: &[],
244 description: "Show current context report or task report",
245 arg_hint: Some("[task-id]"),
246 group: SlashCommandGroup::Everyday,
247 },
248 SlashCommand {
249 name: "agents",
250 aliases: &[],
251 description: "List or kill background agents",
252 arg_hint: Some("[kill <id>|all]"),
253 group: SlashCommandGroup::AdvancedRuntime,
254 },
255 SlashCommand {
256 name: "processes",
257 aliases: &["procs"],
258 description: "List durable runtime processes",
259 arg_hint: None,
260 group: SlashCommandGroup::AdvancedRuntime,
261 },
262 SlashCommand {
263 name: "logs",
264 aliases: &[],
265 description: "Show a durable runtime process log",
266 arg_hint: Some("<process-id>"),
267 group: SlashCommandGroup::AdvancedRuntime,
268 },
269 SlashCommand {
270 name: "stop",
271 aliases: &[],
272 description: "Stop a durable runtime process",
273 arg_hint: Some("<process-id>"),
274 group: SlashCommandGroup::AdvancedRuntime,
275 },
276 SlashCommand {
277 name: "restart",
278 aliases: &[],
279 description: "Restart a durable runtime process",
280 arg_hint: Some("<process-id>"),
281 group: SlashCommandGroup::AdvancedRuntime,
282 },
283 SlashCommand {
284 name: "open",
285 aliases: &[],
286 description: "Open a URL, file, or process target",
287 arg_hint: Some("<url|path|process-id>"),
288 group: SlashCommandGroup::AdvancedRuntime,
289 },
290 SlashCommand {
291 name: "ports",
292 aliases: &[],
293 description: "Show listening TCP ports",
294 arg_hint: None,
295 group: SlashCommandGroup::AdvancedRuntime,
296 },
297 SlashCommand {
298 name: "safety",
299 aliases: &["permission"],
300 description: "Show or set the session safety mode (Shift+Tab also cycles)",
301 arg_hint: Some("[plan|read_only|ask|auto|full_access]"),
302 group: SlashCommandGroup::SafetyRecovery,
303 },
304 SlashCommand {
305 name: "plan",
306 aliases: &[],
307 description: "Enter or leave plan mode (Shift+Tab cycles into it too)",
308 arg_hint: Some("[off|show|config]"),
309 group: SlashCommandGroup::SafetyRecovery,
310 },
311 SlashCommand {
312 name: "config",
313 aliases: &[],
314 description: "Open the settings picker (plan mode section)",
315 arg_hint: None,
316 group: SlashCommandGroup::Everyday,
317 },
318 SlashCommand {
319 name: "approvals",
320 aliases: &[],
321 description: "List pending approvals",
322 arg_hint: None,
323 group: SlashCommandGroup::SafetyRecovery,
324 },
325 SlashCommand {
326 name: "approve",
327 aliases: &[],
328 description: "Approve a pending action",
329 arg_hint: Some("<approval-id>"),
330 group: SlashCommandGroup::SafetyRecovery,
331 },
332 SlashCommand {
333 name: "deny",
334 aliases: &[],
335 description: "Deny a pending action",
336 arg_hint: Some("<approval-id>"),
337 group: SlashCommandGroup::SafetyRecovery,
338 },
339 SlashCommand {
340 name: "checkpoint",
341 aliases: &[],
342 description: "Create a restore checkpoint for one or more paths",
343 arg_hint: Some("<path...>"),
344 group: SlashCommandGroup::SafetyRecovery,
345 },
346 SlashCommand {
347 name: "checkpoints",
348 aliases: &[],
349 description: "List restore checkpoints",
350 arg_hint: None,
351 group: SlashCommandGroup::SafetyRecovery,
352 },
353 SlashCommand {
354 name: "restore",
355 aliases: &[],
356 description: "Restore a checkpoint",
357 arg_hint: Some("<id>"),
358 group: SlashCommandGroup::SafetyRecovery,
359 },
360 SlashCommand {
361 name: "plugins",
362 aliases: &[],
363 description: "List installed plugins",
364 arg_hint: None,
365 group: SlashCommandGroup::Integrations,
366 },
367 SlashCommand {
368 name: "model-info",
369 aliases: &[],
370 description: "Show provider/model capability information",
371 arg_hint: Some("<model>"),
372 group: SlashCommandGroup::ModelContext,
373 },
374 SlashCommand {
375 name: "cloud-setup",
376 aliases: &[],
377 description: "Configure Ollama Cloud API key",
378 arg_hint: None,
379 group: SlashCommandGroup::Integrations,
380 },
381 SlashCommand {
382 name: "theme",
383 aliases: &[],
384 description: "Switch the color theme or show the current one",
385 arg_hint: Some("[dark|light]"),
386 group: SlashCommandGroup::Everyday,
387 },
388 SlashCommand {
389 name: "editor",
390 aliases: &[],
391 description: "Compose the prompt in $VISUAL/$EDITOR (Ctrl+O)",
392 arg_hint: None,
393 group: SlashCommandGroup::Everyday,
394 },
395 SlashCommand {
396 name: "help",
397 aliases: &["h"],
398 description: "Show command help",
399 arg_hint: None,
400 group: SlashCommandGroup::Everyday,
401 },
402 SlashCommand {
403 name: "quit",
404 aliases: &["q"],
405 description: "Quit the application",
406 arg_hint: None,
407 group: SlashCommandGroup::Everyday,
408 },
409];
410
411pub enum PaletteEntry<'a> {
417 Builtin(&'static SlashCommand),
418 Plugin(&'a crate::domain::PluginCommand),
419}
420
421impl PaletteEntry<'_> {
422 pub fn name(&self) -> &str {
423 match self {
424 PaletteEntry::Builtin(c) => c.name,
425 PaletteEntry::Plugin(p) => &p.name,
426 }
427 }
428
429 pub fn description(&self) -> String {
431 match self {
432 PaletteEntry::Builtin(c) => c.description.to_string(),
433 PaletteEntry::Plugin(p) => {
434 if p.description.is_empty() {
435 format!("(plugin:{})", p.plugin)
436 } else {
437 format!("{} (plugin:{})", p.description, p.plugin)
438 }
439 },
440 }
441 }
442
443 pub fn arg_hint(&self) -> Option<&'static str> {
444 match self {
445 PaletteEntry::Builtin(c) => c.arg_hint,
446 PaletteEntry::Plugin(_) => Some("[args]"),
447 }
448 }
449}
450
451pub fn filter_entries<'a>(
456 typed: &str,
457 plugin: &'a [crate::domain::PluginCommand],
458) -> Vec<PaletteEntry<'a>> {
459 let needle = typed.to_lowercase();
460 let mut entries: Vec<PaletteEntry<'a>> = filter_by_prefix(typed)
461 .into_iter()
462 .map(PaletteEntry::Builtin)
463 .collect();
464 entries.extend(
465 plugin
466 .iter()
467 .filter(|p| needle.is_empty() || p.name.starts_with(&needle))
468 .map(PaletteEntry::Plugin),
469 );
470 entries
471}
472
473pub fn filter_by_prefix(typed: &str) -> Vec<&'static SlashCommand> {
478 let needle = typed.to_lowercase();
479 if needle.is_empty() {
480 return COMMAND_REGISTRY.iter().collect();
481 }
482 COMMAND_REGISTRY
488 .iter()
489 .filter(|cmd| {
490 cmd.name.starts_with(&needle) || cmd.aliases.iter().any(|a| a.starts_with(&needle))
491 })
492 .collect()
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn filter_by_prefix_empty_returns_all() {
501 let result = filter_by_prefix("");
502 assert_eq!(result.len(), COMMAND_REGISTRY.len());
503 assert_eq!(result[0].name, COMMAND_REGISTRY[0].name);
505 }
506
507 #[test]
508 fn filter_by_prefix_includes_exact_name_first() {
509 let result = filter_by_prefix("model");
512 assert!(result.iter().any(|c| c.name == "model"));
513 assert_eq!(result[0].name, "model");
514 }
515
516 #[test]
517 fn filter_by_prefix_partial_prefix_includes_model() {
518 let result = filter_by_prefix("mod");
519 assert!(result.iter().any(|c| c.name == "model"));
520 }
521
522 #[test]
523 fn filter_by_prefix_matches_hyphenated_command_by_first_word() {
524 assert!(
528 filter_by_prefix("consolidate")
529 .iter()
530 .any(|c| c.name == "consolidate-memory"),
531 "/consolidate must surface /consolidate-memory"
532 );
533 assert!(
534 filter_by_prefix("conso")
535 .iter()
536 .any(|c| c.name == "consolidate-memory")
537 );
538 assert!(
540 filter_by_prefix("cloud")
541 .iter()
542 .any(|c| c.name == "cloud-setup")
543 );
544 }
545
546 #[test]
547 fn filter_by_prefix_no_match_returns_empty() {
548 let result = filter_by_prefix("zzzzz");
549 assert!(result.is_empty());
550 }
551
552 #[test]
553 fn filter_by_prefix_matches_aliases() {
554 let result = filter_by_prefix("q");
556 assert!(
557 result.iter().any(|c| c.name == "quit"),
558 "expected quit in: {:?}",
559 result.iter().map(|c| c.name).collect::<Vec<_>>()
560 );
561 }
562
563 #[test]
564 fn filter_by_prefix_is_case_insensitive() {
565 let upper = filter_by_prefix("MODEL");
567 assert!(upper.iter().any(|c| c.name == "model"));
568 assert_eq!(upper[0].name, "model");
569 }
570
571 #[test]
572 fn registry_has_no_duplicate_names() {
573 let mut names: Vec<&str> = COMMAND_REGISTRY.iter().map(|c| c.name).collect();
577 names.sort_unstable();
578 let len_before = names.len();
579 names.dedup();
580 assert_eq!(
581 names.len(),
582 len_before,
583 "duplicate command name detected in COMMAND_REGISTRY"
584 );
585 }
586}