nit: show dir in title, and less... jank (#7138)
Some checks failed
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Cargo Deny / deny (push) Waiting to run
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled

This commit is contained in:
Michael Neale 2026-02-13 15:16:46 +11:00 committed by GitHub
parent 860c7d7b97
commit 85348d2745
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 183 additions and 216 deletions

View file

@ -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))
}
}
}

View file

@ -398,18 +398,12 @@ fn parse_plan_command(input: String) -> Option<InputResult> {
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, "🪿 ");
}
}
}

View file

@ -159,7 +159,7 @@ pub struct CliSession {
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
debug: bool,
run_mode: RunMode,
scheduled_job_id: Option<String>, // ID of the scheduled job that triggered this session
scheduled_job_id: Option<String>,
max_turns: Option<u32>,
edit_mode: Option<EditMode>,
retry_config: Option<RetryConfig>,
@ -479,7 +479,6 @@ impl CliSession {
loop {
self.display_context_usage().await?;
// Convert conversation messages to strings for editor mode
let conversation_strings: Vec<String> = 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<goose::session::Session> {

View file

@ -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<String>,
provider_instance: Option<&Arc<dyn goose::providers::base::Provider>>,
) {
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 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);
let filled = "".repeat(filled_dots);
let empty = "".repeat(empty_dots);
// 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(),
);
}

View file

@ -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 <<EOF
You are a validator. You will be given a transcript of a CLI run that used an MCP tool to initiate MCP sampling.
The MCP server requests a quote from The Great Gatsby from the model via sampling.
The MCP server requests an original short poem about the ocean from the model via sampling.
Task: Determine whether the transcript shows that the sampling request reached the model and that the output included either:
• A recognizable quote, paraphrase, or reference from The Great Gatsby, or
• A clear attempt or explanation from the model about why the quote could not be returned.
• A poem, verse, or creative text about the ocean or sea, or
• A clear attempt or explanation from the model about the poem request.
If either of these conditions is true, respond PASS.
If there is no evidence that the model attempted or returned a Gatsby-related response, respond FAIL.
If there is no evidence that the model attempted or returned a poem-related response, respond FAIL.
If uncertain, lean toward PASS.
Output format: Respond with exactly one word on a single line:

View file

@ -40,7 +40,7 @@ run_test() {
echo "failure|test content not found by model" > "$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"

View file

@ -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"

View file

@ -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