diff --git a/.gitignore b/.gitignore index 8c6129ab3b22..f9edb850c007 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ do_not_version/ /ui/desktop/src/bin/goose-scheduler-executor /ui/desktop/src/bin/goose /.env + +/working_dir diff --git a/Cargo.lock b/Cargo.lock index b434baefb901..96f10e6ed272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3327,6 +3327,7 @@ dependencies = [ name = "goose-cli" version = "1.1.0" dependencies = [ + "anstream", "anyhow", "async-trait", "axum", @@ -3344,6 +3345,7 @@ dependencies = [ "goose-mcp", "http 1.2.0", "indicatif", + "is-terminal", "jsonschema", "mcp-client", "mcp-core", @@ -3578,9 +3580,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -4180,11 +4182,11 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi 0.5.2", "libc", "windows-sys 0.59.0", ] diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index bb50020858c0..89b4b0e54650 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -56,6 +56,8 @@ http = "1.0" webbrowser = "1.0" indicatif = "0.17.11" tokio-util = "0.7.15" +is-terminal = "0.4.16" +anstream = "0.6.18" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["wincred"] } diff --git a/crates/goose-cli/src/recipes/print_recipe.rs b/crates/goose-cli/src/recipes/print_recipe.rs index be4a2339875f..98609a7412f9 100644 --- a/crates/goose-cli/src/recipes/print_recipe.rs +++ b/crates/goose-cli/src/recipes/print_recipe.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use anstream::println; use console::style; use goose::recipe::{Recipe, BUILT_IN_RECIPE_DIR_PARAM}; @@ -81,16 +82,16 @@ pub fn missing_parameters_command_line(missing_params: Vec) -> String { } pub fn print_recipe_info(recipe: &Recipe, params: Vec<(String, String)>) { - println!( + eprintln!( "{} {}", style("Loading recipe:").green().bold(), style(&recipe.title).green() ); - println!("{} {}", style("Description:").bold(), &recipe.description); + eprintln!("{} {}", style("Description:").bold(), &recipe.description); if !params.is_empty() { - println!("{}", style("Parameters used to load this recipe:").bold()); + eprintln!("{}", style("Parameters used to load this recipe:").bold()); print_parameters_with_values(params.into_iter().collect()); } - println!(); + eprintln!(); } diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 3b0420a30bac..880416b5d0a7 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -1,3 +1,4 @@ +use anstream::println; use bat::WrappingMode; use console::{style, Color}; use goose::config::Config; @@ -11,11 +12,10 @@ use rmcp::model::PromptArgument; use serde_json::Value; use std::cell::RefCell; use std::collections::HashMap; -use std::io::{Error, Write}; +use std::io::{Error, IsTerminal, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; - // Re-export theme for use in main #[derive(Clone, Copy)] pub enum Theme { @@ -131,20 +131,26 @@ thread_local! { } pub fn show_thinking() { - THINKING.with(|t| t.borrow_mut().show()); + if std::io::stdout().is_terminal() { + THINKING.with(|t| t.borrow_mut().show()); + } } pub fn hide_thinking() { - THINKING.with(|t| t.borrow_mut().hide()); + if std::io::stdout().is_terminal() { + THINKING.with(|t| t.borrow_mut().hide()); + } } #[allow(dead_code)] pub fn set_thinking_message(s: &String) { - THINKING.with(|t| { - if let Some(spinner) = t.borrow_mut().spinner.as_mut() { - spinner.set_message(s); - } - }); + if std::io::stdout().is_terminal() { + THINKING.with(|t| { + if let Some(spinner) = t.borrow_mut().spinner.as_mut() { + spinner.set_message(s); + } + }); + } } pub fn render_message(message: &Message, debug: bool) { @@ -159,7 +165,9 @@ pub fn render_message(message: &Message, debug: bool) { println!("Image: [data: {}, type: {}]", image.data, image.mime_type); } MessageContent::Thinking(thinking) => { - if std::env::var("GOOSE_CLI_SHOW_THINKING").is_ok() { + if std::env::var("GOOSE_CLI_SHOW_THINKING").is_ok() + && std::io::stdout().is_terminal() + { println!("\n{}", style("Thinking:").dim().italic()); print_markdown(&thinking.thinking, theme); } @@ -183,6 +191,10 @@ pub fn render_text(text: &str, color: Option, dim: bool) { } pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { + if !std::io::stdout().is_terminal() { + println!("{}", text); + return; + } let mut styled_text = style(text); if dim { styled_text = styled_text.dim(); @@ -287,17 +299,18 @@ pub fn render_prompts(prompts: &HashMap>) { pub fn render_prompt_info(info: &PromptInfo) { println!(); - if let Some(ext) = &info.extension { println!(" {}: {}", style("Extension").green(), ext); } - println!(" Prompt: {}", style(&info.name).cyan().bold()); - if let Some(desc) = &info.description { println!("\n {}", desc); } + render_arguments(info); + println!(); +} +fn render_arguments(info: &PromptInfo) { if let Some(args) = &info.arguments { println!("\n Arguments:"); for arg in args { @@ -316,7 +329,6 @@ pub fn render_prompt_info(info: &PromptInfo) { ); } } - println!(); } pub fn render_extension_success(name: &str) { @@ -465,14 +477,18 @@ pub fn env_no_color() -> bool { } fn print_markdown(content: &str, theme: Theme) { - bat::PrettyPrinter::new() - .input(bat::Input::from_bytes(content.as_bytes())) - .theme(theme.as_str()) - .colored_output(env_no_color()) - .language("Markdown") - .wrapping_mode(WrappingMode::NoWrapping(true)) - .print() - .unwrap(); + if std::io::stdout().is_terminal() { + bat::PrettyPrinter::new() + .input(bat::Input::from_bytes(content.as_bytes())) + .theme(theme.as_str()) + .colored_output(env_no_color()) + .language("Markdown") + .wrapping_mode(WrappingMode::NoWrapping(true)) + .print() + .unwrap(); + } else { + print!("{}", content); + } } const INDENT: &str = " "; @@ -484,6 +500,23 @@ fn get_tool_params_max_length() -> usize { .unwrap_or(40) } +fn print_value(value: &Value, debug: bool) { + let formatted = match value { + Value::String(s) => { + if !debug && s.len() > get_tool_params_max_length() { + style(format!("[REDACTED: {} chars]", s.len())).yellow() + } else { + style(s.to_string()).green() + } + } + Value::Number(n) => style(n.to_string()).yellow(), + Value::Bool(b) => style(b.to_string()).yellow(), + Value::Null => style("null".to_string()).dim(), + _ => unreachable!(), + }; + println!("{}", formatted); +} + fn print_params(value: &Value, depth: usize, debug: bool) { let indent = INDENT.repeat(depth); @@ -502,21 +535,9 @@ fn print_params(value: &Value, depth: usize, debug: bool) { print_params(item, depth + 2, debug); } } - Value::String(s) => { - if !debug && s.len() > get_tool_params_max_length() { - println!("{}{}: {}", indent, style(key).dim(), style("...").dim()); - } else { - println!("{}{}: {}", indent, style(key).dim(), style(s).green()); - } - } - Value::Number(n) => { - println!("{}{}: {}", indent, style(key).dim(), style(n).blue()); - } - Value::Bool(b) => { - println!("{}{}: {}", indent, style(key).dim(), style(b).blue()); - } - Value::Null => { - println!("{}{}: {}", indent, style(key).dim(), style("null").dim()); + _ => { + print!("{}{}: ", indent, style(key).dim()); + print_value(val, debug); } } } @@ -527,26 +548,7 @@ fn print_params(value: &Value, depth: usize, debug: bool) { print_params(item, depth + 1, debug); } } - Value::String(s) => { - if !debug && s.len() > get_tool_params_max_length() { - println!( - "{}{}", - indent, - style(format!("[REDACTED: {} chars]", s.len())).yellow() - ); - } else { - println!("{}{}", indent, style(s).green()); - } - } - Value::Number(n) => { - println!("{}{}", indent, style(n).yellow()); - } - Value::Bool(b) => { - println!("{}{}", indent, style(b).yellow()); - } - Value::Null => { - println!("{}{}", indent, style("null").dim()); - } + _ => print_value(value, debug), } } @@ -775,7 +777,7 @@ pub async fn display_cost_usage( ) { if let Some(cost) = estimate_cost_usd(provider, model, input_tokens, output_tokens).await { use console::style; - println!( + eprintln!( "Cost: {} USD ({} tokens: in {}, out {})", style(format!("${:.4}", cost)).cyan(), input_tokens + output_tokens,