From 2eea54c202a5f3820fefc0ba2114f6192d21f9ea Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Sun, 5 Jul 2026 04:42:42 -0500 Subject: [PATCH] fix(cli): sync slash help with builtins (#10173) --- crates/goose-cli/src/session/completion.rs | 44 ++++++++++++++-------- crates/goose-cli/src/session/input.rs | 44 ++++++++++++++++++++-- 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index a96ce7540..ee4f3c49d 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -1,3 +1,4 @@ +use goose::agents::execute_commands::list_commands; use goose::config::GooseMode; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::highlight::{CmdKind, Highlighter}; @@ -152,23 +153,25 @@ impl GooseCompleter { /// Complete slash commands fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec)> { - // Define available slash commands - let commands = [ - "/exit", - "/quit", - "/help", - "/?", - "/t", - "/extension", - "/builtin", - "/prompts", - "/prompt", - "/mode", - "/model", - "/recipe", - "/skills", - "/status", + let mut commands = vec![ + "/exit".to_string(), + "/quit".to_string(), + "/help".to_string(), + "/?".to_string(), + "/t".to_string(), + "/extension".to_string(), + "/builtin".to_string(), + "/mode".to_string(), + "/model".to_string(), + "/recipe".to_string(), ]; + commands.extend( + list_commands() + .iter() + .map(|command| format!("/{}", command.name)), + ); + commands.sort(); + commands.dedup(); // Find commands that match the prefix let matching_commands: Vec = commands @@ -578,6 +581,15 @@ mod tests { let (pos, candidates) = completer.complete_slash_commands("/").unwrap(); assert_eq!(pos, 0); assert!(candidates.len() > 1); + for command in list_commands() { + assert!( + candidates + .iter() + .any(|candidate| candidate.display == format!("/{}", command.name)), + "slash completion should list /{}", + command.name + ); + } // Test no match let (_pos, candidates) = completer.complete_slash_commands("/nonexistent").unwrap(); diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 2bd8cafe2..6b9da991a 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -401,10 +401,17 @@ fn parse_plan_command(input: String) -> Option { Some(InputResult::Plan(options)) } -fn print_help() { +fn help_text() -> String { let newline_key = get_newline_key().to_ascii_uppercase(); let modes = GooseMode::VARIANTS.join(", "); - println!( + let additional_builtin_help = additional_builtin_help(); + let additional_builtin_help = if additional_builtin_help.is_empty() { + String::new() + } else { + format!("{additional_builtin_help}\n") + }; + + format!( "Available commands: /exit or /quit - Exit the session /t - Toggle Light/Dark/Ansi theme @@ -425,7 +432,7 @@ fn print_help() { /recipe [filepath] - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml). If no filepath is provided, it will be saved to ./recipe.yaml. /compact - Compact the current conversation to reduce context length while preserving key information. -/status - Show session status: model, provider, mode, and token usage. +{additional_builtin_help}/status - Show session status: model, provider, mode, and token usage. /edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text. Uses $GOOSE_PROMPT_EDITOR, $VISUAL, or $EDITOR (in that order). /skills - List available skills or enable skills by name (usage: /skills [...]) @@ -436,7 +443,23 @@ Navigation: Ctrl+C - Clear current line if text is entered, otherwise exit the session Ctrl+{newline_key} - Add a newline (configurable via GOOSE_CLI_NEWLINE_KEY) Up/Down arrows - Navigate through command history" - ); + ) +} + +fn additional_builtin_help() -> String { + const DOCUMENTED_BUILTINS: &[&str] = + &["prompts", "prompt", "compact", "clear", "skills", "status"]; + + goose::agents::execute_commands::list_commands() + .iter() + .filter(|command| !DOCUMENTED_BUILTINS.contains(&command.name)) + .map(|command| format!("/{} - {}", command.name, command.description)) + .collect::>() + .join("\n") +} + +fn print_help() { + println!("{}", help_text()); } /// Extract recent messages for editor context @@ -535,6 +558,19 @@ mod tests { assert!(handle_slash_command("/unknown").is_none()); } + #[test] + fn help_lists_builtin_agent_commands() { + let help = help_text(); + + for command in goose::agents::execute_commands::list_commands() { + assert!( + help.contains(&format!("/{}", command.name)), + "help output should list /{}", + command.name + ); + } + } + #[test] fn test_prompts_command() { // Test basic prompts command