mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: config will prompt to save from env (#722)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<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),
|
||||
|
||||
Reference in New Issue
Block a user