1use runtime::{compact_session, CompactionConfig, Session};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct CommandManifestEntry {
5 pub name: String,
6 pub source: CommandSource,
7}
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum CommandSource {
11 Builtin,
12 InternalOnly,
13 FeatureGated,
14}
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct CommandRegistry {
18 entries: Vec<CommandManifestEntry>,
19}
20
21impl CommandRegistry {
22 #[must_use]
23 pub fn new(entries: Vec<CommandManifestEntry>) -> Self {
24 Self { entries }
25 }
26
27 #[must_use]
28 pub fn entries(&self) -> &[CommandManifestEntry] {
29 &self.entries
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct SlashCommandSpec {
35 pub name: &'static str,
36 pub summary: &'static str,
37 pub argument_hint: Option<&'static str>,
38 pub resume_supported: bool,
39}
40
41const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
42 SlashCommandSpec {
43 name: "help",
44 summary: "Show available slash commands",
45 argument_hint: None,
46 resume_supported: true,
47 },
48 SlashCommandSpec {
49 name: "status",
50 summary: "Show current session status",
51 argument_hint: None,
52 resume_supported: true,
53 },
54 SlashCommandSpec {
55 name: "compact",
56 summary: "Compact local session history",
57 argument_hint: None,
58 resume_supported: true,
59 },
60 SlashCommandSpec {
61 name: "compress",
62 summary: "Aggressively compress session history into a summary",
63 argument_hint: None,
64 resume_supported: true,
65 },
66 SlashCommandSpec {
67 name: "model",
68 summary: "Show or switch the active model",
69 argument_hint: Some("[model]"),
70 resume_supported: false,
71 },
72 SlashCommandSpec {
73 name: "permissions",
74 summary: "Show or switch the active permission mode",
75 argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
76 resume_supported: false,
77 },
78 SlashCommandSpec {
79 name: "clear",
80 summary: "Start a fresh local session",
81 argument_hint: Some("[--confirm]"),
82 resume_supported: true,
83 },
84 SlashCommandSpec {
85 name: "cost",
86 summary: "Show cumulative token usage for this session",
87 argument_hint: None,
88 resume_supported: true,
89 },
90 SlashCommandSpec {
91 name: "resume",
92 summary: "Load a saved session into the REPL",
93 argument_hint: Some("<session-path>"),
94 resume_supported: false,
95 },
96 SlashCommandSpec {
97 name: "config",
98 summary: "Inspect Ternlang config files or merged sections",
99 argument_hint: Some("[env|hooks|model]"),
100 resume_supported: true,
101 },
102 SlashCommandSpec {
103 name: "memory",
104 summary: "Inspect loaded Ternlang instruction memory files",
105 argument_hint: None,
106 resume_supported: true,
107 },
108 SlashCommandSpec {
109 name: "init",
110 summary: "Create a starter ALBERT.md for this repo",
111 argument_hint: None,
112 resume_supported: true,
113 },
114 SlashCommandSpec {
115 name: "diff",
116 summary: "Show git diff for current workspace changes",
117 argument_hint: None,
118 resume_supported: true,
119 },
120 SlashCommandSpec {
121 name: "version",
122 summary: "Show CLI version and build information",
123 argument_hint: None,
124 resume_supported: true,
125 },
126 SlashCommandSpec {
127 name: "bughunter",
128 summary: "Inspect the codebase for likely bugs",
129 argument_hint: Some("[scope]"),
130 resume_supported: false,
131 },
132 SlashCommandSpec {
133 name: "commit",
134 summary: "Generate a commit message and create a git commit",
135 argument_hint: None,
136 resume_supported: false,
137 },
138 SlashCommandSpec {
139 name: "pr",
140 summary: "Draft or create a pull request from the conversation",
141 argument_hint: Some("[context]"),
142 resume_supported: false,
143 },
144 SlashCommandSpec {
145 name: "issue",
146 summary: "Draft or create a GitHub issue from the conversation",
147 argument_hint: Some("[context]"),
148 resume_supported: false,
149 },
150 SlashCommandSpec {
151 name: "ultraplan",
152 summary: "Run a deep planning prompt with multi-step reasoning",
153 argument_hint: Some("[task]"),
154 resume_supported: false,
155 },
156 SlashCommandSpec {
157 name: "teleport",
158 summary: "Jump to a file or symbol by searching the workspace",
159 argument_hint: Some("<symbol-or-path>"),
160 resume_supported: false,
161 },
162 SlashCommandSpec {
163 name: "debug-tool-call",
164 summary: "Replay the last tool call with debug details",
165 argument_hint: None,
166 resume_supported: false,
167 },
168 SlashCommandSpec {
169 name: "export",
170 summary: "Export the current conversation to a file",
171 argument_hint: Some("[file]"),
172 resume_supported: true,
173 },
174 SlashCommandSpec {
175 name: "session",
176 summary: "List or switch managed local sessions",
177 argument_hint: Some("[list|switch <session-id>]"),
178 resume_supported: false,
179 },
180 SlashCommandSpec {
181 name: "auth",
182 summary: "Configure LLM provider and API keys",
183 argument_hint: Some("[provider]"),
184 resume_supported: false,
185 },
186 SlashCommandSpec {
187 name: "plan",
188 summary: "Restate requirements and assess risks before implementation",
189 argument_hint: Some("[task]"),
190 resume_supported: false,
191 },
192 SlashCommandSpec {
193 name: "tdd",
194 summary: "Enforce test-driven development workflow",
195 argument_hint: Some("[interface]"),
196 resume_supported: false,
197 },
198 SlashCommandSpec {
199 name: "verify",
200 summary: "Run full verification: build, lint, test, and type-check",
201 argument_hint: None,
202 resume_supported: false,
203 },
204 SlashCommandSpec {
205 name: "code-review",
206 summary: "Full quality, security, and maintainability review",
207 argument_hint: Some("[files]"),
208 resume_supported: false,
209 },
210 SlashCommandSpec {
211 name: "build-fix",
212 summary: "Automatically detect and fix build errors",
213 argument_hint: None,
214 resume_supported: false,
215 },
216 SlashCommandSpec {
217 name: "aside",
218 summary: "Ask a quick side question without losing context",
219 argument_hint: Some("<question>"),
220 resume_supported: false,
221 },
222 SlashCommandSpec {
223 name: "learn",
224 summary: "Extract reusable patterns from the current session",
225 argument_hint: None,
226 resume_supported: false,
227 },
228 SlashCommandSpec {
229 name: "refactor",
230 summary: "Remove dead code and consolidate structure",
231 argument_hint: Some("[scope]"),
232 resume_supported: false,
233 },
234 SlashCommandSpec {
235 name: "checkpoint",
236 summary: "Mark a checkpoint in the current session",
237 argument_hint: Some("[label]"),
238 resume_supported: false,
239 },
240 SlashCommandSpec {
241 name: "docs",
242 summary: "Look up library or API documentation",
243 argument_hint: Some("<query>"),
244 resume_supported: false,
245 },
246 SlashCommandSpec {
247 name: "loop",
248 summary: "Engage autopilot loop to complete a mission",
249 argument_hint: Some("<mission>"),
250 resume_supported: false,
251 },
252 SlashCommandSpec {
253 name: "mcp",
254 summary: "Manage MCP servers (list / add / remove)",
255 argument_hint: Some("[list|add <name> <cmd>|remove <name>]"),
256 resume_supported: false,
257 },
258];
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum SlashCommand {
262 Help,
263 Status,
264 Compact,
265 Compress,
266 Bughunter {
267 scope: Option<String>,
268 },
269 Commit,
270 Pr {
271 context: Option<String>,
272 },
273 Issue {
274 context: Option<String>,
275 },
276 Ultraplan {
277 task: Option<String>,
278 },
279 Teleport {
280 target: Option<String>,
281 },
282 DebugToolCall,
283 Model {
284 model: Option<String>,
285 },
286 Permissions {
287 mode: Option<String>,
288 },
289 Clear {
290 confirm: bool,
291 },
292 Cost,
293 Resume {
294 session_path: Option<String>,
295 },
296 Config {
297 section: Option<String>,
298 },
299 Memory,
300 Init,
301 Diff,
302 Version,
303 Export {
304 path: Option<String>,
305 },
306 Session {
307 action: Option<String>,
308 target: Option<String>,
309 },
310 Auth {
311 provider: Option<String>,
312 },
313 Plan {
314 task: Option<String>,
315 },
316 Tdd {
317 interface: Option<String>,
318 },
319 Verify,
320 CodeReview {
321 files: Option<String>,
322 },
323 BuildFix,
324 Aside {
325 question: Option<String>,
326 },
327 Learn,
328 Refactor {
329 scope: Option<String>,
330 },
331 Checkpoint {
332 label: Option<String>,
333 },
334 Docs {
335 query: Option<String>,
336 },
337 Loop {
338 mission: Option<String>,
339 },
340 Mcp {
341 action: Option<String>,
342 args: Option<String>,
343 },
344 Unknown(String),
345}
346
347impl SlashCommand {
348 #[must_use]
349 pub fn parse(input: &str) -> Option<Self> {
350 let trimmed = input.trim();
351 if !trimmed.starts_with('/') {
352 return None;
353 }
354
355 let mut parts = trimmed.trim_start_matches('/').split_whitespace();
356 let command = parts.next().unwrap_or_default();
357 Some(match command {
358 "help" => Self::Help,
359 "status" => Self::Status,
360 "compact" => Self::Compact,
361 "compress" => Self::Compress,
362 "bughunter" => Self::Bughunter {
363 scope: remainder_after_command(trimmed, command),
364 },
365 "commit" => Self::Commit,
366 "pr" => Self::Pr {
367 context: remainder_after_command(trimmed, command),
368 },
369 "issue" => Self::Issue {
370 context: remainder_after_command(trimmed, command),
371 },
372 "ultraplan" => Self::Ultraplan {
373 task: remainder_after_command(trimmed, command),
374 },
375 "teleport" => Self::Teleport {
376 target: remainder_after_command(trimmed, command),
377 },
378 "debug-tool-call" => Self::DebugToolCall,
379 "model" => Self::Model {
380 model: parts.next().map(ToOwned::to_owned),
381 },
382 "permissions" => Self::Permissions {
383 mode: parts.next().map(ToOwned::to_owned),
384 },
385 "clear" => Self::Clear {
386 confirm: parts.next() == Some("--confirm"),
387 },
388 "cost" => Self::Cost,
389 "resume" => Self::Resume {
390 session_path: parts.next().map(ToOwned::to_owned),
391 },
392 "config" => Self::Config {
393 section: parts.next().map(ToOwned::to_owned),
394 },
395 "memory" => Self::Memory,
396 "init" => Self::Init,
397 "diff" => Self::Diff,
398 "version" => Self::Version,
399 "export" => Self::Export {
400 path: parts.next().map(ToOwned::to_owned),
401 },
402 "session" => Self::Session {
403 action: parts.next().map(ToOwned::to_owned),
404 target: parts.next().map(ToOwned::to_owned),
405 },
406 "auth" => Self::Auth {
407 provider: parts.next().map(ToOwned::to_owned),
408 },
409 "plan" => Self::Plan {
410 task: remainder_after_command(trimmed, command),
411 },
412 "tdd" => Self::Tdd {
413 interface: remainder_after_command(trimmed, command),
414 },
415 "verify" => Self::Verify,
416 "code-review" => Self::CodeReview {
417 files: remainder_after_command(trimmed, command),
418 },
419 "build-fix" => Self::BuildFix,
420 "aside" => Self::Aside {
421 question: remainder_after_command(trimmed, command),
422 },
423 "learn" => Self::Learn,
424 "refactor" => Self::Refactor {
425 scope: remainder_after_command(trimmed, command),
426 },
427 "checkpoint" => Self::Checkpoint {
428 label: remainder_after_command(trimmed, command),
429 },
430 "docs" => Self::Docs {
431 query: remainder_after_command(trimmed, command),
432 },
433 "loop" => Self::Loop {
434 mission: remainder_after_command(trimmed, command),
435 },
436 "mcp" => {
437 let rest = remainder_after_command(trimmed, command);
438 let (action, args) = rest.as_deref().map_or((None, None), |s| {
439 let mut iter = s.splitn(2, ' ');
440 let a = iter.next().map(ToOwned::to_owned);
441 let b = iter.next().map(str::trim).filter(|v| !v.is_empty()).map(ToOwned::to_owned);
442 (a, b)
443 });
444 Self::Mcp { action, args }
445 },
446 other => Self::Unknown(other.to_string()),
447 })
448 }
449}
450
451fn remainder_after_command(input: &str, command: &str) -> Option<String> {
452 input
453 .trim()
454 .strip_prefix(&format!("/{command}"))
455 .map(str::trim)
456 .filter(|value| !value.is_empty())
457 .map(ToOwned::to_owned)
458}
459
460#[must_use]
461pub fn slash_command_specs() -> &'static [SlashCommandSpec] {
462 SLASH_COMMAND_SPECS
463}
464
465#[must_use]
466pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
467 slash_command_specs()
468 .iter()
469 .filter(|spec| spec.resume_supported)
470 .collect()
471}
472
473#[must_use]
474pub fn render_slash_command_help() -> String {
475 use console::style;
476
477 let specs = slash_command_specs();
478 let mut output = String::new();
479
480 output.push_str(&format!("\n{}\n", style("SLASH COMMAND LIBRARY").bold().underlined()));
481 output.push_str(&format!(" {}\n", style("[resume] works with --resume SESSION.json").dim()));
482
483 let categories = vec![
484 ("SESSION & CONTEXT", vec!["status", "clear", "resume", "session", "export", "compact", "compress", "cost", "memory", "aside", "checkpoint", "learn"]),
485 ("DEVELOPMENT & REASONING", vec!["ultraplan", "plan", "loop", "tdd", "verify", "code-review", "build-fix", "refactor", "docs", "bughunter", "init", "teleport", "diff", "commit", "pr", "issue", "debug-tool-call"]),
486 ("CONFIGURATION & AUTH", vec!["model", "permissions", "auth", "config"]),
487 ("UTILITY", vec!["help", "version"]),
488 ];
489
490 for (cat_name, cat_cmds) in categories {
491 output.push_str(&format!("\n{}\n", style(cat_name).cyan().bold()));
492 for cmd_name in cat_cmds {
493 if let Some(spec) = specs.iter().find(|s| s.name == cmd_name) {
494 let name_display = match spec.argument_hint {
495 Some(hint) => format!("/{} {}", spec.name, hint),
496 None => format!("/{}", spec.name),
497 };
498 let resume = if spec.resume_supported {
499 style(" [resume]").dim().to_string()
500 } else {
501 "".to_string()
502 };
503 output.push_str(&format!(" {:<25} {}{}\n", style(name_display).green(), spec.summary, resume));
504 }
505 }
506 }
507
508 output
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct SlashCommandResult {
513 pub message: String,
514 pub session: Session,
515}
516
517#[must_use]
518pub fn handle_slash_command(
519 input: &str,
520 session: &Session,
521 compaction: CompactionConfig,
522) -> Option<SlashCommandResult> {
523 match SlashCommand::parse(input)? {
524 SlashCommand::Compact => {
525 let result = compact_session(session, compaction);
526 let message = if result.removed_message_count == 0 {
527 "Compaction skipped: session is below the compaction threshold.".to_string()
528 } else {
529 format!(
530 "Compacted {} messages into a resumable system summary.",
531 result.removed_message_count
532 )
533 };
534 Some(SlashCommandResult {
535 message,
536 session: result.compacted_session,
537 })
538 }
539 SlashCommand::Compress => {
540 let result = compact_session(session, CompactionConfig {
542 preserve_recent_messages: 2,
543 max_estimated_tokens: 1, });
545 let message = if result.removed_message_count == 0 {
546 "Compression skipped: session is empty or too short.".to_string()
547 } else {
548 format!(
549 "Aggressively compressed {} messages. Albert's memory is now lean and sharp.",
550 result.removed_message_count
551 )
552 };
553 Some(SlashCommandResult {
554 message,
555 session: result.compacted_session,
556 })
557 }
558 SlashCommand::Help => Some(SlashCommandResult {
559 message: render_slash_command_help(),
560 session: session.clone(),
561 }),
562 SlashCommand::Auth { .. }
563 | SlashCommand::Status
564 | SlashCommand::Bughunter { .. }
565 | SlashCommand::Commit
566 | SlashCommand::Pr { .. }
567 | SlashCommand::Issue { .. }
568 | SlashCommand::Ultraplan { .. }
569 | SlashCommand::Teleport { .. }
570 | SlashCommand::DebugToolCall
571 | SlashCommand::Model { .. }
572 | SlashCommand::Permissions { .. }
573 | SlashCommand::Clear { .. }
574 | SlashCommand::Cost
575 | SlashCommand::Resume { .. }
576 | SlashCommand::Config { .. }
577 | SlashCommand::Memory
578 | SlashCommand::Init
579 | SlashCommand::Diff
580 | SlashCommand::Version
581 | SlashCommand::Export { .. }
582 | SlashCommand::Session { .. }
583 | SlashCommand::Plan { .. }
584 | SlashCommand::Tdd { .. }
585 | SlashCommand::Verify
586 | SlashCommand::CodeReview { .. }
587 | SlashCommand::BuildFix
588 | SlashCommand::Aside { .. }
589 | SlashCommand::Learn
590 | SlashCommand::Refactor { .. }
591 | SlashCommand::Checkpoint { .. }
592 | SlashCommand::Docs { .. }
593 | SlashCommand::Loop { .. }
594 | SlashCommand::Mcp { .. }
595 | SlashCommand::Unknown(_) => None,
596 }
597}
598
599
600#[cfg(test)]
601mod tests {
602 use super::{
603 handle_slash_command, render_slash_command_help, resume_supported_slash_commands,
604 slash_command_specs, SlashCommand,
605 };
606 use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
607
608 #[test]
609 fn parses_supported_slash_commands() {
610 assert_eq!(SlashCommand::parse("/help"), Some(SlashCommand::Help));
611 assert_eq!(SlashCommand::parse(" /status "), Some(SlashCommand::Status));
612 assert_eq!(
613 SlashCommand::parse("/bughunter runtime"),
614 Some(SlashCommand::Bughunter {
615 scope: Some("runtime".to_string())
616 })
617 );
618 assert_eq!(SlashCommand::parse("/commit"), Some(SlashCommand::Commit));
619 assert_eq!(
620 SlashCommand::parse("/pr ready for review"),
621 Some(SlashCommand::Pr {
622 context: Some("ready for review".to_string())
623 })
624 );
625 assert_eq!(
626 SlashCommand::parse("/issue flaky test"),
627 Some(SlashCommand::Issue {
628 context: Some("flaky test".to_string())
629 })
630 );
631 assert_eq!(
632 SlashCommand::parse("/ultraplan ship both features"),
633 Some(SlashCommand::Ultraplan {
634 task: Some("ship both features".to_string())
635 })
636 );
637 assert_eq!(
638 SlashCommand::parse("/teleport conversation.rs"),
639 Some(SlashCommand::Teleport {
640 target: Some("conversation.rs".to_string())
641 })
642 );
643 assert_eq!(
644 SlashCommand::parse("/debug-tool-call"),
645 Some(SlashCommand::DebugToolCall)
646 );
647 assert_eq!(
648 SlashCommand::parse("/model ternlang-opus"),
649 Some(SlashCommand::Model {
650 model: Some("ternlang-opus".to_string()),
651 })
652 );
653 assert_eq!(
654 SlashCommand::parse("/model"),
655 Some(SlashCommand::Model { model: None })
656 );
657 assert_eq!(
658 SlashCommand::parse("/permissions read-only"),
659 Some(SlashCommand::Permissions {
660 mode: Some("read-only".to_string()),
661 })
662 );
663 assert_eq!(
664 SlashCommand::parse("/clear"),
665 Some(SlashCommand::Clear { confirm: false })
666 );
667 assert_eq!(
668 SlashCommand::parse("/clear --confirm"),
669 Some(SlashCommand::Clear { confirm: true })
670 );
671 assert_eq!(SlashCommand::parse("/cost"), Some(SlashCommand::Cost));
672 assert_eq!(
673 SlashCommand::parse("/resume session.json"),
674 Some(SlashCommand::Resume {
675 session_path: Some("session.json".to_string()),
676 })
677 );
678 assert_eq!(
679 SlashCommand::parse("/config"),
680 Some(SlashCommand::Config { section: None })
681 );
682 assert_eq!(
683 SlashCommand::parse("/config env"),
684 Some(SlashCommand::Config {
685 section: Some("env".to_string())
686 })
687 );
688 assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
689 assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
690 assert_eq!(SlashCommand::parse("/diff"), Some(SlashCommand::Diff));
691 assert_eq!(SlashCommand::parse("/version"), Some(SlashCommand::Version));
692 assert_eq!(
693 SlashCommand::parse("/export notes.txt"),
694 Some(SlashCommand::Export {
695 path: Some("notes.txt".to_string())
696 })
697 );
698 assert_eq!(
699 SlashCommand::parse("/session switch abc123"),
700 Some(SlashCommand::Session {
701 action: Some("switch".to_string()),
702 target: Some("abc123".to_string())
703 })
704 );
705 }
706
707 #[test]
708 fn renders_help_from_shared_specs() {
709 let help = render_slash_command_help();
710 assert!(help.contains("works with --resume SESSION.json"));
711 assert!(help.contains("/help"));
712 assert!(help.contains("/status"));
713 assert!(help.contains("/compact"));
714 assert!(help.contains("/bughunter [scope]"));
715 assert!(help.contains("/commit"));
716 assert!(help.contains("/pr [context]"));
717 assert!(help.contains("/issue [context]"));
718 assert!(help.contains("/ultraplan [task]"));
719 assert!(help.contains("/teleport <symbol-or-path>"));
720 assert!(help.contains("/debug-tool-call"));
721 assert!(help.contains("/model [model]"));
722 assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
723 assert!(help.contains("/clear [--confirm]"));
724 assert!(help.contains("/cost"));
725 assert!(help.contains("/resume <session-path>"));
726 assert!(help.contains("/config [env|hooks|model]"));
727 assert!(help.contains("/memory"));
728 assert!(help.contains("/init"));
729 assert!(help.contains("/diff"));
730 assert!(help.contains("/version"));
731 assert!(help.contains("/export [file]"));
732 assert!(help.contains("/session [list|switch <session-id>]"));
733 assert_eq!(slash_command_specs().len(), 22);
734 assert_eq!(resume_supported_slash_commands().len(), 11);
735 }
736
737 #[test]
738 fn compacts_sessions_via_slash_command() {
739 let session = Session {
740 version: 1,
741 messages: vec![
742 ConversationMessage::user_text("a ".repeat(200)),
743 ConversationMessage::assistant(vec![ContentBlock::Text {
744 text: "b ".repeat(200),
745 }]),
746 ConversationMessage::tool_result("1", "bash", "ok ".repeat(200), false),
747 ConversationMessage::assistant(vec![ContentBlock::Text {
748 text: "recent".to_string(),
749 }]),
750 ],
751 };
752
753 let result = handle_slash_command(
754 "/compact",
755 &session,
756 CompactionConfig {
757 preserve_recent_messages: 2,
758 max_estimated_tokens: 1,
759 },
760 )
761 .expect("slash command should be handled");
762
763 assert!(result.message.contains("Compacted 2 messages"));
764 assert_eq!(result.session.messages[0].role, MessageRole::System);
765 }
766
767 #[test]
768 fn help_command_is_non_mutating() {
769 let session = Session::new();
770 let result = handle_slash_command("/help", &session, CompactionConfig::default())
771 .expect("help command should be handled");
772 assert_eq!(result.session, session);
773 assert!(result.message.contains("Slash commands"));
774 }
775
776 #[test]
777 fn ignores_unknown_or_runtime_bound_slash_commands() {
778 let session = Session::new();
779 assert!(handle_slash_command("/unknown", &session, CompactionConfig::default()).is_none());
780 assert!(handle_slash_command("/status", &session, CompactionConfig::default()).is_none());
781 assert!(
782 handle_slash_command("/bughunter", &session, CompactionConfig::default()).is_none()
783 );
784 assert!(handle_slash_command("/commit", &session, CompactionConfig::default()).is_none());
785 assert!(handle_slash_command("/pr", &session, CompactionConfig::default()).is_none());
786 assert!(handle_slash_command("/issue", &session, CompactionConfig::default()).is_none());
787 assert!(
788 handle_slash_command("/ultraplan", &session, CompactionConfig::default()).is_none()
789 );
790 assert!(
791 handle_slash_command("/teleport foo", &session, CompactionConfig::default()).is_none()
792 );
793 assert!(
794 handle_slash_command("/debug-tool-call", &session, CompactionConfig::default())
795 .is_none()
796 );
797 assert!(
798 handle_slash_command("/model ternlang", &session, CompactionConfig::default()).is_none()
799 );
800 assert!(handle_slash_command(
801 "/permissions read-only",
802 &session,
803 CompactionConfig::default()
804 )
805 .is_none());
806 assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
807 assert!(
808 handle_slash_command("/clear --confirm", &session, CompactionConfig::default())
809 .is_none()
810 );
811 assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
812 assert!(handle_slash_command(
813 "/resume session.json",
814 &session,
815 CompactionConfig::default()
816 )
817 .is_none());
818 assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none());
819 assert!(
820 handle_slash_command("/config env", &session, CompactionConfig::default()).is_none()
821 );
822 assert!(handle_slash_command("/diff", &session, CompactionConfig::default()).is_none());
823 assert!(handle_slash_command("/version", &session, CompactionConfig::default()).is_none());
824 assert!(
825 handle_slash_command("/export note.txt", &session, CompactionConfig::default())
826 .is_none()
827 );
828 assert!(
829 handle_slash_command("/session list", &session, CompactionConfig::default()).is_none()
830 );
831 }
832}