From 738a26519702f281d2579b4317f8dd9423fd5aaa Mon Sep 17 00:00:00 2001 From: CodeWhale Agent Date: Fri, 12 Jun 2026 16:32:32 -0700 Subject: [PATCH] fix(tui): show Bash for shell work in visible UI --- crates/tui/src/localization.rs | 16 +++++------ crates/tui/src/tools/subagent/mod.rs | 18 +++++++++++-- crates/tui/src/tools/subagent/tests.rs | 14 ++++++++++ crates/tui/src/tui/shell_job_routing.rs | 10 ++++--- crates/tui/src/tui/sidebar.rs | 36 ++++++++++++------------- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index a829615b..455de7ce 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -1370,7 +1370,7 @@ fn english(id: MessageId) -> &'static str { MessageId::CmdInitDescription => "Generate AGENTS.md for project", MessageId::CmdLspDescription => "Toggle LSP diagnostics on or off", MessageId::CmdShareDescription => "Export current session as a shareable web URL", - MessageId::CmdJobsDescription => "Inspect and control background commands", + MessageId::CmdJobsDescription => "Inspect and control background Bash jobs", MessageId::CmdLinksDescription => "Show DeepSeek dashboard and docs links", MessageId::CmdLoadDescription => "Load session from file", MessageId::CmdLogoutDescription => "Clear API key and return to setup", @@ -1700,7 +1700,7 @@ fn english(id: MessageId) -> &'static str { MessageId::ApprovalRiskDestructive => "DESTRUCTIVE", MessageId::ApprovalCategorySafe => "Safe", MessageId::ApprovalCategoryFileWrite => "File Write", - MessageId::ApprovalCategoryShell => "Shell Command", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "Network", MessageId::ApprovalCategoryMcpRead => "MCP Read", MessageId::ApprovalCategoryMcpAction => "MCP Action", @@ -2327,7 +2327,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> { MessageId::ApprovalRiskDestructive => "NGUY HẠI", MessageId::ApprovalCategorySafe => "An toàn", MessageId::ApprovalCategoryFileWrite => "Ghi Tệp", - MessageId::ApprovalCategoryShell => "Lệnh Shell", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "Mạng", MessageId::ApprovalCategoryMcpRead => "Đọc MCP", MessageId::ApprovalCategoryMcpAction => "Hành động MCP", @@ -2484,7 +2484,7 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> { MessageId::ApprovalRiskDestructive => "破壞性", MessageId::ApprovalCategorySafe => "安全", MessageId::ApprovalCategoryFileWrite => "檔案寫入", - MessageId::ApprovalCategoryShell => "Shell 命令", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "網路", MessageId::ApprovalCategoryMcpRead => "MCP 讀取", MessageId::ApprovalCategoryMcpAction => "MCP 操作", @@ -3090,7 +3090,7 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::ApprovalRiskDestructive => "破壊的操作", MessageId::ApprovalCategorySafe => "安全", MessageId::ApprovalCategoryFileWrite => "ファイル書き込み", - MessageId::ApprovalCategoryShell => "シェルコマンド", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "ネットワーク", MessageId::ApprovalCategoryMcpRead => "MCP読み取り", MessageId::ApprovalCategoryMcpAction => "MCPアクション", @@ -3619,7 +3619,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::ApprovalRiskDestructive => "破坏性", MessageId::ApprovalCategorySafe => "安全", MessageId::ApprovalCategoryFileWrite => "文件写入", - MessageId::ApprovalCategoryShell => "Shell 命令", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "网络", MessageId::ApprovalCategoryMcpRead => "MCP 读取", MessageId::ApprovalCategoryMcpAction => "MCP 操作", @@ -4214,7 +4214,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::ApprovalRiskDestructive => "DESTRUTIVO", MessageId::ApprovalCategorySafe => "Seguro", MessageId::ApprovalCategoryFileWrite => "Escrita de Arquivo", - MessageId::ApprovalCategoryShell => "Comando Shell", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "Rede", MessageId::ApprovalCategoryMcpRead => "Leitura MCP", MessageId::ApprovalCategoryMcpAction => "Ação MCP", @@ -4835,7 +4835,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> { MessageId::ApprovalRiskDestructive => "DESTRUCTIVO", MessageId::ApprovalCategorySafe => "Seguro", MessageId::ApprovalCategoryFileWrite => "Escritura de Archivo", - MessageId::ApprovalCategoryShell => "Comando Shell", + MessageId::ApprovalCategoryShell => "Bash", MessageId::ApprovalCategoryNetwork => "Red", MessageId::ApprovalCategoryMcpRead => "Lectura MCP", MessageId::ApprovalCategoryMcpAction => "Acción MCP", diff --git a/crates/tui/src/tools/subagent/mod.rs b/crates/tui/src/tools/subagent/mod.rs index d1f9d661..e269cec0 100644 --- a/crates/tui/src/tools/subagent/mod.rs +++ b/crates/tui/src/tools/subagent/mod.rs @@ -5108,11 +5108,12 @@ async fn run_subagent( ); let mut tool_results: Vec = Vec::new(); for (tool_id, tool_name, tool_input) in tool_uses { + let tool_display_name = subagent_progress_tool_display_name(&tool_name); record_agent_progress( runtime, &agent_id, format!( - "{}: running tool '{tool_name}'", + "{}: running tool '{tool_display_name}'", format_step_counter(steps, max_steps) ), ); @@ -5139,7 +5140,7 @@ async fn run_subagent( runtime, &agent_id, format!( - "{}: finished tool '{tool_name}'", + "{}: finished tool '{tool_display_name}'", format_step_counter(steps, max_steps) ), ); @@ -6046,6 +6047,19 @@ fn parse_progress_tool_name(message: &str) -> Option { (!tool.is_empty()).then(|| tool.to_string()) } +fn subagent_progress_tool_display_name(name: &str) -> &str { + match name { + "exec_shell" + | "exec_shell_wait" + | "exec_shell_interact" + | "exec_wait" + | "exec_interact" + | "task_shell_start" + | "task_shell_wait" => "Bash", + _ => name, + } +} + fn emit_agent_progress( event_tx: Option<&mpsc::Sender>, mailbox: Option<&Mailbox>, diff --git a/crates/tui/src/tools/subagent/tests.rs b/crates/tui/src/tools/subagent/tests.rs index d3d26666..8b0fa8eb 100644 --- a/crates/tui/src/tools/subagent/tests.rs +++ b/crates/tui/src/tools/subagent/tests.rs @@ -106,6 +106,20 @@ fn headless_worker_record_tracks_lifecycle_without_tui_projection() { ); } +#[test] +fn subagent_progress_displays_shell_tools_as_bash() { + assert_eq!(subagent_progress_tool_display_name("exec_shell"), "Bash"); + assert_eq!(subagent_progress_tool_display_name("exec_wait"), "Bash"); + assert_eq!( + subagent_progress_tool_display_name("task_shell_wait"), + "Bash" + ); + assert_eq!( + subagent_progress_tool_display_name("read_file"), + "read_file" + ); +} + #[test] fn headless_worker_records_persist_with_subagent_state() { let tmp = tempdir().expect("tempdir"); diff --git a/crates/tui/src/tui/shell_job_routing.rs b/crates/tui/src/tui/shell_job_routing.rs index 4070fafc..571b17d6 100644 --- a/crates/tui/src/tui/shell_job_routing.rs +++ b/crates/tui/src/tui/shell_job_routing.rs @@ -31,11 +31,11 @@ fn format_elapsed(ms: u64) -> String { pub(super) fn format_shell_job_list(jobs: &[ShellJobSnapshot]) -> String { if jobs.is_empty() { - return "No live background commands. Commands are process-local; after a restart, inspect durable task artifacts for prior command output.".to_string(); + return "No live Bash jobs. Bash jobs are process-local; after a restart, inspect durable task artifacts for prior command output.".to_string(); } let mut lines = vec![ - format!("Background commands ({})", jobs.len()), + format!("Bash jobs ({})", jobs.len()), "----------------------------------------".to_string(), ]; for job in jobs { @@ -73,7 +73,7 @@ pub(super) fn format_shell_job_list(jobs: &[ShellJobSnapshot]) -> String { pub(super) fn format_shell_poll(result: &ShellResult) -> String { let mut lines = vec![ format!( - "Command {}: {} exit={:?} elapsed={}", + "Bash job {}: {} exit={:?} elapsed={}", result.task_id.as_deref().unwrap_or("(unknown)"), status_label(&result.status, false), result.exit_code, @@ -104,7 +104,7 @@ pub(super) fn open_shell_job_pager(app: &mut App, detail: &ShellJobDetail) { .unwrap_or(100) .saturating_sub(4); app.view_stack.push(PagerView::from_text( - format!("Shell Job {}", detail.snapshot.id), + format!("Bash Job {}", detail.snapshot.id), &format_shell_job_detail(detail), width.max(60), )); @@ -174,6 +174,8 @@ mod tests { linked_task_id: Some("task_1".to_string()), }]; let formatted = format_shell_job_list(&jobs); + assert!(formatted.contains("Bash jobs (1)")); + assert!(!formatted.contains("Background commands")); assert!(formatted.contains("shell_dead")); assert!(formatted.contains("stale")); assert!(formatted.contains("/jobs poll ")); diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index 86982709..c09e40e4 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -859,11 +859,11 @@ fn task_panel_rows( .count(); let done = background_rows.len().saturating_sub(running); let label = if running == 0 { - format!("Background commands: {done} completed") + format!("Bash jobs: {done} completed") } else if done == 0 { - format!("Background commands: {running} running") + format!("Bash jobs: {running} running") } else { - format!("Background commands: {running} running, {done} completed") + format!("Bash jobs: {running} running, {done} completed") }; lines.push(Line::from(Span::styled( label, @@ -985,11 +985,11 @@ fn task_panel_hover_texts(app: &App, max_rows: usize) -> Vec { .count(); let done = background_rows.len().saturating_sub(running); let label = if running == 0 { - format!("Background commands: {done} completed") + format!("Bash jobs: {done} completed") } else if done == 0 { - format!("Background commands: {running} running") + format!("Bash jobs: {running} running") } else { - format!("Background commands: {running} running, {done} completed") + format!("Bash jobs: {running} running, {done} completed") }; texts.push(label); @@ -1136,8 +1136,8 @@ fn background_task_labels(task: &TaskPanelEntry, duration: &str) -> (String, Str if let Some(command) = task.prompt_summary.strip_prefix("shell: ") { let command = concise_shell_command_label(command, 96); return ( - format!("{} {} {}", task.status, command, duration), - format!("{} \u{00B7} command", task.id), + format!("Bash {} {} {}", task.status, command, duration), + format!("{} \u{00B7} Bash", task.id), ); } @@ -1482,9 +1482,9 @@ fn failure_summary_with_hint(summary: &str) -> String { fn friendly_generic_tool_name(name: &str) -> &str { match name { - "task_shell_start" => "start command", - "task_shell_wait" => "wait command", - "task_shell_write" => "write command", + "task_shell_start" => "start Bash", + "task_shell_wait" => "wait Bash", + "task_shell_write" => "write Bash", _ => name, } } @@ -1493,7 +1493,7 @@ fn generic_tool_sidebar_summary(generic: &GenericToolCell) -> String { match generic.name.as_str() { "task_shell_start" => compact_join([ generic.input_summary.clone().unwrap_or_default(), - "background command".to_string(), + "background Bash".to_string(), ]), "task_shell_wait" => compact_join([ generic.input_summary.clone().unwrap_or_default(), @@ -1772,7 +1772,7 @@ fn is_ci_poll_row(row: &SidebarToolRow) -> bool { } fn is_shell_wait_poll_row(row: &SidebarToolRow) -> bool { - row.status == ToolStatus::Running && row.name == "wait command" + row.status == ToolStatus::Running && row.name == "wait Bash" } fn shell_wait_poll_key(row: &SidebarToolRow) -> String { @@ -3214,7 +3214,7 @@ mod tests { "running shell command should not render as both live and background: {text:?}" ); assert!( - !text.iter().any(|line| line.contains("Background commands")), + !text.iter().any(|line| line.contains("Bash jobs")), "duplicate background shell row should be hidden: {text:?}" ); } @@ -3267,7 +3267,7 @@ mod tests { "reasoning row should show live thinking duration: {text:?}" ); assert!( - !text.iter().any(|line| line.contains("Background commands")), + !text.iter().any(|line| line.contains("Bash jobs")), "reasoning must not be counted as a background command: {text:?}" ); } @@ -3327,7 +3327,7 @@ mod tests { let header_idx = text .iter() - .position(|line| line.starts_with("Background commands")) + .position(|line| line.starts_with("Bash jobs")) .expect("background header row"); assert!(actions[header_idx].is_none(), "header is not clickable"); @@ -3714,7 +3714,7 @@ mod tests { let text = lines_to_text(&task_panel_lines(&app, 80, 6)); assert!( - text.iter().any(|line| line.contains("[~] wait command")), + text.iter().any(|line| line.contains("[~] wait Bash")), "shell helper should render as a user-facing activity: {text:?}" ); assert!( @@ -3748,7 +3748,7 @@ mod tests { assert_eq!( text.iter() - .filter(|line| line.contains("[~] wait command")) + .filter(|line| line.contains("[~] wait Bash")) .count(), 1, "duplicate waits for the same shell job should collapse: {text:?}"