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];
253
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub enum SlashCommand {
256    Help,
257    Status,
258    Compact,
259    Compress,
260    Bughunter {
261        scope: Option<String>,
262    },
263    Commit,
264    Pr {
265        context: Option<String>,
266    },
267    Issue {
268        context: Option<String>,
269    },
270    Ultraplan {
271        task: Option<String>,
272    },
273    Teleport {
274        target: Option<String>,
275    },
276    DebugToolCall,
277    Model {
278        model: Option<String>,
279    },
280    Permissions {
281        mode: Option<String>,
282    },
283    Clear {
284        confirm: bool,
285    },
286    Cost,
287    Resume {
288        session_path: Option<String>,
289    },
290    Config {
291        section: Option<String>,
292    },
293    Memory,
294    Init,
295    Diff,
296    Version,
297    Export {
298        path: Option<String>,
299    },
300    Session {
301        action: Option<String>,
302        target: Option<String>,
303    },
304    Auth {
305        provider: Option<String>,
306    },
307    Plan {
308        task: Option<String>,
309    },
310    Tdd {
311        interface: Option<String>,
312    },
313    Verify,
314    CodeReview {
315        files: Option<String>,
316    },
317    BuildFix,
318    Aside {
319        question: Option<String>,
320    },
321    Learn,
322    Refactor {
323        scope: Option<String>,
324    },
325    Checkpoint {
326        label: Option<String>,
327    },
328    Docs {
329        query: Option<String>,
330    },
331    Loop {
332        mission: Option<String>,
333    },
334    Unknown(String),
335}
336
337impl SlashCommand {
338    #[must_use]
339    pub fn parse(input: &str) -> Option<Self> {
340        let trimmed = input.trim();
341        if !trimmed.starts_with('/') {
342            return None;
343        }
344
345        let mut parts = trimmed.trim_start_matches('/').split_whitespace();
346        let command = parts.next().unwrap_or_default();
347        Some(match command {
348            "help" => Self::Help,
349            "status" => Self::Status,
350            "compact" => Self::Compact,
351            "compress" => Self::Compress,
352            "bughunter" => Self::Bughunter {
353                scope: remainder_after_command(trimmed, command),
354            },
355            "commit" => Self::Commit,
356            "pr" => Self::Pr {
357                context: remainder_after_command(trimmed, command),
358            },
359            "issue" => Self::Issue {
360                context: remainder_after_command(trimmed, command),
361            },
362            "ultraplan" => Self::Ultraplan {
363                task: remainder_after_command(trimmed, command),
364            },
365            "teleport" => Self::Teleport {
366                target: remainder_after_command(trimmed, command),
367            },
368            "debug-tool-call" => Self::DebugToolCall,
369            "model" => Self::Model {
370                model: parts.next().map(ToOwned::to_owned),
371            },
372            "permissions" => Self::Permissions {
373                mode: parts.next().map(ToOwned::to_owned),
374            },
375            "clear" => Self::Clear {
376                confirm: parts.next() == Some("--confirm"),
377            },
378            "cost" => Self::Cost,
379            "resume" => Self::Resume {
380                session_path: parts.next().map(ToOwned::to_owned),
381            },
382            "config" => Self::Config {
383                section: parts.next().map(ToOwned::to_owned),
384            },
385            "memory" => Self::Memory,
386            "init" => Self::Init,
387            "diff" => Self::Diff,
388            "version" => Self::Version,
389            "export" => Self::Export {
390                path: parts.next().map(ToOwned::to_owned),
391            },
392            "session" => Self::Session {
393                action: parts.next().map(ToOwned::to_owned),
394                target: parts.next().map(ToOwned::to_owned),
395            },
396            "auth" => Self::Auth {
397                provider: parts.next().map(ToOwned::to_owned),
398            },
399            "plan" => Self::Plan {
400                task: remainder_after_command(trimmed, command),
401            },
402            "tdd" => Self::Tdd {
403                interface: remainder_after_command(trimmed, command),
404            },
405            "verify" => Self::Verify,
406            "code-review" => Self::CodeReview {
407                files: remainder_after_command(trimmed, command),
408            },
409            "build-fix" => Self::BuildFix,
410            "aside" => Self::Aside {
411                question: remainder_after_command(trimmed, command),
412            },
413            "learn" => Self::Learn,
414            "refactor" => Self::Refactor {
415                scope: remainder_after_command(trimmed, command),
416            },
417            "checkpoint" => Self::Checkpoint {
418                label: remainder_after_command(trimmed, command),
419            },
420            "docs" => Self::Docs {
421                query: remainder_after_command(trimmed, command),
422            },
423            "loop" => Self::Loop {
424                mission: remainder_after_command(trimmed, command),
425            },
426            other => Self::Unknown(other.to_string()),
427        })
428    }
429}
430
431fn remainder_after_command(input: &str, command: &str) -> Option<String> {
432    input
433        .trim()
434        .strip_prefix(&format!("/{command}"))
435        .map(str::trim)
436        .filter(|value| !value.is_empty())
437        .map(ToOwned::to_owned)
438}
439
440#[must_use]
441pub fn slash_command_specs() -> &'static [SlashCommandSpec] {
442    SLASH_COMMAND_SPECS
443}
444
445#[must_use]
446pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
447    slash_command_specs()
448        .iter()
449        .filter(|spec| spec.resume_supported)
450        .collect()
451}
452
453#[must_use]
454pub fn render_slash_command_help() -> String {
455    use console::style;
456    
457    let specs = slash_command_specs();
458    let mut output = String::new();
459    
460    output.push_str(&format!("\n{}\n", style("SLASH COMMAND LIBRARY").bold().underlined()));
461    output.push_str(&format!("  {}\n", style("[resume] works with --resume SESSION.json").dim()));
462
463    let categories = vec![
464        ("SESSION & CONTEXT", vec!["status", "clear", "resume", "session", "export", "compact", "compress", "cost", "memory", "aside", "checkpoint", "learn"]),
465        ("DEVELOPMENT & REASONING", vec!["ultraplan", "plan", "loop", "tdd", "verify", "code-review", "build-fix", "refactor", "docs", "bughunter", "init", "teleport", "diff", "commit", "pr", "issue", "debug-tool-call"]),
466        ("CONFIGURATION & AUTH", vec!["model", "permissions", "auth", "config"]),
467        ("UTILITY", vec!["help", "version"]),
468    ];
469
470    for (cat_name, cat_cmds) in categories {
471        output.push_str(&format!("\n{}\n", style(cat_name).cyan().bold()));
472        for cmd_name in cat_cmds {
473            if let Some(spec) = specs.iter().find(|s| s.name == cmd_name) {
474                let name_display = match spec.argument_hint {
475                    Some(hint) => format!("/{} {}", spec.name, hint),
476                    None => format!("/{}", spec.name),
477                };
478                let resume = if spec.resume_supported {
479                    style(" [resume]").dim().to_string()
480                } else {
481                    "".to_string()
482                };
483                output.push_str(&format!("  {:<25} {}{}\n", style(name_display).green(), spec.summary, resume));
484            }
485        }
486    }
487    
488    output
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub struct SlashCommandResult {
493    pub message: String,
494    pub session: Session,
495}
496
497#[must_use]
498pub fn handle_slash_command(
499    input: &str,
500    session: &Session,
501    compaction: CompactionConfig,
502) -> Option<SlashCommandResult> {
503    match SlashCommand::parse(input)? {
504        SlashCommand::Compact => {
505            let result = compact_session(session, compaction);
506            let message = if result.removed_message_count == 0 {
507                "Compaction skipped: session is below the compaction threshold.".to_string()
508            } else {
509                format!(
510                    "Compacted {} messages into a resumable system summary.",
511                    result.removed_message_count
512                )
513            };
514            Some(SlashCommandResult {
515                message,
516                session: result.compacted_session,
517            })
518        }
519        SlashCommand::Compress => {
520            // Aggressive manual compression: only keep last 2 messages
521            let result = compact_session(session, CompactionConfig {
522                preserve_recent_messages: 2,
523                max_estimated_tokens: 1, // Force it
524            });
525            let message = if result.removed_message_count == 0 {
526                "Compression skipped: session is empty or too short.".to_string()
527            } else {
528                format!(
529                    "Aggressively compressed {} messages. Albert's memory is now lean and sharp.",
530                    result.removed_message_count
531                )
532            };
533            Some(SlashCommandResult {
534                message,
535                session: result.compacted_session,
536            })
537        }
538        SlashCommand::Help => Some(SlashCommandResult {
539            message: render_slash_command_help(),
540            session: session.clone(),
541        }),
542        SlashCommand::Auth { .. }
543        | SlashCommand::Status
544        | SlashCommand::Bughunter { .. }
545        | SlashCommand::Commit
546        | SlashCommand::Pr { .. }
547        | SlashCommand::Issue { .. }
548        | SlashCommand::Ultraplan { .. }
549        | SlashCommand::Teleport { .. }
550        | SlashCommand::DebugToolCall
551        | SlashCommand::Model { .. }
552        | SlashCommand::Permissions { .. }
553        | SlashCommand::Clear { .. }
554        | SlashCommand::Cost
555        | SlashCommand::Resume { .. }
556        | SlashCommand::Config { .. }
557        | SlashCommand::Memory
558        | SlashCommand::Init
559        | SlashCommand::Diff
560        | SlashCommand::Version
561        | SlashCommand::Export { .. }
562        | SlashCommand::Session { .. }
563        | SlashCommand::Plan { .. }
564        | SlashCommand::Tdd { .. }
565        | SlashCommand::Verify
566        | SlashCommand::CodeReview { .. }
567        | SlashCommand::BuildFix
568        | SlashCommand::Aside { .. }
569        | SlashCommand::Learn
570        | SlashCommand::Refactor { .. }
571        | SlashCommand::Checkpoint { .. }
572        | SlashCommand::Docs { .. }
573        | SlashCommand::Loop { .. }
574        | SlashCommand::Unknown(_) => None,
575    }
576}
577
578
579#[cfg(test)]
580mod tests {
581    use super::{
582        handle_slash_command, render_slash_command_help, resume_supported_slash_commands,
583        slash_command_specs, SlashCommand,
584    };
585    use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
586
587    #[test]
588    fn parses_supported_slash_commands() {
589        assert_eq!(SlashCommand::parse("/help"), Some(SlashCommand::Help));
590        assert_eq!(SlashCommand::parse(" /status "), Some(SlashCommand::Status));
591        assert_eq!(
592            SlashCommand::parse("/bughunter runtime"),
593            Some(SlashCommand::Bughunter {
594                scope: Some("runtime".to_string())
595            })
596        );
597        assert_eq!(SlashCommand::parse("/commit"), Some(SlashCommand::Commit));
598        assert_eq!(
599            SlashCommand::parse("/pr ready for review"),
600            Some(SlashCommand::Pr {
601                context: Some("ready for review".to_string())
602            })
603        );
604        assert_eq!(
605            SlashCommand::parse("/issue flaky test"),
606            Some(SlashCommand::Issue {
607                context: Some("flaky test".to_string())
608            })
609        );
610        assert_eq!(
611            SlashCommand::parse("/ultraplan ship both features"),
612            Some(SlashCommand::Ultraplan {
613                task: Some("ship both features".to_string())
614            })
615        );
616        assert_eq!(
617            SlashCommand::parse("/teleport conversation.rs"),
618            Some(SlashCommand::Teleport {
619                target: Some("conversation.rs".to_string())
620            })
621        );
622        assert_eq!(
623            SlashCommand::parse("/debug-tool-call"),
624            Some(SlashCommand::DebugToolCall)
625        );
626        assert_eq!(
627            SlashCommand::parse("/model ternlang-opus"),
628            Some(SlashCommand::Model {
629                model: Some("ternlang-opus".to_string()),
630            })
631        );
632        assert_eq!(
633            SlashCommand::parse("/model"),
634            Some(SlashCommand::Model { model: None })
635        );
636        assert_eq!(
637            SlashCommand::parse("/permissions read-only"),
638            Some(SlashCommand::Permissions {
639                mode: Some("read-only".to_string()),
640            })
641        );
642        assert_eq!(
643            SlashCommand::parse("/clear"),
644            Some(SlashCommand::Clear { confirm: false })
645        );
646        assert_eq!(
647            SlashCommand::parse("/clear --confirm"),
648            Some(SlashCommand::Clear { confirm: true })
649        );
650        assert_eq!(SlashCommand::parse("/cost"), Some(SlashCommand::Cost));
651        assert_eq!(
652            SlashCommand::parse("/resume session.json"),
653            Some(SlashCommand::Resume {
654                session_path: Some("session.json".to_string()),
655            })
656        );
657        assert_eq!(
658            SlashCommand::parse("/config"),
659            Some(SlashCommand::Config { section: None })
660        );
661        assert_eq!(
662            SlashCommand::parse("/config env"),
663            Some(SlashCommand::Config {
664                section: Some("env".to_string())
665            })
666        );
667        assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
668        assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
669        assert_eq!(SlashCommand::parse("/diff"), Some(SlashCommand::Diff));
670        assert_eq!(SlashCommand::parse("/version"), Some(SlashCommand::Version));
671        assert_eq!(
672            SlashCommand::parse("/export notes.txt"),
673            Some(SlashCommand::Export {
674                path: Some("notes.txt".to_string())
675            })
676        );
677        assert_eq!(
678            SlashCommand::parse("/session switch abc123"),
679            Some(SlashCommand::Session {
680                action: Some("switch".to_string()),
681                target: Some("abc123".to_string())
682            })
683        );
684    }
685
686    #[test]
687    fn renders_help_from_shared_specs() {
688        let help = render_slash_command_help();
689        assert!(help.contains("works with --resume SESSION.json"));
690        assert!(help.contains("/help"));
691        assert!(help.contains("/status"));
692        assert!(help.contains("/compact"));
693        assert!(help.contains("/bughunter [scope]"));
694        assert!(help.contains("/commit"));
695        assert!(help.contains("/pr [context]"));
696        assert!(help.contains("/issue [context]"));
697        assert!(help.contains("/ultraplan [task]"));
698        assert!(help.contains("/teleport <symbol-or-path>"));
699        assert!(help.contains("/debug-tool-call"));
700        assert!(help.contains("/model [model]"));
701        assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
702        assert!(help.contains("/clear [--confirm]"));
703        assert!(help.contains("/cost"));
704        assert!(help.contains("/resume <session-path>"));
705        assert!(help.contains("/config [env|hooks|model]"));
706        assert!(help.contains("/memory"));
707        assert!(help.contains("/init"));
708        assert!(help.contains("/diff"));
709        assert!(help.contains("/version"));
710        assert!(help.contains("/export [file]"));
711        assert!(help.contains("/session [list|switch <session-id>]"));
712        assert_eq!(slash_command_specs().len(), 22);
713        assert_eq!(resume_supported_slash_commands().len(), 11);
714    }
715
716    #[test]
717    fn compacts_sessions_via_slash_command() {
718        let session = Session {
719            version: 1,
720            messages: vec![
721                ConversationMessage::user_text("a ".repeat(200)),
722                ConversationMessage::assistant(vec![ContentBlock::Text {
723                    text: "b ".repeat(200),
724                }]),
725                ConversationMessage::tool_result("1", "bash", "ok ".repeat(200), false),
726                ConversationMessage::assistant(vec![ContentBlock::Text {
727                    text: "recent".to_string(),
728                }]),
729            ],
730        };
731
732        let result = handle_slash_command(
733            "/compact",
734            &session,
735            CompactionConfig {
736                preserve_recent_messages: 2,
737                max_estimated_tokens: 1,
738            },
739        )
740        .expect("slash command should be handled");
741
742        assert!(result.message.contains("Compacted 2 messages"));
743        assert_eq!(result.session.messages[0].role, MessageRole::System);
744    }
745
746    #[test]
747    fn help_command_is_non_mutating() {
748        let session = Session::new();
749        let result = handle_slash_command("/help", &session, CompactionConfig::default())
750            .expect("help command should be handled");
751        assert_eq!(result.session, session);
752        assert!(result.message.contains("Slash commands"));
753    }
754
755    #[test]
756    fn ignores_unknown_or_runtime_bound_slash_commands() {
757        let session = Session::new();
758        assert!(handle_slash_command("/unknown", &session, CompactionConfig::default()).is_none());
759        assert!(handle_slash_command("/status", &session, CompactionConfig::default()).is_none());
760        assert!(
761            handle_slash_command("/bughunter", &session, CompactionConfig::default()).is_none()
762        );
763        assert!(handle_slash_command("/commit", &session, CompactionConfig::default()).is_none());
764        assert!(handle_slash_command("/pr", &session, CompactionConfig::default()).is_none());
765        assert!(handle_slash_command("/issue", &session, CompactionConfig::default()).is_none());
766        assert!(
767            handle_slash_command("/ultraplan", &session, CompactionConfig::default()).is_none()
768        );
769        assert!(
770            handle_slash_command("/teleport foo", &session, CompactionConfig::default()).is_none()
771        );
772        assert!(
773            handle_slash_command("/debug-tool-call", &session, CompactionConfig::default())
774                .is_none()
775        );
776        assert!(
777            handle_slash_command("/model ternlang", &session, CompactionConfig::default()).is_none()
778        );
779        assert!(handle_slash_command(
780            "/permissions read-only",
781            &session,
782            CompactionConfig::default()
783        )
784        .is_none());
785        assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
786        assert!(
787            handle_slash_command("/clear --confirm", &session, CompactionConfig::default())
788                .is_none()
789        );
790        assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
791        assert!(handle_slash_command(
792            "/resume session.json",
793            &session,
794            CompactionConfig::default()
795        )
796        .is_none());
797        assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none());
798        assert!(
799            handle_slash_command("/config env", &session, CompactionConfig::default()).is_none()
800        );
801        assert!(handle_slash_command("/diff", &session, CompactionConfig::default()).is_none());
802        assert!(handle_slash_command("/version", &session, CompactionConfig::default()).is_none());
803        assert!(
804            handle_slash_command("/export note.txt", &session, CompactionConfig::default())
805                .is_none()
806        );
807        assert!(
808            handle_slash_command("/session list", &session, CompactionConfig::default()).is_none()
809        );
810    }
811}