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