Skip to content

Commit

Permalink
feat: config will prompt to save from env (#722)
Browse files Browse the repository at this point in the history
  • Loading branch information
baxen authored Jan 23, 2025
1 parent 4e9d85b commit f60d6c9
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 41 deletions.
104 changes: 65 additions & 39 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,60 +123,86 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
continue;
}

// Try to get existing value from environment or config
let existing: Result<String, _> = 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<String, _> = 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
Expand Down
131 changes: 130 additions & 1 deletion crates/goose-cli/src/prompt/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::io::{self, Write};
use std::path::PathBuf;

use bat::WrappingMode;
use console::style;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 10 additions & 1 deletion crates/goose-cli/src/prompt/rustyline.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -26,12 +28,19 @@ impl RustylinePrompt {
let mut renderers: HashMap<String, Box<dyn ToolRenderer>> = 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),
Expand Down

0 comments on commit f60d6c9

Please sign in to comment.