From e30f50a92e8c92486fafcdd8b417c6a463a199cc Mon Sep 17 00:00:00 2001 From: Wendy Tang Date: Tue, 4 Mar 2025 13:56:09 -0800 Subject: [PATCH] feat: add mode completion (#1506) --- crates/goose-cli/src/session/completion.rs | 45 ++++++++++++++++++++++ crates/goose-cli/src/session/input.rs | 33 +++++++++++----- crates/goose-cli/src/session/mod.rs | 22 +++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index 5230a70e55..9346d499b7 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -75,6 +75,46 @@ impl GooseCompleter { Ok((line.len(), vec![])) } + /// Complete flags for the /mode command + fn complete_mode_flags(&self, line: &str) -> Result<(usize, Vec)> { + let modes = ["auto", "approve", "chat"]; + + let parts: Vec<&str> = line.split_whitespace().collect(); + + // If we're just after "/mode" with a space, show all options + if line == "/mode " { + return Ok(( + line.len(), + modes + .iter() + .map(|mode| Pair { + display: mode.to_string(), + replacement: format!("{} ", mode), + }) + .collect(), + )); + } + + // If we're typing a mode name, show the flags for that mode + if parts.len() == 2 { + let partial = parts[1].to_lowercase(); + return Ok(( + line.len() - partial.len(), + modes + .iter() + .filter(|mode| mode.to_lowercase().starts_with(&partial.to_lowercase())) + .map(|mode| Pair { + display: mode.to_string(), + replacement: format!("{} ", mode), + }) + .collect(), + )); + } + + // No completions available + Ok((line.len(), vec![])) + } + /// Complete slash commands fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec)> { // Define available slash commands @@ -88,6 +128,7 @@ impl GooseCompleter { "/builtin", "/prompts", "/prompt", + "/mode", ]; // Find commands that match the prefix @@ -292,6 +333,10 @@ impl Completer for GooseCompleter { )); } } + + if line.starts_with("/mode") { + return self.complete_mode_flags(line); + } } // Default: no completions diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 4dd1c43b30..56a9d35e65 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -1,10 +1,9 @@ +use super::completion::GooseCompleter; use anyhow::Result; use rustyline::Editor; use shlex; use std::collections::HashMap; -use super::completion::GooseCompleter; - #[derive(Debug)] pub enum InputResult { Message(String), @@ -15,6 +14,7 @@ pub enum InputResult { Retry, ListPrompts(Option), PromptCommand(PromptCommandOptions), + GooseMode(String), } #[derive(Debug)] @@ -65,6 +65,14 @@ pub fn get_input( fn handle_slash_command(input: &str) -> Option { let input = input.trim(); + // Command prefix constants + const CMD_PROMPTS: &str = "/prompts "; + const CMD_PROMPT: &str = "/prompt"; + const CMD_PROMPT_WITH_SPACE: &str = "/prompt "; + const CMD_EXTENSION: &str = "/extension "; + const CMD_BUILTIN: &str = "/builtin "; + const CMD_MODE: &str = "/mode "; + match input { "/exit" | "/quit" => Some(InputResult::Exit), "/?" | "/help" => { @@ -73,20 +81,20 @@ fn handle_slash_command(input: &str) -> Option { } "/t" => Some(InputResult::ToggleTheme), "/prompts" => Some(InputResult::ListPrompts(None)), - s if s.starts_with("/prompts ") => { + s if s.starts_with(CMD_PROMPTS) => { // Parse arguments for /prompts command - let args = s.strip_prefix("/prompts ").unwrap_or_default(); + let args = s.strip_prefix(CMD_PROMPTS).unwrap_or_default(); parse_prompts_command(args) } - s if s.starts_with("/prompt") => { - if s == "/prompt" { + s if s.starts_with(CMD_PROMPT) => { + if s == CMD_PROMPT { // No arguments case Some(InputResult::PromptCommand(PromptCommandOptions { name: String::new(), // Empty name will trigger the error message in the rendering info: false, arguments: HashMap::new(), })) - } else if let Some(stripped) = s.strip_prefix("/prompt ") { + } else if let Some(stripped) = s.strip_prefix(CMD_PROMPT_WITH_SPACE) { // Has arguments case parse_prompt_command(stripped) } else { @@ -94,8 +102,15 @@ fn handle_slash_command(input: &str) -> Option { None } } - s if s.starts_with("/extension ") => Some(InputResult::AddExtension(s[11..].to_string())), - s if s.starts_with("/builtin ") => Some(InputResult::AddBuiltin(s[9..].to_string())), + s if s.starts_with(CMD_EXTENSION) => Some(InputResult::AddExtension( + s[CMD_EXTENSION.len()..].to_string(), + )), + s if s.starts_with(CMD_BUILTIN) => { + Some(InputResult::AddBuiltin(s[CMD_BUILTIN.len()..].to_string())) + } + s if s.starts_with(CMD_MODE) => { + Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) + } _ => None, } } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 5f39ab9e32..9a477f612f 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -14,6 +14,7 @@ use etcetera::choose_app_strategy; use etcetera::AppStrategy; use goose::agents::extension::{Envs, ExtensionConfig}; use goose::agents::Agent; +use goose::config::Config; use goose::message::{Message, MessageContent}; use goose::session; use mcp_core::handler::ToolError; @@ -325,6 +326,27 @@ impl Session { Err(e) => output::render_error(&e.to_string()), } } + input::InputResult::GooseMode(mode) => { + save_history(&mut editor); + + let config = Config::global(); + let mode = mode.to_lowercase(); + + // Check if mode is valid + if !["auto", "approve", "chat"].contains(&mode.as_str()) { + output::render_error(&format!( + "Invalid mode '{}'. Mode must be one of: auto, approve, chat", + mode + )); + continue; + } + + config + .set("GOOSE_MODE", Value::String(mode.to_string())) + .unwrap(); + println!("Goose mode set to '{}'", mode); + continue; + } input::InputResult::PromptCommand(opts) => { save_history(&mut editor);