From ce72b0cbc481ecccdd1b05f31b8cba93bf6142f2 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 31 May 2026 02:52:24 -0700 Subject: [PATCH] fix(tui): clarify shell tool availability errors (#2412) Harvested from #2402 with thanks to @axobase001. Keeps `allow_shell` guidance visible for gated shell tools even when missing-tool suggestions exist, removes the nonexistent task_shell_cancel matcher, and broadens regression coverage. Partially addresses #2328. --- crates/tui/src/core/engine/tests.rs | 38 +++++++++++++++++++++ crates/tui/src/core/engine/tool_catalog.rs | 39 ++++++++++++++++++++-- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 06ae4030..f7397361 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -2359,6 +2359,44 @@ fn missing_tool_error_message_includes_discovery_guidance_when_no_match() { assert!(message.contains(TOOL_SEARCH_BM25_NAME)); } +#[test] +fn missing_shell_tool_error_message_names_allow_shell_gate() { + let catalog = vec![api_tool("read_file")]; + + for tool_name in [ + "exec_shell", + "exec_shell_wait", + "exec_shell_interact", + "task_shell_start", + "task_shell_wait", + ] { + let message = missing_tool_error_message(tool_name, &catalog); + assert!(message.contains("not available in the current tool catalog")); + assert!(message.contains("allow_shell"), "{tool_name}: {message}"); + assert!( + message.contains("trusted workspaces"), + "{tool_name}: {message}" + ); + assert!( + message.contains(TOOL_SEARCH_BM25_NAME), + "{tool_name}: {message}" + ); + } +} + +#[test] +fn missing_shell_tool_error_message_keeps_allow_shell_hint_with_suggestions() { + let catalog = vec![api_tool("exec")]; + + let message = missing_tool_error_message("exec_shell", &catalog); + + assert!(message.contains("Did you mean:")); + assert!(message.contains("exec")); + assert!(message.contains("allow_shell")); + assert!(message.contains("trusted workspaces")); + assert!(message.contains(TOOL_SEARCH_BM25_NAME)); +} + #[test] fn filter_tool_call_delta_strips_bracket_marker() { let mut in_block = false; diff --git a/crates/tui/src/core/engine/tool_catalog.rs b/crates/tui/src/core/engine/tool_catalog.rs index 867c6895..51789632 100644 --- a/crates/tui/src/core/engine/tool_catalog.rs +++ b/crates/tui/src/core/engine/tool_catalog.rs @@ -438,17 +438,52 @@ fn suggest_tool_names(catalog: &[Tool], requested: &str, limit: usize) -> Vec String { let suggestions = suggest_tool_names(catalog, tool_name, 3); + let shell_hint = if is_shell_tool_name(tool_name) { + Some(shell_tool_allow_shell_hint()) + } else { + None + }; if suggestions.is_empty() { + if let Some(shell_hint) = shell_hint { + return format!( + "Tool '{tool_name}' is not available in the current tool catalog. \ + {shell_hint}, or use {TOOL_SEARCH_BM25_NAME} with a short query." + ); + } return format!( "Tool '{tool_name}' is not available in the current tool catalog. \ Verify mode/feature flags, or use {TOOL_SEARCH_BM25_NAME} with a short query." ); } + let suggestion_text = format!("Did you mean: {}?", suggestions.join(", ")); + if let Some(shell_hint) = shell_hint { + return format!( + "Tool '{tool_name}' is not available in the current tool catalog. \ + {suggestion_text} {shell_hint}. \ + You can also use {TOOL_SEARCH_BM25_NAME} to discover tools." + ); + } + format!( "Tool '{tool_name}' is not available in the current tool catalog. \ - Did you mean: {}? You can also use {TOOL_SEARCH_BM25_NAME} to discover tools.", - suggestions.join(", ") + {suggestion_text} You can also use {TOOL_SEARCH_BM25_NAME} to discover tools." + ) +} + +fn shell_tool_allow_shell_hint() -> &'static str { + "Shell tools are gated by `allow_shell`; enable `allow_shell = true` for trusted workspaces, \ + or switch to an auto-approve mode that permits shell access" +} + +fn is_shell_tool_name(tool_name: &str) -> bool { + matches!( + tool_name, + "exec_shell" + | "exec_shell_wait" + | "exec_shell_interact" + | "task_shell_start" + | "task_shell_wait" ) }