Skip to main content

albert_commands/
lib.rs

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            // Aggressive manual compression: only keep last 2 messages
541            let result = compact_session(session, CompactionConfig {
542                preserve_recent_messages: 2,
543                max_estimated_tokens: 1, // Force it
544            });
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}