From 52fdbb1d72f7324a28142a7ba7678d4b53c902f4 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Mon, 13 Jul 2026 17:29:23 +1000 Subject: [PATCH] refactor(cli): clarify terminal color semantics (#10414) Co-authored-by: michaelneale --- crates/goose-cli/src/session/input.rs | 2 +- crates/goose-cli/src/session/output.rs | 75 +++++++++++++------------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 6b9da991a..e7e14f980 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -150,7 +150,7 @@ pub fn get_input( rustyline::EventHandler::Conditional(Box::new(CtrlCHandler::new(completion_cache))), ); - let input = match editor.readline("> ") { + let input = match editor.readline("› ") { Ok(text) => text, Err(e) => match e { rustyline::error::ReadlineError::Interrupted => return Ok(InputResult::Exit), diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 17648a089..fc0f253bc 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -1,6 +1,6 @@ use anstream::println; use bat::WrappingMode; -use console::{measure_text_width, style, Color, Term}; +use console::{measure_text_width, style, Color, StyledObject, Term}; use goose::config::Config; use goose::conversation::message::{ ActionRequiredData, Message, MessageContent, SystemNotificationContent, SystemNotificationType, @@ -16,6 +16,7 @@ use rmcp::model::{CallToolRequestParams, JsonObject, PromptArgument}; use serde_json::Value; use std::cell::RefCell; use std::collections::HashMap; +use std::fmt::Display; use std::io::{Error, IsTerminal, Write}; use std::path::Path; use std::time::Duration; @@ -26,6 +27,22 @@ pub const DEFAULT_MIN_PRIORITY: f32 = 0.0; pub const DEFAULT_CLI_LIGHT_THEME: &str = "GitHub"; pub const DEFAULT_CLI_DARK_THEME: &str = "zenburn"; +fn accent(value: T) -> StyledObject { + style(value).cyan() +} + +fn success(value: T) -> StyledObject { + style(value).green() +} + +fn warning(value: T) -> StyledObject { + style(value).yellow() +} + +fn danger(value: T) -> StyledObject { + style(value).red() +} + // Re-export theme for use in main #[derive(Clone, Copy)] pub enum Theme { @@ -255,7 +272,7 @@ pub fn render_message(message: &Message, debug: bool) { } SystemNotificationType::InlineMessage => { hide_thinking(); - println!("\n{}", style(¬ification.msg).yellow()); + println!("\n{} {}", style("·").dim(), ¬ification.msg); } SystemNotificationType::CreditsExhausted => { render_credits_exhausted_notification(notification); @@ -339,7 +356,7 @@ pub fn render_message_streaming( SystemNotificationType::InlineMessage => { flush_markdown_buffer(buffer, theme); hide_thinking(); - println!("\n{}", style(¬ification.msg).yellow()); + println!("\n{} {}", style("·").dim(), ¬ification.msg); } SystemNotificationType::CreditsExhausted => { flush_markdown_buffer(buffer, theme); @@ -359,7 +376,7 @@ pub fn render_message_streaming( fn render_credits_exhausted_notification(notification: &SystemNotificationContent) { hide_thinking(); - println!("\n{}", style(¬ification.msg).yellow()); + println!("\n{} {}", warning("warning:").bold(), ¬ification.msg); if let Some(url) = notification .data @@ -367,10 +384,7 @@ fn render_credits_exhausted_notification(notification: &SystemNotificationConten .and_then(|d| d.get("top_up_url")) .and_then(|v| v.as_str()) { - println!( - "{}", - style(format!("Visit this URL to top up credits: {url}")).yellow() - ); + println!("{} {}", style("top up:").dim(), accent(url)); } } @@ -417,8 +431,6 @@ pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { } if let Some(color) = color { styled_text = styled_text.fg(color); - } else { - styled_text = styled_text.green(); } print!("{}", styled_text); } @@ -426,9 +438,8 @@ pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { pub fn render_enter_plan_mode() { println!( "\n{} {}\n", - style("Entering plan mode.").green().bold(), + accent("Entering plan mode.").bold(), style("You can provide instructions to create a plan and then act on it. To exit early, type /endplan") - .green() .dim() ); } @@ -436,18 +447,16 @@ pub fn render_enter_plan_mode() { pub fn render_act_on_plan() { println!( "\n{}\n", - style("Exiting plan mode and acting on the above plan") - .green() - .bold(), + accent("Exiting plan mode and acting on the above plan").bold(), ); } pub fn render_exit_plan_mode() { - println!("\n{}\n", style("Exiting plan mode.").green().bold()); + println!("\n{}\n", accent("Exiting plan mode.").bold()); } pub fn goose_mode_message(text: &str) { - println!("\n{}", style(text).yellow(),); + println!("\n{} {}", accent("mode:"), text); } fn should_show_thinking() -> bool { @@ -583,13 +592,13 @@ fn is_file_tool_name(name: &str) -> bool { } pub fn render_error(message: &str) { - println!("\n {} {}\n", style("error:").red().bold(), message); + println!("\n {} {}\n", danger("error:").bold(), message); } pub fn render_prompts(prompts: &HashMap>) { println!(); for (extension, prompts) in prompts { - println!(" {}", style(extension).green()); + println!(" {}", accent(extension)); for prompt in prompts { println!(" - {}", style(prompt).cyan()); } @@ -600,7 +609,7 @@ 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!(" {}: {}", accent("Extension"), ext); } println!(" Prompt: {}", style(&info.name).cyan().bold()); if let Some(desc) = &info.description { @@ -616,14 +625,14 @@ fn render_arguments(info: &PromptInfo) { for arg in args { let required = arg.required.unwrap_or(false); let req_str = if required { - style("(required)").red() + style("(required)").bold() } else { style("(optional)").dim() }; println!( " {} {} {}", - style(&arg.name).yellow(), + accent(&arg.name), req_str, arg.description.as_deref().unwrap_or("") ); @@ -633,21 +642,13 @@ fn render_arguments(info: &PromptInfo) { pub fn render_extension_success(name: &str) { println!(); - println!( - " {} extension `{}`", - style("added").green(), - style(name).cyan(), - ); + println!(" {} extension `{}`", success("added"), accent(name),); println!(); } pub fn render_extension_error(name: &str, error: &str) { println!(); - println!( - " {} to add extension {}", - style("failed").red(), - style(name).red() - ); + println!(" {} to add extension {}", danger("failed"), danger(name)); println!(); println!("{}", style(error).dim()); println!(); @@ -657,9 +658,9 @@ pub fn render_builtin_success(names: &str) { println!(); println!( " {} builtin{}: {}", - style("added").green(), + success("added"), if names.contains(',') { "s" } else { "" }, - style(names).cyan() + accent(names) ); println!(); } @@ -668,9 +669,9 @@ pub fn render_builtin_error(names: &str, error: &str) { println!(); println!( " {} to add builtin{}: {}", - style("failed").red(), + danger("failed"), if names.contains(',') { "s" } else { "" }, - style(names).red() + danger(names) ); println!(); println!("{}", style(error).dim()); @@ -771,7 +772,7 @@ fn render_execute_code_request(call: &CallToolRequestParams, debug: bool) { .and_then(Value::as_str) .filter(|c| !c.is_empty()); if code.is_some_and(|_| debug) { - println!("{}", style(code.unwrap_or_default()).green()); + println!("{}", code.unwrap_or_default()); } println!();