diff --git a/Cargo.lock b/Cargo.lock index 4001954600bc..73a753ddcf97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,11 +1201,12 @@ dependencies = [ [[package]] name = "bat" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcc9e5637c2330d8eb7b920f2aa5d9e184446c258466f825ea1412c7614cc86" +checksum = "2ab792c2ad113a666f08856c88cdec0a62d732559b1f3982eedf0142571e669a" dependencies = [ "ansi_colours", + "anyhow", "bincode", "bugreport", "bytesize", @@ -1220,17 +1221,23 @@ dependencies = [ "globset", "grep-cli", "home", - "nu-ansi-term 0.49.0", + "indexmap 2.7.1", + "itertools 0.13.0", + "nu-ansi-term 0.50.1", "once_cell", "path_abs", "plist", "regex", "semver", "serde", + "serde_derive", + "serde_with", "serde_yaml", "shell-words", "syntect", + "terminal-colorsaurus", "thiserror 1.0.69", + "toml 0.8.20", "unicode-width 0.1.14", "walkdir", "wild", @@ -1782,14 +1789,12 @@ dependencies = [ [[package]] name = "clircle" -version = "0.4.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e87cbed5354f17bd8ca8821a097fb62599787fe8f611743fad7ee156a0a600" +checksum = "7d9334f725b46fb9bed8580b9b47a932587e044fadb344ed7fa98774b067ac1a" dependencies = [ "cfg-if", - "libc", - "serde", - "winapi", + "windows 0.56.0", ] [[package]] @@ -3285,9 +3290,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.9.0", "libc", @@ -5124,9 +5129,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -5720,11 +5725,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -8247,6 +8252,32 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal-colorsaurus" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7afe4c174a3cbfb52ebcb11b28965daf74fe9111d4e07e40689d05af06e26e8" +dependencies = [ + "cfg-if", + "libc", + "memchr", + "mio", + "terminal-trx", + "windows-sys 0.59.0", + "xterm-color", +] + +[[package]] +name = "terminal-trx" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975b4233aefa1b02456d5e53b22c61653c743e308c51cf4181191d8ce41753ab" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "terminal_size" version = "0.4.1" @@ -8614,6 +8645,7 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", @@ -9464,6 +9496,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.57.0" @@ -9493,6 +9535,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -9518,6 +9572,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -9540,6 +9605,17 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -9945,6 +10021,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "xterm-color" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index da00f89e1e2c..67379050049f 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -24,7 +24,7 @@ mcp-core = { path = "../mcp-core" } clap = { version = "4.4", features = ["derive"] } cliclack = "0.3.5" console = "0.15.8" -bat = "0.24.0" +bat = "0.25.0" anyhow = "1.0" serde_json = "1.0" tokio = { version = "1.43", features = ["full"] } diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 3cfc71809c5f..563b5cdb3b65 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -510,6 +510,13 @@ enum Command { )] quiet: bool, + /// Porcelain mode - redirect all CLI output to stderr and only output final text message to stdout + #[arg( + long = "porcelain", + help = "Porcelain mode. Redirect all CLI output to stderr and only output final text message to stdout" + )] + porcelain: bool, + /// Scheduled job ID (used internally for scheduled executions) #[arg( long = "scheduled-job-id", @@ -677,6 +684,7 @@ pub async fn cli() -> Result<()> { scheduled_job_id: None, interactive: true, quiet: false, + porcelain: false, sub_recipes: None, }) .await; @@ -725,6 +733,7 @@ pub async fn cli() -> Result<()> { render_recipe, scheduled_job_id, quiet, + porcelain, }) => { let (input_config, session_settings, sub_recipes) = match ( instructions, @@ -830,6 +839,7 @@ pub async fn cli() -> Result<()> { scheduled_job_id, interactive, // Use the interactive flag from the Run command quiet, + porcelain, sub_recipes, }) .await; @@ -949,6 +959,7 @@ pub async fn cli() -> Result<()> { scheduled_job_id: None, interactive: true, // Default case is always interactive quiet: false, + porcelain: false, sub_recipes: None, }) .await; diff --git a/crates/goose-cli/src/commands/bench.rs b/crates/goose-cli/src/commands/bench.rs index 83d485626399..46ccdbd6adfd 100644 --- a/crates/goose-cli/src/commands/bench.rs +++ b/crates/goose-cli/src/commands/bench.rs @@ -46,6 +46,7 @@ pub async fn agent_generator( interactive: false, // Benchmarking is non-interactive scheduled_job_id: None, quiet: false, + porcelain: false, sub_recipes: None, }) .await; diff --git a/crates/goose-cli/src/lib.rs b/crates/goose-cli/src/lib.rs index 68f2357f5ee0..05f3933e04c1 100644 --- a/crates/goose-cli/src/lib.rs +++ b/crates/goose-cli/src/lib.rs @@ -3,6 +3,7 @@ use once_cell::sync::Lazy; pub mod cli; pub mod commands; pub mod logging; +pub mod print_macros; pub mod project_tracker; pub mod recipes; pub mod session; diff --git a/crates/goose-cli/src/print_macros.rs b/crates/goose-cli/src/print_macros.rs new file mode 100644 index 000000000000..e0741a8d7192 --- /dev/null +++ b/crates/goose-cli/src/print_macros.rs @@ -0,0 +1,42 @@ +/// Macro for conditional printing to stderr or stdout based on a boolean flag. +/// +/// When `use_stderr` is true, prints to stderr using `eprintln!`. +/// When `use_stderr` is false, prints to stdout using `println!`. +/// +/// # Examples +/// +/// ``` +/// use goose_cli::cli_println; +/// cli_println!(true, "This goes to stderr"); +/// cli_println!(false, "This goes to stdout"); +/// let some_flag = true; +/// cli_println!(some_flag, "Hello {}", "world"); +/// ``` +#[macro_export] +macro_rules! cli_println { + ($use_stderr:expr, $($arg:tt)*) => { + if $use_stderr { + eprintln!($($arg)*); + } else { + println!($($arg)*); + } + }; +} + +#[cfg(test)] +mod tests { + #[test] + fn test_cli_println_compiles() { + // Test that the macro compiles with different argument patterns + cli_println!(true, "test"); + cli_println!(false, "test {}", "arg"); + cli_println!(true, "test {} {}", "arg1", "arg2"); + + // Test with variables + let use_stderr = true; + cli_println!(use_stderr, "variable test"); + + let use_stderr = false; + cli_println!(use_stderr, "variable test {}", "with arg"); + } +} diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 2bbc0e709051..56360a6020b0 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -47,6 +47,8 @@ pub struct SessionBuilderConfig { pub interactive: bool, /// Quiet mode - suppress non-response output pub quiet: bool, + /// Porcelain mode - redirect all CLI output to stderr and only output final text message to stdout + pub porcelain: bool, /// Sub-recipes to add to the session pub sub_recipes: Option>, } @@ -120,7 +122,8 @@ async fn offer_extension_debugging_help( std::env::temp_dir().join(format!("goose_debug_extension_{}.jsonl", extension_name)); // Create the debugging session - let mut debug_session = Session::new(debug_agent, temp_session_file.clone(), false, None); + let mut debug_session = + Session::new(debug_agent, temp_session_file.clone(), false, false, None); // Process the debugging request println!("{}", style("Analyzing the extension failure...").yellow()); @@ -189,7 +192,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { If your system is unable to use the keyring, please try setting secret key(s) via environment variables.\n\ For more info, see: https://block.github.io/goose/docs/troubleshooting/#keychainkeyring-errors", e - )); + ), session_config.porcelain); process::exit(1); } }; @@ -212,7 +215,10 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { .update_provider(new_provider) .await .unwrap_or_else(|e| { - output::render_error(&format!("Failed to initialize agent: {}", e)); + output::render_error( + &format!("Failed to initialize agent: {}", e), + session_config.porcelain, + ); process::exit(1); }); @@ -237,15 +243,21 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { let session_file = match session::get_path(identifier) { Ok(path) => path, Err(e) => { - output::render_error(&format!("Invalid session identifier: {}", e)); + output::render_error( + &format!("Invalid session identifier: {}", e), + session_config.porcelain, + ); process::exit(1); } }; if !session_file.exists() { - output::render_error(&format!( - "Cannot resume session {} - no such session exists", - style(session_file.display()).cyan() - )); + output::render_error( + &format!( + "Cannot resume session {} - no such session exists", + style(session_file.display()).cyan() + ), + session_config.porcelain, + ); process::exit(1); } @@ -255,7 +267,10 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { match session::get_most_recent_session() { Ok(file) => file, Err(_) => { - output::render_error("Cannot resume - no previous sessions found"); + output::render_error( + "Cannot resume - no previous sessions found", + session_config.porcelain, + ); process::exit(1); } } @@ -271,7 +286,10 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { match session::get_path(id) { Ok(path) => path, Err(e) => { - output::render_error(&format!("Failed to create session path: {}", e)); + output::render_error( + &format!("Failed to create session path: {}", e), + session_config.porcelain, + ); process::exit(1); } } @@ -280,7 +298,10 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { if session_config.resume && !session_config.no_session { // Read the session metadata let metadata = session::read_metadata(&session_file).unwrap_or_else(|e| { - output::render_error(&format!("Failed to read session metadata: {}", e)); + output::render_error( + &format!("Failed to read session metadata: {}", e), + session_config.porcelain, + ); process::exit(1); }); @@ -294,15 +315,18 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { if change_workdir { if !metadata.working_dir.exists() { - output::render_error(&format!( - "Cannot switch to original working directory - {} no longer exists", - style(metadata.working_dir.display()).cyan() - )); + output::render_error( + &format!( + "Cannot switch to original working directory - {} no longer exists", + style(metadata.working_dir.display()).cyan() + ), + session_config.porcelain, + ); } else if let Err(e) = std::env::set_current_dir(&metadata.working_dir) { - output::render_error(&format!( - "Failed to switch to original working directory: {}", - e - )); + output::render_error( + &format!("Failed to switch to original working directory: {}", e), + session_config.porcelain, + ); } } } @@ -365,6 +389,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { agent, session_file.clone(), session_config.debug, + session_config.porcelain, session_config.scheduled_job_id.clone(), ); @@ -492,6 +517,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { &model_name, &session_file, Some(&provider_for_display), + session_config.porcelain, ); } session @@ -518,6 +544,7 @@ mod tests { scheduled_job_id: None, interactive: true, quiet: false, + porcelain: false, sub_recipes: None, }; @@ -529,6 +556,7 @@ mod tests { assert!(config.scheduled_job_id.is_none()); assert!(config.interactive); assert!(!config.quiet); + assert!(!config.porcelain); } #[test] @@ -548,6 +576,7 @@ mod tests { assert!(config.scheduled_job_id.is_none()); assert!(!config.interactive); assert!(!config.quiet); + assert!(!config.porcelain); } #[tokio::test] diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 6f09c1ee653d..e74456132eb0 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -38,6 +38,8 @@ use std::sync::Arc; use std::time::Instant; use tokio; +use crate::cli_println; + pub enum RunMode { Normal, Plan, @@ -49,7 +51,8 @@ pub struct Session { session_file: PathBuf, // Cache for completion data - using std::sync for thread safety without async completion_cache: Arc>, - debug: bool, // New field for debug mode + debug: bool, + porcelain: bool, run_mode: RunMode, scheduled_job_id: Option, // ID of the scheduled job that triggered this session } @@ -112,6 +115,7 @@ impl Session { agent: Agent, session_file: PathBuf, debug: bool, + porcelain: bool, scheduled_job_id: Option, ) -> Self { let messages = match session::read_messages(&session_file) { @@ -128,6 +132,7 @@ impl Session { session_file, completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())), debug, + porcelain, run_mode: RunMode::Normal, scheduled_job_id, } @@ -138,11 +143,12 @@ impl Session { messages: &mut Vec, agent: &Agent, message_suffix: &str, + porcelain: bool, ) -> Result<()> { // Summarize messages to fit within context length let (summarized_messages, _) = agent.summarize_context(messages).await?; let msg = format!("Context maxed out\n{}\n{}", "-".repeat(50), message_suffix); - output::render_text(&msg, Some(Color::Yellow), true); + output::render_text(&msg, Some(Color::Yellow), true, porcelain); *messages = summarized_messages; Ok(()) @@ -395,7 +401,7 @@ impl Session { } }; - output::display_greeting(); + output::display_greeting(self.porcelain); loop { // Display context usage before each prompt self.display_context_usage().await?; @@ -441,7 +447,7 @@ impl Session { RunMode::Plan => { let mut plan_messages = self.messages.clone(); plan_messages.push(Message::user().with_text(&content)); - let reasoner = get_reasoner()?; + let reasoner = get_reasoner(self.porcelain)?; self.plan_with_reasoner_model(plan_messages, reasoner) .await?; } @@ -452,16 +458,20 @@ impl Session { save_history(&mut editor); match self.add_extension(cmd.clone()).await { - Ok(_) => output::render_extension_success(&cmd), - Err(e) => output::render_extension_error(&cmd, &e.to_string()), + Ok(_) => output::render_extension_success(&cmd, self.porcelain), + Err(e) => { + output::render_extension_error(&cmd, &e.to_string(), self.porcelain) + } } } input::InputResult::AddBuiltin(names) => { save_history(&mut editor); match self.add_builtin(names.clone()).await { - Ok(_) => output::render_builtin_success(&names), - Err(e) => output::render_builtin_error(&names, &e.to_string()), + Ok(_) => output::render_builtin_success(&names, self.porcelain), + Err(e) => { + output::render_builtin_error(&names, &e.to_string(), self.porcelain) + } } } input::InputResult::ToggleTheme => { @@ -470,15 +480,15 @@ impl Session { let current = output::get_theme(); let new_theme = match current { output::Theme::Light => { - println!("Switching to Dark theme"); + cli_println!(self.porcelain, "Switching to Dark theme"); output::Theme::Dark } output::Theme::Dark => { - println!("Switching to Ansi theme"); + cli_println!(self.porcelain, "Switching to Ansi theme"); output::Theme::Ansi } output::Theme::Ansi => { - println!("Switching to Light theme"); + cli_println!(self.porcelain, "Switching to Light theme"); output::Theme::Light } }; @@ -490,8 +500,8 @@ impl Session { save_history(&mut editor); match self.list_prompts(extension).await { - Ok(prompts) => output::render_prompts(&prompts), - Err(e) => output::render_error(&e.to_string()), + Ok(prompts) => output::render_prompts(&prompts, self.porcelain), + Err(e) => output::render_error(&e.to_string(), self.porcelain), } } input::InputResult::GooseMode(mode) => { @@ -502,22 +512,28 @@ impl Session { // Check if mode is valid if !["auto", "approve", "chat", "smart_approve"].contains(&mode.as_str()) { - output::render_error(&format!( - "Invalid mode '{}'. Mode must be one of: auto, approve, chat", - mode - )); + output::render_error( + &format!( + "Invalid mode '{}'. Mode must be one of: auto, approve, chat", + mode + ), + self.porcelain, + ); continue; } config .set_param("GOOSE_MODE", Value::String(mode.to_string())) .unwrap(); - output::goose_mode_message(&format!("Goose mode set to '{}'", mode)); + output::goose_mode_message( + &format!("Goose mode set to '{}'", mode), + self.porcelain, + ); continue; } input::InputResult::Plan(options) => { self.run_mode = RunMode::Plan; - output::render_enter_plan_mode(); + output::render_enter_plan_mode(self.porcelain); let message_text = options.message_text; if message_text.is_empty() { @@ -526,13 +542,13 @@ impl Session { let mut plan_messages = self.messages.clone(); plan_messages.push(Message::user().with_text(&message_text)); - let reasoner = get_reasoner()?; + let reasoner = get_reasoner(self.porcelain)?; self.plan_with_reasoner_model(plan_messages, reasoner) .await?; } input::InputResult::EndPlan => { self.run_mode = RunMode::Normal; - output::render_exit_plan_mode(); + output::render_exit_plan_mode(self.porcelain); continue; } input::InputResult::Clear => { @@ -543,6 +559,7 @@ impl Session { output::render_message( &Message::assistant().with_text("Chat context cleared."), self.debug, + self.porcelain, ); continue; } @@ -551,7 +568,11 @@ impl Session { self.handle_prompt_command(opts).await?; } InputResult::Recipe(filepath_opt) => { - println!("{}", console::style("Generating Recipe").green()); + cli_println!( + self.porcelain, + "{}", + console::style("Generating Recipe").green() + ); output::show_thinking(); let recipe = self.agent.create_recipe(self.messages.clone()).await; @@ -562,22 +583,23 @@ impl Session { // Use provided filepath or default let filepath_str = filepath_opt.as_deref().unwrap_or("recipe.yaml"); match self.save_recipe(&recipe, filepath_str) { - Ok(path) => println!( - "{}", - console::style(format!("Saved recipe to {}", path.display())) - .green() - ), + Ok(path) => { + let msg = format!("Saved recipe to {}", path.display()); + cli_println!( + self.porcelain, + "{}", + console::style(&msg).green() + ); + } Err(e) => { - println!("{}", console::style(e).red()); + let msg = format!("Failed to generate recipe: {:?}", e); + cli_println!(self.porcelain, "{}", console::style(&msg).red()); } } } Err(e) => { - println!( - "{}: {:?}", - console::style("Failed to generate recipe").red(), - e - ); + let msg = format!("Failed to generate recipe: {:?}", e); + output::render_text(&msg, Some(Color::Red), false, self.porcelain); } } @@ -600,7 +622,11 @@ impl Session { }; if should_summarize { - println!("{}", console::style("Summarizing conversation...").yellow()); + cli_println!( + self.porcelain, + "{}", + console::style("Summarizing conversation...").yellow() + ); output::show_thinking(); // Get the provider for summarization @@ -623,11 +649,13 @@ impl Session { .await?; output::hide_thinking(); - println!( + cli_println!( + self.porcelain, "{}", console::style("Conversation has been summarized.").green() ); - println!( + cli_println!( + self.porcelain, "{}", console::style( "Key information has been preserved while reducing context length." @@ -635,7 +663,11 @@ impl Session { .green() ); } else { - println!("{}", console::style("Summarization cancelled.").yellow()); + cli_println!( + self.porcelain, + "{}", + console::style("Summarization cancelled.").yellow() + ); } continue; @@ -643,10 +675,11 @@ impl Session { } } - println!( + let message = format!( "\nClosing session. Recorded to {}", self.session_file.display() ); + cli_println!(self.porcelain, "{}", message); Ok(()) } @@ -658,7 +691,7 @@ impl Session { let plan_prompt = self.agent.get_plan_prompt().await?; output::show_thinking(); let (plan_response, _usage) = reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; - output::render_message(&plan_response, self.debug); + output::render_message(&plan_response, self.debug, self.porcelain); output::hide_thinking(); let planner_response_type = classify_planner_response(plan_response.as_concat_text(), self.agent.provider().await?) @@ -666,7 +699,7 @@ impl Session { match planner_response_type { PlannerResponseType::Plan => { - println!(); + cli_println!(self.porcelain, ""); let should_act = match cliclack::confirm( "Do you want to clear message history & act on this plan?", ) @@ -683,7 +716,7 @@ impl Session { } }; if should_act { - output::render_act_on_plan(); + output::render_act_on_plan(self.porcelain); self.run_mode = RunMode::Normal; // set goose mode: auto if that isn't already the case let config = Config::global(); @@ -729,7 +762,12 @@ impl Session { /// Process a single message and exit pub async fn headless(&mut self, message: String) -> Result<()> { - self.process_message(message).await + let result = self.process_message(message).await; + + // Output final message if in porcelain mode + self.output_final_message(); + + result } async fn process_agent_response(&mut self, interactive: bool) -> Result<()> { @@ -784,7 +822,7 @@ impl Session { }; if permission == Permission::Cancel { - output::render_text("Tool call cancelled. Returning to chat...", Some(Color::Yellow), true); + output::render_text("Tool call cancelled. Returning to chat...", Some(Color::Yellow), true, self.porcelain); let mut response_message = Message::user(); response_message.content.push(MessageContent::tool_response( @@ -838,7 +876,7 @@ impl Session { } else { format!("Session cleared.\n{}", "-".repeat(50)) }; - output::render_text(&msg, Some(Color::Yellow), true); + output::render_text(&msg, Some(Color::Yellow), true, self.porcelain); break; // exit the loop to hand back control to the user } "truncate" => { @@ -849,8 +887,8 @@ impl Session { } else { format!("Context maxed out\n{}\nGoose tried its best to truncate messages for you.", "-".repeat(50)) }; - output::render_text("", Some(Color::Yellow), true); - output::render_text(&msg, Some(Color::Yellow), true); + output::render_text("", Some(Color::Yellow), true, self.porcelain); + output::render_text(&msg, Some(Color::Yellow), true, self.porcelain); self.messages = truncated_messages; } "summarize" => { @@ -862,7 +900,7 @@ impl Session { } else { "Goose automatically summarized messages to continue processing." }; - Self::summarize_context_messages(&mut self.messages, &self.agent, message_suffix).await?; + Self::summarize_context_messages(&mut self.messages, &self.agent, message_suffix, self.porcelain).await?; } _ => { unreachable!() @@ -893,7 +931,7 @@ impl Session { if interactive {output::hide_thinking()}; let _ = progress_bars.hide(); - output::render_message(&message, self.debug); + output::render_message(&message, self.debug, self.porcelain); if interactive {output::show_thinking()}; } } @@ -975,7 +1013,7 @@ impl Session { // Show subagent notifications immediately (no buffering) with compact spacing if interactive { let _ = progress_bars.hide(); - println!("{}", console::style(&formatted_message).green().dim()); + cli_println!(self.porcelain, "{}", console::style(&formatted_message).green().dim()); } else { progress_bars.log(&formatted_message); } @@ -983,7 +1021,7 @@ impl Session { // Non-subagent notification, display immediately with compact spacing if interactive { let _ = progress_bars.hide(); - println!("{}", console::style(&formatted_message).green().dim()); + cli_println!(self.porcelain, "{}", console::style(&formatted_message).green().dim()); } else { progress_bars.log(&formatted_message); } @@ -1027,6 +1065,7 @@ impl Session { These errors are often related to connection or authentication\n\ We've removed the conversation up to the most recent user message\n\ - depending on the error you may be able to continue", + self.porcelain, ); break; } @@ -1111,7 +1150,11 @@ impl Session { ) .await?; - output::render_message(&Message::assistant().with_text(&prompt), self.debug); + output::render_message( + &Message::assistant().with_text(&prompt), + self.debug, + self.porcelain, + ); } else { // An interruption occurred outside of a tool request-response. if let Some(last_msg) = self.messages.last() { @@ -1134,6 +1177,7 @@ impl Session { output::render_message( &Message::assistant().with_text(prompt), self.debug, + self.porcelain, ); } Some(_) => { @@ -1143,6 +1187,7 @@ impl Session { output::render_message( &Message::assistant().with_text(prompt), self.debug, + self.porcelain, ); } None => panic!("No content in last message"), @@ -1209,7 +1254,8 @@ impl Session { } // Print session restored message - println!( + cli_println!( + self.porcelain, "\n{} {} messages loaded into context.", console::style("Session restored:").green().bold(), console::style(self.messages.len()).green() @@ -1217,11 +1263,12 @@ impl Session { // Render each message for message in &self.messages { - output::render_message(message, self.debug); + output::render_message(message, self.debug, self.porcelain); } // Add a visual separator after restored messages - println!( + cli_println!( + self.porcelain, "\n{}\n", console::style("──────── New Messages ────────").dim() ); @@ -1251,11 +1298,10 @@ impl Session { match self.get_metadata() { Ok(metadata) => { let total_tokens = metadata.total_tokens.unwrap_or(0) as usize; - - output::display_context_usage(total_tokens, context_limit); + output::display_context_usage(total_tokens, context_limit, self.porcelain); } Err(_) => { - output::display_context_usage(0, context_limit); + output::display_context_usage(0, context_limit, self.porcelain); } } @@ -1266,14 +1312,17 @@ impl Session { async fn handle_prompt_command(&mut self, opts: input::PromptCommandOptions) -> Result<()> { // name is required if opts.name.is_empty() { - output::render_error("Prompt name argument is required"); + output::render_error("Prompt name argument is required", self.porcelain); return Ok(()); } if opts.info { match self.get_prompt_info(&opts.name).await? { - Some(info) => output::render_prompt_info(&info), - None => output::render_error(&format!("Prompt '{}' not found", opts.name)), + Some(info) => output::render_prompt_info(&info, self.porcelain), + None => output::render_error( + &format!("Prompt '{}' not found", opts.name), + self.porcelain, + ), } } else { // Convert the arguments HashMap to a Value @@ -1294,10 +1343,13 @@ impl Session { }; if msg.role != expected_role { - output::render_error(&format!( - "Expected {:?} message at position {}, but found {:?}", - expected_role, i, msg.role - )); + output::render_error( + &format!( + "Expected {:?} message at position {}, but found {:?}", + expected_role, i, msg.role + ), + self.porcelain, + ); valid = false; // get rid of everything we added to messages self.messages.truncate(start_len); @@ -1305,7 +1357,7 @@ impl Session { } if msg.role == mcp_core::Role::User { - output::render_message(&msg, self.debug); + output::render_message(&msg, self.debug, self.porcelain); } self.messages.push(msg); } @@ -1316,7 +1368,7 @@ impl Session { output::hide_thinking(); } } - Err(e) => output::render_error(&e.to_string()), + Err(e) => output::render_error(&e.to_string(), self.porcelain), } } @@ -1365,9 +1417,26 @@ impl Session { Ok(path) } + + pub fn output_final_message(&self) { + if self.porcelain { + // Find the last assistant message with text content + for message in self.messages.iter().rev() { + if message.role == mcp_core::role::Role::Assistant { + for content in &message.content { + if let MessageContent::Text(text) = content { + // Output to stdout without any formatting + println!("{}", text.text); + return; + } + } + } + } + } + } } -fn get_reasoner() -> Result, anyhow::Error> { +fn get_reasoner(porcelain: bool) -> Result, anyhow::Error> { use goose::model::ModelConfig; use goose::providers::create; @@ -1377,7 +1446,10 @@ fn get_reasoner() -> Result, anyhow::Error> { let provider = if let Ok(provider) = config.get_param::("GOOSE_PLANNER_PROVIDER") { provider } else { - println!("WARNING: GOOSE_PLANNER_PROVIDER not found. Using default provider..."); + cli_println!( + porcelain, + "WARNING: GOOSE_PLANNER_PROVIDER not found. Using default provider..." + ); config .get_param::("GOOSE_PROVIDER") .expect("No provider configured. Run 'goose configure' first") @@ -1387,7 +1459,10 @@ fn get_reasoner() -> Result, anyhow::Error> { let model = if let Ok(model) = config.get_param::("GOOSE_PLANNER_MODEL") { model } else { - println!("WARNING: GOOSE_PLANNER_MODEL not found. Using default model..."); + cli_println!( + porcelain, + "WARNING: GOOSE_PLANNER_MODEL not found. Using default model..." + ); config .get_param::("GOOSE_MODEL") .expect("No model configured. Run 'goose configure' first") diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 8bc26780677e..37cf479c8aac 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -13,6 +13,8 @@ use std::path::Path; use std::sync::Arc; use std::time::Duration; +use crate::cli_println; + // Re-export theme for use in main #[derive(Clone, Copy)] pub enum Theme { @@ -126,41 +128,50 @@ pub fn set_thinking_message(s: &String) { }); } -pub fn render_message(message: &Message, debug: bool) { +pub fn render_message(message: &Message, debug: bool, porcelain: bool) { let theme = get_theme(); for content in &message.content { match content { - MessageContent::Text(text) => print_markdown(&text.text, theme), - MessageContent::ToolRequest(req) => render_tool_request(req, theme, debug), - MessageContent::ToolResponse(resp) => render_tool_response(resp, theme, debug), + MessageContent::Text(text) => print_markdown(&text.text, theme, porcelain), + MessageContent::ToolRequest(req) => render_tool_request(req, theme, debug, porcelain), + MessageContent::ToolResponse(resp) => { + render_tool_response(resp, theme, debug, porcelain) + } MessageContent::Image(image) => { - println!("Image: [data: {}, type: {}]", image.data, image.mime_type); + cli_println!( + porcelain, + "Image: [data: {}, type: {}]", + image.data, + image.mime_type + ); } MessageContent::Thinking(thinking) => { if std::env::var("GOOSE_CLI_SHOW_THINKING").is_ok() { - println!("\n{}", style("Thinking:").dim().italic()); - print_markdown(&thinking.thinking, theme); + cli_println!(porcelain, "\n{}", style("Thinking:").dim().italic()); + print_markdown(&thinking.thinking, theme, porcelain); } } MessageContent::RedactedThinking(_) => { - // For redacted thinking, print thinking was redacted - println!("\n{}", style("Thinking:").dim().italic()); - print_markdown("Thinking was redacted", theme); + cli_println!(porcelain, "\n{}", style("Thinking:").dim().italic()); + print_markdown("Thinking was redacted", theme, porcelain); } _ => { - println!("WARNING: Message content type could not be rendered"); + cli_println!( + porcelain, + "WARNING: Message content type could not be rendered" + ); } } } - println!(); + cli_println!(porcelain, ""); } -pub fn render_text(text: &str, color: Option, dim: bool) { - render_text_no_newlines(format!("\n{}\n\n", text).as_str(), color, dim); +pub fn render_text(text: &str, color: Option, dim: bool, porcelain: bool) { + render_text_no_newlines(&format!("\n{}\n\n", text), color, dim, porcelain); } -pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { +pub fn render_text_no_newlines(text: &str, color: Option, dim: bool, porcelain: bool) { let mut styled_text = style(text); if dim { styled_text = styled_text.dim(); @@ -170,11 +181,13 @@ pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { } else { styled_text = styled_text.green(); } - print!("{}", styled_text); + + cli_println!(porcelain, "{}", styled_text); } -pub fn render_enter_plan_mode() { - println!( +pub fn render_enter_plan_mode(porcelain: bool) { + cli_println!( + porcelain, "\n{} {}\n", style("Entering plan mode.").green().bold(), style("You can provide instructions to create a plan and then act on it. To exit early, type /endplan") @@ -183,8 +196,9 @@ pub fn render_enter_plan_mode() { ); } -pub fn render_act_on_plan() { - println!( +pub fn render_act_on_plan(porcelain: bool) { + cli_println!( + porcelain, "\n{}\n", style("Exiting plan mode and acting on the above plan") .green() @@ -192,26 +206,30 @@ pub fn render_act_on_plan() { ); } -pub fn render_exit_plan_mode() { - println!("\n{}\n", style("Exiting plan mode.").green().bold()); +pub fn render_exit_plan_mode(porcelain: bool) { + cli_println!( + porcelain, + "\n{}\n", + style("Exiting plan mode.").green().bold() + ); } -pub fn goose_mode_message(text: &str) { - println!("\n{}", style(text).yellow(),); +pub fn goose_mode_message(text: &str, porcelain: bool) { + cli_println!(porcelain, "\n{}", style(text).yellow()); } -fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) { +fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool, porcelain: bool) { match &req.tool_call { Ok(call) => match call.name.as_str() { - "developer__text_editor" => render_text_editor_request(call, debug), - "developer__shell" => render_shell_request(call, debug), - _ => render_default_request(call, debug), + "developer__text_editor" => render_text_editor_request(call, debug, porcelain), + "developer__shell" => render_shell_request(call, debug, porcelain), + _ => render_default_request(call, debug, porcelain), }, - Err(e) => print_markdown(&e.to_string(), theme), + Err(e) => print_markdown(&e.to_string(), theme, porcelain), } } -fn render_tool_response(resp: &ToolResponse, theme: Theme, debug: bool) { +fn render_tool_response(resp: &ToolResponse, theme: Theme, debug: bool, porcelain: bool) { let config = Config::global(); match &resp.tool_result { @@ -237,46 +255,52 @@ fn render_tool_response(resp: &ToolResponse, theme: Theme, debug: bool) { } if debug { - println!("{:#?}", content); + cli_println!(porcelain, "{:#?}", content); } else if let mcp_core::content::Content::Text(text) = content { - print_markdown(&text.text, theme); + print_markdown(&text.text, theme, porcelain); } } } - Err(e) => print_markdown(&e.to_string(), theme), + Err(e) => print_markdown(&e.to_string(), theme, porcelain), } } -pub fn render_error(message: &str) { - println!("\n {} {}\n", style("error:").red().bold(), message); +pub fn render_error(message: &str, porcelain: bool) { + cli_println!( + porcelain, + "\n {} {}\n", + style("error:").red().bold(), + message + ); } -pub fn render_prompts(prompts: &HashMap>) { - println!(); +pub fn render_prompts(prompts: &HashMap>, porcelain: bool) { + cli_println!(porcelain, ""); for (extension, prompts) in prompts { - println!(" {}", style(extension).green()); + cli_println!(porcelain, " {}", style(extension).green()); for prompt in prompts { - println!(" - {}", style(prompt).cyan()); + cli_println!(porcelain, " - {}", style(prompt).cyan()); } } - println!(); + cli_println!(porcelain, ""); } -pub fn render_prompt_info(info: &PromptInfo) { - println!(); +pub fn render_prompt_info(info: &PromptInfo, porcelain: bool) { + cli_println!(porcelain, ""); if let Some(ext) = &info.extension { - println!(" {}: {}", style("Extension").green(), ext); + cli_println!(porcelain, " {}: {}", style("Extension").green(), ext); } - println!(" Prompt: {}", style(&info.name).cyan().bold()); + cli_println!(porcelain, " Prompt: {}", style(&info.name).cyan().bold()); if let Some(desc) = &info.description { - println!("\n {}", desc); + cli_println!(porcelain, "\n {}", desc); } if let Some(args) = &info.arguments { - println!("\n Arguments:"); + cli_println!(porcelain, "\n Arguments:"); + for arg in args { let required = arg.required.unwrap_or(false); let req_str = if required { @@ -285,7 +309,8 @@ pub fn render_prompt_info(info: &PromptInfo) { style("(optional)").dim() }; - println!( + cli_println!( + porcelain, " {} {} {}", style(&arg.name).yellow(), req_str, @@ -293,61 +318,67 @@ pub fn render_prompt_info(info: &PromptInfo) { ); } } - println!(); + + cli_println!(porcelain, ""); } -pub fn render_extension_success(name: &str) { - println!(); - println!( +pub fn render_extension_success(name: &str, porcelain: bool) { + cli_println!(porcelain, ""); + cli_println!( + porcelain, " {} extension `{}`", style("added").green(), style(name).cyan(), ); - println!(); + cli_println!(porcelain, ""); } -pub fn render_extension_error(name: &str, error: &str) { - println!(); - println!( +pub fn render_extension_error(name: &str, error: &str, porcelain: bool) { + cli_println!(porcelain, ""); + cli_println!( + porcelain, " {} to add extension {}", style("failed").red(), style(name).red() ); - println!(); - println!("{}", style(error).dim()); - println!(); + cli_println!(porcelain, ""); + cli_println!(porcelain, "{}", style(error).dim()); + cli_println!(porcelain, ""); } -pub fn render_builtin_success(names: &str) { - println!(); - println!( +pub fn render_builtin_success(names: &str, porcelain: bool) { + cli_println!(porcelain, ""); + cli_println!( + porcelain, " {} builtin{}: {}", style("added").green(), if names.contains(',') { "s" } else { "" }, style(names).cyan() ); - println!(); + cli_println!(porcelain, ""); } -pub fn render_builtin_error(names: &str, error: &str) { - println!(); - println!( +pub fn render_builtin_error(names: &str, error: &str, porcelain: bool) { + cli_println!(porcelain, ""); + cli_println!( + porcelain, " {} to add builtin{}: {}", style("failed").red(), if names.contains(',') { "s" } else { "" }, style(names).red() ); - println!(); - println!("{}", style(error).dim()); - println!(); + cli_println!(porcelain, ""); + cli_println!(porcelain, "{}", style(error).dim()); + cli_println!(porcelain, ""); } -fn render_text_editor_request(call: &ToolCall, debug: bool) { - print_tool_header(call); +fn render_text_editor_request(call: &ToolCall, debug: bool, porcelain: bool) { + print_tool_header(call, porcelain); // Print path first with special formatting if let Some(Value::String(path)) = call.arguments.get("path") { - println!( + cli_println!( + porcelain, "{}: {}", style("path").dim(), style(shorten_path(path, debug)).green() @@ -362,31 +393,36 @@ fn render_text_editor_request(call: &ToolCall, debug: bool) { other_args.insert(k.clone(), v.clone()); } } - print_params(&Value::Object(other_args), 0, debug); + print_params(&Value::Object(other_args), 0, debug, porcelain); } - println!(); + cli_println!(porcelain, ""); } -fn render_shell_request(call: &ToolCall, debug: bool) { - print_tool_header(call); +fn render_shell_request(call: &ToolCall, debug: bool, porcelain: bool) { + print_tool_header(call, porcelain); match call.arguments.get("command") { Some(Value::String(s)) => { - println!("{}: {}", style("command").dim(), style(s).green()); + cli_println!( + porcelain, + "{}: {}", + style("command").dim(), + style(s).green() + ); } - _ => print_params(&call.arguments, 0, debug), + _ => print_params(&call.arguments, 0, debug, porcelain), } } -fn render_default_request(call: &ToolCall, debug: bool) { - print_tool_header(call); - print_params(&call.arguments, 0, debug); - println!(); +fn render_default_request(call: &ToolCall, debug: bool, porcelain: bool) { + print_tool_header(call, porcelain); + print_params(&call.arguments, 0, debug, porcelain); + cli_println!(porcelain, ""); } // Helper functions -fn print_tool_header(call: &ToolCall) { +fn print_tool_header(call: &ToolCall, porcelain: bool) { let parts: Vec<_> = call.name.rsplit("__").collect(); let tool_header = format!( "─── {} | {} ──────────────────────────", @@ -400,8 +436,8 @@ fn print_tool_header(call: &ToolCall) { .magenta() .dim(), ); - println!(); - println!("{}", tool_header); + cli_println!(porcelain, ""); + cli_println!(porcelain, "{}", tool_header); } // Respect NO_COLOR, as https://crates.io/crates/console already does @@ -410,15 +446,29 @@ pub fn env_no_color() -> bool { std::env::var_os("NO_COLOR").is_none() } -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(); +fn print_markdown(content: &str, theme: Theme, porcelain: bool) { + if porcelain { + let mut stderr_buffer = String::new(); + 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_with_writer(Some(&mut stderr_buffer)) + .unwrap(); + eprint!("{}", stderr_buffer); + } else { + // Normal mode uses bat for markdown formatting + 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(); + } } const INDENT: &str = " "; @@ -430,7 +480,7 @@ fn get_tool_params_max_length() -> usize { .unwrap_or(40) } -fn print_params(value: &Value, depth: usize, debug: bool) { +fn print_params(value: &Value, depth: usize, debug: bool, porcelain: bool) { let indent = INDENT.repeat(depth); match value { @@ -438,60 +488,91 @@ fn print_params(value: &Value, depth: usize, debug: bool) { for (key, val) in map { match val { Value::Object(_) => { - println!("{}{}:", indent, style(key).dim()); - print_params(val, depth + 1, debug); + cli_println!(porcelain, "{}{}:", indent, style(key).dim()); + print_params(val, depth + 1, debug, porcelain); } Value::Array(arr) => { - println!("{}{}:", indent, style(key).dim()); + cli_println!(porcelain, "{}{}:", indent, style(key).dim()); for item in arr.iter() { - println!("{}{}- ", indent, INDENT); - print_params(item, depth + 2, debug); + cli_println!(porcelain, "{}{}- ", indent, INDENT); + print_params(item, depth + 2, debug, porcelain); } } Value::String(s) => { if !debug && s.len() > get_tool_params_max_length() { - println!("{}{}: {}", indent, style(key).dim(), style("...").dim()); + cli_println!( + porcelain, + "{}{}: {}", + indent, + style(key).dim(), + style("...").dim() + ); } else { - println!("{}{}: {}", indent, style(key).dim(), style(s).green()); + cli_println!( + porcelain, + "{}{}: {}", + indent, + style(key).dim(), + style(s).green() + ); } } Value::Number(n) => { - println!("{}{}: {}", indent, style(key).dim(), style(n).blue()); + cli_println!( + porcelain, + "{}{}: {}", + indent, + style(key).dim(), + style(n).blue() + ); } Value::Bool(b) => { - println!("{}{}: {}", indent, style(key).dim(), style(b).blue()); + cli_println!( + porcelain, + "{}{}: {}", + indent, + style(key).dim(), + style(b).blue() + ); } Value::Null => { - println!("{}{}: {}", indent, style(key).dim(), style("null").dim()); + cli_println!( + porcelain, + "{}{}: {}", + indent, + style(key).dim(), + style("null").dim() + ); } } } } Value::Array(arr) => { for (i, item) in arr.iter().enumerate() { - println!("{}{}.", indent, i + 1); - print_params(item, depth + 1, debug); + cli_println!(porcelain, "{}{}.", indent, i + 1); + print_params(item, depth + 1, debug, porcelain); } } Value::String(s) => { if !debug && s.len() > get_tool_params_max_length() { - println!( + cli_println!( + porcelain, "{}{}", indent, style(format!("[REDACTED: {} chars]", s.len())).yellow() ); } else { - println!("{}{}", indent, style(s).green()); + cli_println!(porcelain, "{}{}", indent, style(s).green()); } } Value::Number(n) => { - println!("{}{}", indent, style(n).yellow()); + cli_println!(porcelain, "{}{}", indent, style(n).yellow()); } Value::Bool(b) => { - println!("{}{}", indent, style(b).yellow()); + cli_println!(porcelain, "{}{}", indent, style(b).yellow()); } Value::Null => { - println!("{}{}", indent, style("null").dim()); + cli_println!(porcelain, "{}{}", indent, style("null").dim()); } } } @@ -552,6 +633,7 @@ pub fn display_session_info( model: &str, session_file: &Path, provider_instance: Option<&Arc>, + porcelain: bool, ) { let start_session_msg = if resume { "resuming session |" @@ -561,11 +643,11 @@ pub fn display_session_info( "starting session |" }; - // Check if we have lead/worker mode - if let Some(provider_inst) = provider_instance { - if let Some(lead_worker) = provider_inst.as_lead_worker() { + // Build the main session info string + let session_info = match provider_instance.and_then(|p| p.as_lead_worker()) { + Some(lead_worker) => { let (lead_model, worker_model) = lead_worker.get_model_info(); - println!( + format!( "{} {} {} {} {} {} {}", style(start_session_msg).dim(), style("provider:").dim(), @@ -574,56 +656,53 @@ pub fn display_session_info( style(&lead_model).cyan().dim(), style("worker model:").dim(), style(&worker_model).cyan().dim(), - ); - } else { - println!( + ) + } + None => { + format!( "{} {} {} {} {}", style(start_session_msg).dim(), style("provider:").dim(), style(provider).cyan().dim(), style("model:").dim(), style(model).cyan().dim(), - ); + ) } - } else { - // Fallback to original behavior if no provider instance - println!( - "{} {} {} {} {}", - style(start_session_msg).dim(), - style("provider:").dim(), - style(provider).cyan().dim(), - style("model:").dim(), - style(model).cyan().dim(), - ); - } + }; + cli_println!(porcelain, "{}", session_info); if session_file.to_str() != Some("/dev/null") && session_file.to_str() != Some("NUL") { - println!( + let logging_info = format!( " {} {}", style("logging to").dim(), style(session_file.display()).dim().cyan(), ); + cli_println!(porcelain, "{}", logging_info); } - println!( + let working_dir_info = format!( " {} {}", style("working directory:").dim(), style(std::env::current_dir().unwrap().display()) .cyan() .dim() ); + cli_println!(porcelain, "{}", working_dir_info); } -pub fn display_greeting() { - println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n"); +pub fn display_greeting(porcelain: bool) { + cli_println!( + porcelain, + "\nGoose is running! Enter your instructions, or try asking what goose can do.\n" + ); } /// Display context window usage with both current and session totals -pub fn display_context_usage(total_tokens: usize, context_limit: usize) { +pub fn display_context_usage(total_tokens: usize, context_limit: usize, porcelain: bool) { use console::style; if context_limit == 0 { - println!("Context: Error - context limit is zero"); + cli_println!(porcelain, "Context: Error - context limit is zero"); return; } @@ -651,9 +730,13 @@ pub fn display_context_usage(total_tokens: usize, context_limit: usize) { }; // Print the status line - println!( + cli_println!( + porcelain, "Context: {} {}% ({}/{} tokens)", - colored_dots, percentage, total_tokens, context_limit + colored_dots, + percentage, + total_tokens, + context_limit ); } diff --git a/documentation/docs/guides/goose-cli-commands.md b/documentation/docs/guides/goose-cli-commands.md index 4489b492e232..79378934ce14 100644 --- a/documentation/docs/guides/goose-cli-commands.md +++ b/documentation/docs/guides/goose-cli-commands.md @@ -293,6 +293,7 @@ Execute commands from an instruction file or stdin. Check out the [full guide](/ - **`--debug`**: Output complete tool responses, detailed parameter values, and full file paths - **`--explain`**: Show a recipe's title, description, and parameters - **`--no-session`**: Run goose commands without creating or storing a session file +- **`--porcelain`**: Porcelain mode. Redirect all CLI output to stderr and only output final text message to stdout **Usage:**