diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index 31cd86839d2..b19a02ea68c 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -410,10 +410,7 @@ impl Hinter for GooseCompleter { } HintStatus::Default => { let newline_key = super::input::get_newline_key().to_ascii_uppercase(); - Some(format!( - "Press Enter to send, Ctrl-{} for new line", - newline_key - )) + Some(format!("Enter to send ยท Ctrl+{} newline", newline_key)) } } } diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index dc7227859a3..11765998f91 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -398,18 +398,12 @@ fn parse_plan_command(input: String) -> Option { Some(InputResult::Plan(options)) } -/// Generates the input prompt string for the CLI interface. -/// Returns a styled prompt with the goose face "( O)>" followed by a space. -/// On Windows, returns plain text without ANSI styling for better compatibility. -/// On other platforms, applies styling using ANSI escape codes. fn get_input_prompt_string() -> String { - let goose = "( O)>"; + let goose = "๐Ÿชฟ"; if cfg!(target_os = "windows") { - // Use plain text on Windows to avoid ANSI compatibility issues format!("{goose} ") } else { - // On other platforms, use styled prompt with ANSI colors - format!("{} ", console::style(goose).cyan().bold()) + format!("{} ", console::style(goose)) } } @@ -702,38 +696,14 @@ mod tests { let prompt = get_input_prompt_string(); // Prompt should always end with a space - assert!(prompt.ends_with(" ")); + assert!(prompt.ends_with(' ')); - // Prompt should contain the goose face - assert!(prompt.contains("( O)>")); + // Prompt should contain the goose emoji + assert!(prompt.contains("๐Ÿชฟ")); - // On Windows, prompt should be plain text without ANSI codes #[cfg(target_os = "windows")] { - assert_eq!(prompt, "( O)> "); - // Ensure no ANSI escape sequences - assert!(!prompt.contains("\x1b[")); - } - - // On non-Windows, prompt behavior depends on terminal capabilities - #[cfg(not(target_os = "windows"))] - { - // In CI environments, console crate may strip ANSI codes - let is_ci = std::env::var("CI").is_ok(); - - if is_ci { - // In CI, just verify basic structure - console crate handles ANSI detection - assert!(prompt.len() >= "( O)> ".len()); - } else { - // In interactive terminals, expect styling to be applied - // Note: This may still vary based on terminal capabilities - assert!(prompt.len() >= "( O)> ".len()); - - // If ANSI codes are present, they should be valid - if prompt.contains("\x1b[") { - assert!(prompt.contains("36") || prompt.contains("1")); - } - } + assert_eq!(prompt, "๐Ÿชฟ "); } } } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index fec6ceadc1e..1757d85aeb0 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -159,7 +159,7 @@ pub struct CliSession { completion_cache: Arc>, debug: bool, run_mode: RunMode, - scheduled_job_id: Option, // ID of the scheduled job that triggered this session + scheduled_job_id: Option, max_turns: Option, edit_mode: Option, retry_config: Option, @@ -479,7 +479,6 @@ impl CliSession { loop { self.display_context_usage().await?; - // Convert conversation messages to strings for editor mode let conversation_strings: Vec = self .messages .iter() @@ -502,8 +501,9 @@ impl CliSession { } println!( - "Closing session. Session ID: {}", - console::style(&self.session_id).cyan() + "\n {} {}", + console::style("โ—").red(), + console::style(format!("session closed ยท {}", &self.session_id)).dim() ); Ok(()) @@ -636,10 +636,7 @@ impl CliSession { let elapsed = start_time.elapsed(); let elapsed_str = format_elapsed_time(elapsed); - println!( - "\n{}", - console::style(format!("โฑ๏ธ Elapsed time: {}", elapsed_str)).dim() - ); + println!("{}", console::style(format!(" โฑ {}", elapsed_str)).dim()); } RunMode::Plan => { let mut plan_messages = self.messages.clone(); @@ -1152,20 +1149,10 @@ impl CliSession { .collect() }); + let interrupt_prompt = "Yes โ€” what would you like me to do?"; + if !tool_requests.is_empty() { - // Interrupted during a tool request - // Create tool responses for all interrupted tool requests - // TODO(Douwe): if we need this, it should happen in agent reply let mut response_message = Message::user(); - let last_tool_name = tool_requests - .last() - .and_then(|(_, tool_call)| { - tool_call - .as_ref() - .ok() - .map(|tool| tool.name.to_string().clone()) - }) - .unwrap_or_else(|| "tool".to_string()); let notification = if interrupt { "Interrupted by the user to make a correction".to_string() @@ -1183,36 +1170,29 @@ impl CliSession { )); } self.push_message(response_message); - let prompt = format!( - "The existing call to {} was interrupted. How would you like to proceed?", - last_tool_name + self.push_message(Message::assistant().with_text(interrupt_prompt)); + output::render_message( + &Message::assistant().with_text(interrupt_prompt), + self.debug, ); - self.push_message(Message::assistant().with_text(&prompt)); - output::render_message(&Message::assistant().with_text(&prompt), self.debug); - } else { - // An interruption occurred outside of a tool request-response. - if let Some(last_msg) = self.messages.last() { - if last_msg.role == rmcp::model::Role::User { - match last_msg.content.first() { - Some(MessageContent::ToolResponse(_)) => { - // Interruption occurred after a tool had completed but not assistant reply - let prompt = "The tool calling loop was interrupted. How would you like to proceed?"; - self.push_message(Message::assistant().with_text(prompt)); - output::render_message( - &Message::assistant().with_text(prompt), - self.debug, - ); - } - Some(_) => { - // A real users message - self.messages.pop(); - let prompt = "Interrupted before the model replied and removed the last message."; - output::render_message( - &Message::assistant().with_text(prompt), - self.debug, - ); - } - None => panic!("No content in last message"), + } else if let Some(last_msg) = self.messages.last() { + if last_msg.role == rmcp::model::Role::User { + match last_msg.content.first() { + Some(MessageContent::ToolResponse(_)) => { + self.push_message(Message::assistant().with_text(interrupt_prompt)); + output::render_message( + &Message::assistant().with_text(interrupt_prompt), + self.debug, + ); + } + Some(_) => { + self.messages.pop(); + let assistant_msg = Message::assistant().with_text(interrupt_prompt); + self.push_message(assistant_msg.clone()); + output::render_message(&assistant_msg, self.debug); + } + None => { + // Empty message content โ€” nothing to do, just continue gracefully } } } @@ -1271,11 +1251,10 @@ impl CliSession { return; } - // Print session restored message println!( - "\n{} {} messages loaded into context.", - console::style("Session restored:").green().bold(), - console::style(self.messages.len()).green() + "\n {} {}", + console::style("โ†ป").cyan(), + console::style(format!("{} messages restored", self.messages.len())).dim() ); // Render each message @@ -1283,11 +1262,7 @@ impl CliSession { output::render_message(message, self.debug); } - // Add a visual separator after restored messages - println!( - "\n{}\n", - console::style("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ New Messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€").dim() - ); + println!(); } pub async fn get_session(&self) -> Result { diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index f1343e1bf39..a293349dc91 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -120,16 +120,18 @@ pub struct ThinkingIndicator { impl ThinkingIndicator { pub fn show(&mut self) { let spinner = cliclack::spinner(); + let hint = style("(Ctrl+C to interrupt)").dim(); if Config::global() .get_param("RANDOM_THINKING_MESSAGES") .unwrap_or(true) { spinner.start(format!( - "{}...", - super::thinking::get_random_thinking_message() + "{}... {}", + super::thinking::get_random_thinking_message(), + hint, )); } else { - spinner.start("Thinking..."); + spinner.start(format!("Thinking... {}", hint)); } self.spinner = Some(spinner); } @@ -467,17 +469,15 @@ pub fn render_builtin_error(names: &str, error: &str) { fn render_text_editor_request(call: &CallToolRequestParams, debug: bool) { print_tool_header(call); - // Print path first with special formatting if let Some(args) = &call.arguments { if let Some(Value::String(path)) = args.get("path") { println!( - "{}: {}", + " {} {}", style("path").dim(), - style(shorten_path(path, debug)).green() + style(shorten_path(path, debug)).dim() ); } - // Print other arguments normally, excluding path if let Some(args) = &call.arguments { let mut other_args = serde_json::Map::new(); for (k, v) in args { @@ -486,7 +486,7 @@ fn render_text_editor_request(call: &CallToolRequestParams, debug: bool) { } } if !other_args.is_empty() { - print_params(&Some(other_args), 0, debug); + print_params(&Some(other_args), 1, debug); } } } @@ -495,7 +495,7 @@ fn render_text_editor_request(call: &CallToolRequestParams, debug: bool) { fn render_shell_request(call: &CallToolRequestParams, debug: bool) { print_tool_header(call); - print_params(&call.arguments, 0, debug); + print_params(&call.arguments, 1, debug); println!(); } @@ -515,10 +515,11 @@ fn render_execute_code_request(call: &CallToolRequestParams, debug: bool) { let plural = if count == 1 { "" } else { "s" }; println!(); println!( - "โ”€โ”€โ”€ {} tool call{} | {} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", - style(count).cyan(), + " {} {} {} tool call{}", + style("โ–ธ").dim(), + style("execute").dim(), + style(count).dim(), plural, - style("execute").magenta().dim() ); for (i, node) in tool_graph.iter().filter_map(Value::as_object).enumerate() { @@ -544,10 +545,10 @@ fn render_execute_code_request(call: &CallToolRequestParams, debug: bool) { format!(" (uses {})", deps.join(", ")) }; println!( - " {}. {}: {}{}", + " {}. {} {}{}", style(i + 1).dim(), - style(tool).cyan(), - style(desc).green(), + style(tool).dim(), + style(desc).dim(), style(deps_str).dim() ); } @@ -570,7 +571,7 @@ fn render_delegate_request(call: &CallToolRequestParams, debug: bool) { if let Some(args) = &call.arguments { if let Some(Value::String(source)) = args.get("source") { - println!("{}: {}", style("source").dim(), style(source).cyan()); + println!(" {} {}", style("source").dim(), style(source).dim()); } if let Some(Value::String(instructions)) = args.get("instructions") { @@ -580,15 +581,15 @@ fn render_delegate_request(call: &CallToolRequestParams, debug: bool) { instructions.clone() }; println!( - "{}: {}", + " {} {}", style("instructions").dim(), - style(display).green() + style(display).dim() ); } if let Some(Value::Object(params)) = args.get("parameters") { - println!("{}:", style("parameters").dim()); - print_params(&Some(params.clone()), 1, debug); + println!(" {}:", style("parameters").dim()); + print_params(&Some(params.clone()), 2, debug); } let skip_keys = ["source", "instructions", "parameters"]; @@ -599,7 +600,7 @@ fn render_delegate_request(call: &CallToolRequestParams, debug: bool) { } } if !other_args.is_empty() { - print_params(&Some(other_args), 0, debug); + print_params(&Some(other_args), 1, debug); } } @@ -611,7 +612,7 @@ fn render_todo_request(call: &CallToolRequestParams, _debug: bool) { if let Some(args) = &call.arguments { if let Some(Value::String(content)) = args.get("content") { - println!("{}: {}", style("content").dim(), style(content).green()); + println!(" {} {}", style("content").dim(), style(content).dim()); } } println!(); @@ -619,7 +620,7 @@ fn render_todo_request(call: &CallToolRequestParams, _debug: bool) { fn render_default_request(call: &CallToolRequestParams, debug: bool) { print_tool_header(call); - print_params(&call.arguments, 0, debug); + print_params(&call.arguments, 1, debug); println!(); } @@ -660,14 +661,13 @@ pub fn render_subagent_tool_call( } } let tool_header = format!( - "โ”€โ”€โ”€ {} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", - style(format_subagent_tool_call_message(subagent_id, tool_name)) - .magenta() - .dim() + " {} {}", + style("โ–ธ").dim(), + style(format_subagent_tool_call_message(subagent_id, tool_name)).dim(), ); println!(); println!("{}", tool_header); - print_params(&arguments.cloned(), 0, debug); + print_params(&arguments.cloned(), 1, debug); println!(); } @@ -677,11 +677,12 @@ fn render_subagent_tool_graph(subagent_id: &str, tool_graph: &[Value]) { let plural = if count == 1 { "" } else { "s" }; println!(); println!( - "โ”€โ”€โ”€ {} {} tool call{} | {} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", - style(format!("[subagent:{}]", short_id)).cyan(), - style(count).cyan(), + " {} {} {} {} tool call{}", + style("โ–ธ").dim(), + style(format!("[subagent:{}]", short_id)).dim(), + style("execute_code").dim(), + style(count).dim(), plural, - style("execute_code").magenta().dim() ); for (i, node) in tool_graph.iter().filter_map(Value::as_object).enumerate() { @@ -707,10 +708,10 @@ fn render_subagent_tool_graph(subagent_id: &str, tool_graph: &[Value]) { format!(" (uses {})", deps.join(", ")) }; println!( - " {}. {}: {}{}", + " {}. {} {}{}", style(i + 1).dim(), - style(tool).cyan(), - style(desc).green(), + style(tool).dim(), + style(desc).dim(), style(deps_str).dim() ); } @@ -721,11 +722,16 @@ fn render_subagent_tool_graph(subagent_id: &str, tool_graph: &[Value]) { fn print_tool_header(call: &CallToolRequestParams) { let (tool, extension) = split_tool_name(&call.name); - let tool_header = format!( - "โ”€โ”€โ”€ {} | {} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€", - style(tool), - style(extension).magenta().dim(), - ); + let tool_header = if extension.is_empty() { + format!(" {} {}", style("โ–ธ").dim(), style(&tool).dim()) + } else { + format!( + " {} {} {}", + style("โ–ธ").dim(), + style(&tool).dim(), + style(extension).magenta().dim(), + ) + }; println!(); println!("{}", tool_header); } @@ -889,7 +895,6 @@ fn shorten_path(path: &str, debug: bool) -> String { shortened.join("/") } -// Session display functions pub fn display_session_info( resume: bool, provider: &str, @@ -897,107 +902,126 @@ pub fn display_session_info( session_id: &Option, provider_instance: Option<&Arc>, ) { - let start_session_msg = if resume { - "resuming session |" + let status = if resume { + "resuming" } else if session_id.is_none() { - "running without session |" + "ephemeral" } else { - "starting session |" + "new session" }; - // Check if we have lead/worker mode - if let Some(provider_inst) = provider_instance { + let model_display = if let Some(provider_inst) = provider_instance { if let Some(lead_worker) = provider_inst.as_lead_worker() { let (lead_model, worker_model) = lead_worker.get_model_info(); - println!( - "{} {} {} {} {} {} {}", - style(start_session_msg).dim(), - style("provider:").dim(), - style(provider).cyan().dim(), - style("lead model:").dim(), - style(&lead_model).cyan().dim(), - style("worker model:").dim(), - style(&worker_model).cyan().dim(), - ); + format!("{} โ†’ {}", lead_model, worker_model) } else { - println!( - "{} {} {} {} {}", - style(start_session_msg).dim(), - style("provider:").dim(), - style(provider).cyan().dim(), - style("model:").dim(), - style(model).cyan().dim(), - ); + model.to_string() } } 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(), - ); - } + model.to_string() + }; + + println!( + "\n {} {} {} {} {}", + style("โ—").green(), + style(status).dim(), + style("ยท").dim(), + style(provider).dim(), + style(&model_display).cyan(), + ); + + let cwd_display = std::env::current_dir() + .ok() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "unknown".to_string()); if let Some(id) = session_id { println!( - " {} {}", - style("session id:").dim(), - style(id).cyan().dim() + " {} {} {}", + style(" ").dim(), + style(id).dim(), + style(format!("ยท {}", cwd_display)).dim(), + ); + } else { + println!( + " {} {}", + style(" ").dim(), + style(format!(" {}", cwd_display)).dim(), ); } +} - println!( - " {} {}", - style("working directory:").dim(), - style(std::env::current_dir().unwrap().display()) - .cyan() - .dim() - ); +pub fn set_terminal_title() { + if !std::io::stdout().is_terminal() { + return; + } + let dir_name = std::env::current_dir() + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) + .unwrap_or_default(); + // Sanitize: strip control characters (ESC, BEL, etc.) to prevent terminal escape injection + let sanitized: String = dir_name.chars().filter(|c| !c.is_control()).collect(); + // OSC 0 sets the terminal window/tab title + print!("\x1b]0;๐Ÿชฟ {}\x07", sanitized); + let _ = std::io::stdout().flush(); } pub fn display_greeting() { - println!("\ngoose is running! Enter your instructions, or try asking what goose can do.\n"); + set_terminal_title(); + println!( + "\n{} {}\n", + style("๐Ÿชฟ goose").bold(), + style("ready โ€” type a message to get started").dim() + ); } -/// Display context window usage with both current and session totals pub fn display_context_usage(total_tokens: usize, context_limit: usize) { use console::style; if context_limit == 0 { - println!("Context: Error - context limit is zero"); + println!( + " {}", + style("context usage unavailable (context limit is 0)").dim() + ); return; } - // Calculate percentage used with bounds checking let percentage = (((total_tokens as f64 / context_limit as f64) * 100.0).round() as usize).min(100); - // Create dot visualization with safety bounds - let dot_count = 10; - let filled_dots = - (((percentage as f64 / 100.0) * dot_count as f64).round() as usize).min(dot_count); - let empty_dots = dot_count - filled_dots; - - let filled = "โ—".repeat(filled_dots); - let empty = "โ—‹".repeat(empty_dots); + let bar_width = 20; + let filled = ((percentage as f64 / 100.0) * bar_width as f64).round() as usize; + let empty = bar_width - filled.min(bar_width); - // Combine dots and apply color - let dots = format!("{}{}", filled, empty); - let colored_dots = if percentage < 50 { - style(dots).green() + let bar = format!("{}{}", "โ”".repeat(filled), "โ•Œ".repeat(empty)); + let colored_bar = if percentage < 50 { + style(bar).green().dim() } else if percentage < 85 { - style(dots).yellow() + style(bar).yellow() } else { - style(dots).red() + style(bar).red() }; - // Print the status line + fn format_tokens(n: usize) -> String { + if n >= 1_000_000 { + format!("{:.1}M", n as f64 / 1_000_000.0) + } else if n >= 1_000 { + format!("{:.0}k", n as f64 / 1_000.0) + } else { + n.to_string() + } + } + println!( - "Context: {} {}% ({}/{} tokens)", - colored_dots, percentage, total_tokens, context_limit + " {} {} {}", + colored_bar, + style(format!("{}%", percentage)).dim(), + style(format!( + "{}/{}", + format_tokens(total_tokens), + format_tokens(context_limit) + )) + .dim(), ); } diff --git a/scripts/test_mcp.sh b/scripts/test_mcp.sh index 85d72f9297b..cc3e6bafc15 100755 --- a/scripts/test_mcp.sh +++ b/scripts/test_mcp.sh @@ -57,7 +57,7 @@ TMPFILE=$(mktemp) (cd "$TESTDIR" && GOOSE_PROVIDER="$TEST_PROVIDER" GOOSE_MODEL="$TEST_MODEL" \ "$GOOSE_BIN" run --recipe recipe.yaml 2>&1) | tee "$TMPFILE" -if grep -q "add | test_mcp" "$TMPFILE" && grep -q "100" "$TMPFILE"; then +if grep -qE "(add \| test_mcp)|(โ–ธ.*add.*test_mcp)" "$TMPFILE" && grep -q "100" "$TMPFILE"; then echo "โœ“ FastMCP stderr test passed" RESULTS+=("โœ“ FastMCP stderr") else @@ -73,20 +73,20 @@ TESTDIR=$(mktemp -d) TMPFILE=$(mktemp) (cd "$TESTDIR" && GOOSE_PROVIDER="$TEST_PROVIDER" GOOSE_MODEL="$TEST_MODEL" \ - "$GOOSE_BIN" run --text "Use the sampleLLM tool to ask for a quote from The Great Gatsby" \ + "$GOOSE_BIN" run --text "Use the sampleLLM tool to ask for an original short poem about the ocean" \ --with-extension "npx -y @modelcontextprotocol/server-everything@2026.1.14" 2>&1) | tee "$TMPFILE" -if grep -q "$MCP_SAMPLING_TOOL | " "$TMPFILE"; then +if grep -qE "($MCP_SAMPLING_TOOL \| )|(โ–ธ.*$MCP_SAMPLING_TOOL)" "$TMPFILE"; then JUDGE_PROMPT=$(cat < "$result_file" fi else - if ! grep -q "text_editor | developer" "$output_file"; then + if ! grep -qE "(text_editor \| developer)|(โ–ธ.*text_editor.*developer)" "$output_file"; then echo "failure|model did not use text_editor tool" > "$result_file" elif ! grep -q "TEST-CONTENT-ABC123" "$output_file"; then echo "failure|model did not return uppercased file content" > "$result_file" diff --git a/scripts/test_providers_code_exec.sh b/scripts/test_providers_code_exec.sh index d0737c37cef..46e65495af0 100755 --- a/scripts/test_providers_code_exec.sh +++ b/scripts/test_providers_code_exec.sh @@ -30,8 +30,9 @@ run_test() { # Verify: code_execution tool must be called # Matches: "execute | code_execution", "get_function_details | code_execution", - # "tool call | execute", "tool calls | execute" - if grep -qE "(execute \| code_execution)|(get_function_details \| code_execution)|(tool calls? \| execute)" "$output_file"; then + # "tool call | execute", "tool calls | execute" (old format) + # "โ–ธ execute N tool call" (new format with tool_graph) + if grep -qE "(execute \| code_execution)|(get_function_details \| code_execution)|(tool calls? \| execute)|(โ–ธ.*execute.*tool call)" "$output_file"; then echo "success|code_execution tool called" > "$result_file" else echo "failure|no code_execution tool calls found" > "$result_file" diff --git a/scripts/test_subrecipes.sh b/scripts/test_subrecipes.sh index 4e48f749686..3ac8370a3c2 100755 --- a/scripts/test_subrecipes.sh +++ b/scripts/test_subrecipes.sh @@ -77,8 +77,8 @@ check_recipe_output() { local tmpfile=$1 local mode=$2 - # Check for delegate tool invocation (new format: "โ”€โ”€โ”€ delegate |") - if grep -q "โ”€โ”€โ”€ delegate" "$tmpfile"; then + # Check for delegate tool invocation (old: "โ”€โ”€โ”€ delegate |", new: "โ–ธ delegate") + if grep -qE "(โ”€โ”€โ”€ delegate)|(โ–ธ.*delegate)" "$tmpfile"; then echo "โœ“ SUCCESS: Delegate tool invoked" RESULTS+=("โœ“ Delegate tool invocation ($mode)") else