diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 5b56f7ab9..a9c96bd4a 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -123,60 +123,86 @@ pub async fn configure_provider_dialog() -> Result> { continue; } - // Try to get existing value from environment or config - let existing: Result = if key.secret { - config.get_secret(&key.name) - } else { - config.get(&key.name) - }; - - match existing { - Ok(_) => { - let _ = cliclack::log::info(format!("{} is already configured", key.name)); - if cliclack::confirm("Would you like to update this value?").interact()? { - let new_value: String = if key.secret { - cliclack::password(format!("Enter new value for {}", key.name)) - .mask('▪') - .interact()? - } else { - cliclack::input(format!("Enter new value for {}", key.name)).interact()? - }; - + // First check if the value is set via environment variable + let from_env = std::env::var(&key.name).ok(); + + match from_env { + Some(env_value) => { + let _ = + cliclack::log::info(format!("{} is set via environment variable", key.name)); + if cliclack::confirm("Would you like to save this value to your config file?") + .initial_value(true) + .interact()? + { if key.secret { - config.set_secret(&key.name, Value::String(new_value))?; + config.set_secret(&key.name, Value::String(env_value))?; } else { - config.set(&key.name, Value::String(new_value))?; + config.set(&key.name, Value::String(env_value))?; } + let _ = cliclack::log::info(format!("Saved {} to config file", key.name)); } } - Err(_) => { - let value: String = if key.secret { - cliclack::password(format!( - "Provider {} requires {}, please enter a value", - provider_meta.display_name, key.name - )) - .mask('▪') - .interact()? + None => { + // No env var, check config/secret storage + let existing: Result = if key.secret { + config.get_secret(&key.name) } else { - cliclack::input(format!( - "Provider {} requires {}, please enter a value", - provider_meta.display_name, key.name - )) - .interact()? + config.get(&key.name) }; - if key.secret { - config.set_secret(&key.name, Value::String(value))?; - } else { - config.set(&key.name, Value::String(value))?; + match existing { + Ok(_) => { + let _ = cliclack::log::info(format!("{} is already configured", key.name)); + if cliclack::confirm("Would you like to update this value?").interact()? { + let new_value: String = if key.secret { + cliclack::password(format!("Enter new value for {}", key.name)) + .mask('▪') + .interact()? + } else { + cliclack::input(format!("Enter new value for {}", key.name)) + .interact()? + }; + + if key.secret { + config.set_secret(&key.name, Value::String(new_value))?; + } else { + config.set(&key.name, Value::String(new_value))?; + } + } + } + Err(_) => { + let value: String = if key.secret { + cliclack::password(format!( + "Provider {} requires {}, please enter a value", + provider_meta.display_name, key.name + )) + .mask('▪') + .interact()? + } else { + cliclack::input(format!( + "Provider {} requires {}, please enter a value", + provider_meta.display_name, key.name + )) + .interact()? + }; + + if key.secret { + config.set_secret(&key.name, Value::String(value))?; + } else { + config.set(&key.name, Value::String(value))?; + } + } } } } } // Select model, defaulting to the provider's recommended model + let default_model = config + .get("GOOSE_MODEL") + .unwrap_or(provider_meta.default_model.clone()); let model: String = cliclack::input("Enter a model from that provider:") - .default_input(&provider_meta.default_model) + .default_input(&default_model) .interact()?; // Update config with new values diff --git a/crates/goose-cli/src/prompt/renderer.rs b/crates/goose-cli/src/prompt/renderer.rs index 5a2700bc5..0926ba0b5 100644 --- a/crates/goose-cli/src/prompt/renderer.rs +++ b/crates/goose-cli/src/prompt/renderer.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::io::{self, Write}; +use std::path::PathBuf; use bat::WrappingMode; use console::style; @@ -11,8 +12,93 @@ use serde_json::Value; use super::Theme; const MAX_STRING_LENGTH: usize = 40; +const MAX_PATH_LENGTH: usize = 60; const INDENT: &str = " "; +/// Shortens a path string by abbreviating directory names while keeping the last two components intact. +/// If the path starts with the user's home directory, it will be replaced with ~. +/// +/// # Examples +/// ``` +/// let path = "/Users/alice/Development/very/long/path/to/file.txt"; +/// assert_eq!( +/// shorten_path(path), +/// "~/D/v/l/p/to/file.txt" +/// ); +/// ``` +fn shorten_path(path: &str) -> String { + let path = PathBuf::from(path); + + // First try to convert to ~ if it's in home directory + let home = dirs::home_dir(); + let path_str = if let Some(home) = home { + if let Ok(stripped) = path.strip_prefix(home) { + format!("~/{}", stripped.display()) + } else { + path.display().to_string() + } + } else { + path.display().to_string() + }; + + // If path is already short enough, return as is + if path_str.len() <= MAX_PATH_LENGTH { + return path_str; + } + + let parts: Vec<_> = path_str.split('/').collect(); + + // If we have 3 or fewer parts, return as is + if parts.len() <= 3 { + return path_str; + } + + // Keep the first component (empty string before root / or ~) and last two components intact + let mut shortened = vec![parts[0].to_string()]; + + // Shorten middle components to their first letter + for component in &parts[1..parts.len() - 2] { + if !component.is_empty() { + shortened.push(component.chars().next().unwrap_or('?').to_string()); + } + } + + // Add the last two components + shortened.push(parts[parts.len() - 2].to_string()); + shortened.push(parts[parts.len() - 1].to_string()); + + shortened.join("/") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_shorten_path() { + // Test a long path without home directory + let long_path = "/Users/test/Development/this/is/a/very/long/nested/deeply/example.txt"; + let shortened = shorten_path(long_path); + assert!( + shortened.len() < long_path.len(), + "Shortened path '{}' should be shorter than original '{}'", + shortened, + long_path + ); + assert!( + shortened.ends_with("deeply/example.txt"), + "Shortened path '{}' should end with 'deeply/example.txt'", + shortened + ); + + // Test a short path (shouldn't be modified) + assert_eq!(shorten_path("/usr/local/bin"), "/usr/local/bin"); + + // Test path with less than 3 components + assert_eq!(shorten_path("/usr/local"), "/usr/local"); + } +} + /// Implement the ToolRenderer trait for each tool that you want to render in the prompt. pub trait ToolRenderer: ToolRendererClone { fn tool_name(&self) -> String; @@ -68,12 +154,55 @@ impl ToolRenderer for DefaultRenderer { } } +#[derive(Clone)] +pub struct TextEditorRenderer; + +impl ToolRenderer for TextEditorRenderer { + fn tool_name(&self) -> String { + "developer__text_editor".to_string() + } + + fn request(&self, tool_request: &ToolRequest, theme: &str) { + match &tool_request.tool_call { + Ok(call) => { + default_print_request_header(call); + + // Print path first with special formatting + if let Some(Value::String(path)) = call.arguments.get("path") { + println!( + "{}: {}", + style("path").dim(), + style(shorten_path(path)).green() + ); + } + + // Print other arguments normally, excluding path + if let Some(args) = call.arguments.as_object() { + let mut other_args = serde_json::Map::new(); + for (k, v) in args { + if k != "path" { + other_args.insert(k.clone(), v.clone()); + } + } + print_params(&Value::Object(other_args), 0); + } + print_newline(); + } + Err(e) => print_markdown(&e.to_string(), theme), + } + } + + fn response(&self, tool_response: &ToolResponse, theme: &str) { + default_response_renderer(tool_response, theme); + } +} + #[derive(Clone)] pub struct BashDeveloperExtensionRenderer; impl ToolRenderer for BashDeveloperExtensionRenderer { fn tool_name(&self) -> String { - "DeveloperExtension__bash".to_string() + "developer__shell".to_string() } fn request(&self, tool_request: &ToolRequest, theme: &str) { diff --git a/crates/goose-cli/src/prompt/rustyline.rs b/crates/goose-cli/src/prompt/rustyline.rs index c196bb325..bedba60be 100644 --- a/crates/goose-cli/src/prompt/rustyline.rs +++ b/crates/goose-cli/src/prompt/rustyline.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use super::{ - renderer::{render, BashDeveloperExtensionRenderer, DefaultRenderer, ToolRenderer}, + renderer::{ + render, BashDeveloperExtensionRenderer, DefaultRenderer, TextEditorRenderer, ToolRenderer, + }, thinking::get_random_thinking_message, Input, InputType, Prompt, Theme, }; @@ -26,12 +28,19 @@ impl RustylinePrompt { let mut renderers: HashMap> = HashMap::new(); let default_renderer = DefaultRenderer; renderers.insert(default_renderer.tool_name(), Box::new(default_renderer)); + let bash_dev_extension_renderer = BashDeveloperExtensionRenderer; renderers.insert( bash_dev_extension_renderer.tool_name(), Box::new(bash_dev_extension_renderer), ); + let text_editor_renderer = TextEditorRenderer; + renderers.insert( + text_editor_renderer.tool_name(), + Box::new(text_editor_renderer), + ); + let mut editor = DefaultEditor::new().expect("Failed to create editor"); editor.bind_sequence( KeyEvent(KeyCode::Char('j'), Modifiers::CTRL),