fix(tui): summarize live tool status noise (#1618)

Summary:
- concise live shell/tool labels
- collapsed pending CI polling rows
- hardened stale task-panel timing test

Validation: CI green before merge.
This commit is contained in:
Hunter Bown
2026-05-14 03:21:57 -05:00
committed by GitHub
parent d5c45d962d
commit 9eb588c383
4 changed files with 339 additions and 23 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ use crate::tui::ui::{
active_foreground_shell_running, context_usage_snapshot, selected_detail_footer_label,
status_color,
};
use crate::tui::ui_text::truncate_line_to_width;
use crate::tui::ui_text::{concise_shell_command_label, truncate_line_to_width};
use crate::tui::widgets::{FooterProps, FooterToast, FooterWidget, Renderable};
use crate::tui::workspace_context;
@@ -265,7 +265,7 @@ fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatu
};
match tool {
ToolCell::Exec(exec) => snapshot.record(
format!("run {}", one_line_summary(&exec.command, 80)),
concise_shell_command_label(&exec.command, 80),
exec.status,
exec.started_at,
),
+214 -20
View File
@@ -25,7 +25,7 @@ use crate::tools::todo::TodoStatus;
use super::app::{App, SidebarFocus, TaskPanelEntry};
use super::history::{GenericToolCell, HistoryCell, ToolCell, ToolStatus, summarize_tool_output};
use super::subagent_routing::active_fanout_counts;
use super::ui_text::truncate_line_to_width;
use super::ui_text::{concise_shell_command_label, truncate_line_to_width};
/// Tolerance for floating-point cost comparison in the sidebar breakdown.
/// Must be large enough that accumulated f64 error across hundreds of turns
@@ -821,17 +821,19 @@ fn sidebar_tool_row_from_cell(cell: &HistoryCell) -> Option<SidebarToolRow> {
};
match tool {
ToolCell::Exec(exec) => Some(SidebarToolRow {
name: "run".to_string(),
status: exec.status,
summary: compact_join([
exec.command.clone(),
exec.output_summary.clone().unwrap_or_default(),
exec.output
.as_deref()
.map(first_nonempty_line)
.unwrap_or_default()
.to_string(),
]),
name: concise_shell_command_label(&exec.command, 48),
status: shell_status_for_sidebar(
&exec.command,
exec.status,
exec.output_summary.as_deref(),
exec.output.as_deref(),
),
summary: shell_summary_for_sidebar(
&exec.command,
exec.status,
exec.output_summary.as_deref(),
exec.output.as_deref(),
),
duration_ms: exec.duration_ms.or_else(|| {
(exec.status == ToolStatus::Running).then(|| {
u64::try_from(
@@ -934,6 +936,87 @@ fn sidebar_tool_row_from_cell(cell: &HistoryCell) -> Option<SidebarToolRow> {
}
}
fn shell_status_for_sidebar(
command: &str,
status: ToolStatus,
output_summary: Option<&str>,
output: Option<&str>,
) -> ToolStatus {
if status == ToolStatus::Failed && looks_like_pending_ci(command, output_summary, output) {
ToolStatus::Running
} else {
status
}
}
fn shell_summary_for_sidebar(
command: &str,
status: ToolStatus,
output_summary: Option<&str>,
output: Option<&str>,
) -> String {
if status == ToolStatus::Failed && looks_like_pending_ci(command, output_summary, output) {
return format!(
"Waiting for CI \u{00B7} {} details",
crate::tui::key_shortcuts::tool_details_shortcut_label()
);
}
let summary = compact_join([
output_summary.unwrap_or_default().to_string(),
output
.map(first_nonempty_line)
.unwrap_or_default()
.to_string(),
]);
if status == ToolStatus::Failed {
failure_summary_with_hint(&summary)
} else {
summary
}
}
fn looks_like_pending_ci(
command: &str,
output_summary: Option<&str>,
output: Option<&str>,
) -> bool {
let command_label = concise_shell_command_label(command, 80).to_ascii_lowercase();
if !command_label.starts_with("gh pr checks") && !command_label.starts_with("gh run watch") {
return false;
}
let text = compact_join([
output_summary.unwrap_or_default().to_string(),
output.unwrap_or_default().to_string(),
])
.to_ascii_lowercase();
if text.is_empty() {
return false;
}
let pending = ["pending", "queued", "in_progress", "in progress", "waiting"]
.iter()
.any(|needle| text.contains(needle));
let hard_failure = ["failed", "failure", "error", "cancelled", "canceled"]
.iter()
.any(|needle| text.contains(needle));
pending && !hard_failure
}
fn failure_summary_with_hint(summary: &str) -> String {
let hint = format!(
"inspect details with {}",
crate::tui::key_shortcuts::tool_details_shortcut_label()
);
if summary.trim().is_empty() {
hint
} else if summary.contains(&hint) {
summary.to_string()
} else {
format!("{summary} \u{00B7} {hint}")
}
}
fn friendly_generic_tool_name(name: &str) -> &str {
match name {
"task_shell_start" => "start shell job",
@@ -997,8 +1080,8 @@ fn background_task_duplicates_live_tool(
!command.is_empty()
&& active_rows.iter().any(|row| {
row.status == ToolStatus::Running
&& row.name == "run"
&& normalize_activity_text(&row.summary).contains(&command)
&& normalize_activity_text(&format!("{} {}", row.name, row.summary))
.contains(&command)
})
}
@@ -1012,9 +1095,29 @@ fn editorial_tool_rows(rows: Vec<SidebarToolRow>, limit: usize) -> Vec<SidebarTo
let mut candidates: Vec<Candidate> = Vec::new();
let mut low_value_groups: Vec<(usize, SidebarToolRow, usize)> = Vec::new();
let mut ci_poll_groups: Vec<(usize, SidebarToolRow, usize)> = Vec::new();
let mut seen_success: Vec<String> = Vec::new();
for (order, row) in rows.into_iter().enumerate() {
for (order, mut row) in rows.into_iter().enumerate() {
if row.status == ToolStatus::Failed {
row.summary = failure_summary_with_hint(&row.summary);
}
if is_ci_poll_row(&row) {
if let Some((_, grouped, count)) = ci_poll_groups
.iter_mut()
.find(|(_, grouped, _)| grouped.name == row.name)
{
*count += 1;
if grouped.duration_ms.is_none() {
grouped.duration_ms = row.duration_ms;
}
} else {
ci_poll_groups.push((order, row, 1));
}
continue;
}
if is_low_value_tool(&row.name) && row.status == ToolStatus::Success {
if let Some((_, grouped, count)) = low_value_groups
.iter_mut()
@@ -1045,6 +1148,23 @@ fn editorial_tool_rows(rows: Vec<SidebarToolRow>, limit: usize) -> Vec<SidebarTo
});
}
for (order, mut row, count) in ci_poll_groups {
if count > 1 {
let command = row.name.clone();
row.name = "Waiting for CI".to_string();
row.summary = format!(
"{command} \u{00B7} {count} polls collapsed \u{00B7} {} details",
crate::tui::key_shortcuts::tool_details_shortcut_label()
);
row.status = ToolStatus::Running;
}
candidates.push(Candidate {
rank: tool_row_rank(&row),
order,
row,
});
}
for (order, mut row, count) in low_value_groups {
if count > 1 {
row.name = format!("{} x{count}", row.name);
@@ -1075,6 +1195,10 @@ fn sidebar_row_identity(row: &SidebarToolRow) -> String {
)
}
fn is_ci_poll_row(row: &SidebarToolRow) -> bool {
row.name.starts_with("gh pr checks") || row.name.starts_with("gh run watch")
}
fn normalize_activity_text(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
@@ -1965,10 +2089,8 @@ mod tests {
command: command.to_string(),
status: ToolStatus::Running,
output: None,
started_at: Some(
Instant::now() - ACTIVE_TOOL_STALE_RUNNING_ROW_TTL - Duration::from_secs(1),
),
duration_ms: None,
started_at: None,
duration_ms: Some(ACTIVE_TOOL_STALE_RUNNING_ROW_TTL.as_millis() as u64 + 1),
source: ExecSource::Assistant,
interaction: None,
output_summary: None,
@@ -2100,6 +2222,77 @@ mod tests {
);
}
#[test]
fn tasks_panel_collapses_repeated_pending_ci_polls() {
let mut app = create_test_app();
for _ in 0..3 {
app.history.push(HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: "cd /tmp/repo && sleep 15 && gh pr checks 1616 --repo Hmbown/DeepSeek-TUI"
.to_string(),
status: ToolStatus::Failed,
output: Some("Lint pending\nTest pending".to_string()),
started_at: None,
duration_ms: Some(15_000),
source: ExecSource::Assistant,
interaction: None,
output_summary: Some("2 checks pending".to_string()),
})));
}
let text = lines_to_text(&task_panel_lines(&app, 80, 12));
assert!(
text.iter().any(|line| line.contains("[~] Waiting for CI")),
"pending CI should not render as a hard failure: {text:?}"
);
assert!(
text.iter().any(|line| line.contains("gh pr checks 1616")),
"concise command label should remain visible: {text:?}"
);
assert!(
text.iter().any(|line| line.contains("3 polls collapsed")),
"repeated polling should collapse into one row: {text:?}"
);
assert!(
text.iter()
.any(|line| line.contains(crate::tui::key_shortcuts::tool_details_shortcut_label())),
"collapsed CI row should point to details: {text:?}"
);
assert!(
!text.iter().any(|line| line.contains("[!] gh pr checks")),
"pending CI should not look like a real failure: {text:?}"
);
}
#[test]
fn tasks_panel_failed_shell_rows_point_to_activity_details() {
let mut app = create_test_app();
app.history.push(HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: "cargo test -p deepseek-tui".to_string(),
status: ToolStatus::Failed,
output: Some("test failed".to_string()),
started_at: None,
duration_ms: Some(1_250),
source: ExecSource::Assistant,
interaction: None,
output_summary: Some("test failed".to_string()),
})));
let text = lines_to_text(&task_panel_lines(&app, 80, 8));
assert!(
text.iter().any(|line| line.contains("[!] cargo test")),
"failed shell command should keep its concise label: {text:?}"
);
assert!(
text.iter().any(|line| line.contains(&format!(
"inspect details with {}",
crate::tui::key_shortcuts::tool_details_shortcut_label()
))),
"failed row should include the next action: {text:?}"
);
}
#[test]
fn tasks_panel_keeps_duration_and_status_on_recent_shell_rows() {
let mut app = create_test_app();
@@ -2117,7 +2310,8 @@ mod tests {
let text = lines_to_text(&task_panel_lines(&app, 80, 8));
assert!(
text.iter().any(|line| line.contains("[x] run 1.2s")),
text.iter()
.any(|line| line.contains("[x] cargo check 1.2s")),
"status marker and duration should stay in the row label: {text:?}"
);
assert!(
+29 -1
View File
@@ -1456,12 +1456,40 @@ fn active_tool_status_label_summarizes_live_tool_group() {
let label = active_tool_status_label(&app).expect("status label");
assert!(label.contains("run cargo test"));
assert!(label.contains("cargo test"));
assert!(label.contains("1 active"));
assert!(label.contains("1 done"));
assert!(label.contains(crate::tui::key_shortcuts::tool_details_shortcut_label()));
}
#[test]
fn active_tool_status_label_strips_shell_wrappers_from_ci_polling() {
let mut app = create_test_app();
app.turn_started_at = Some(Instant::now() - Duration::from_secs(5));
let mut active = ActiveCell::new();
active.push_tool(
"exec-1",
HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: "cd /tmp/repo && sleep 15 && gh pr checks 1611 --repo Hmbown/DeepSeek-TUI"
.to_string(),
status: ToolStatus::Running,
output: None,
started_at: app.turn_started_at,
duration_ms: None,
source: ExecSource::Assistant,
interaction: None,
output_summary: None,
})),
);
app.active_cell = Some(active);
let label = active_tool_status_label(&app).expect("status label");
assert!(label.contains("gh pr checks 1611"), "label: {label}");
assert!(!label.contains("cd /tmp"), "label: {label}");
assert!(!label.contains("sleep 15"), "label: {label}");
}
#[test]
fn active_tool_status_label_counts_foreground_rlm_work() {
let mut app = create_test_app();
+94
View File
@@ -43,6 +43,85 @@ pub(crate) fn truncate_line_to_width(text: &str, max_width: usize) -> String {
out
}
pub(crate) fn concise_shell_command_label(command: &str, max_width: usize) -> String {
let normalized = normalize_shell_text(command);
if let Some(label) = gh_command_label(&normalized) {
return truncate_line_to_width(&label, max_width);
}
let segment = actionable_shell_segment(&normalized).unwrap_or_else(|| normalized.clone());
truncate_line_to_width(&segment, max_width)
}
fn normalize_shell_text(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn actionable_shell_segment(command: &str) -> Option<String> {
command
.replace("&&", "\n")
.replace("||", "\n")
.replace('|', "\n")
.split(['\n', ';'])
.map(str::trim)
.find(|segment| {
!segment.is_empty()
&& !segment.starts_with("cd ")
&& !segment.starts_with("sleep ")
&& !segment.starts_with("export ")
&& *segment != "true"
&& *segment != ":"
})
.map(str::to_string)
}
fn gh_command_label(command: &str) -> Option<String> {
let tokens: Vec<String> = command
.split_whitespace()
.map(|token| {
token
.trim_matches(|ch: char| matches!(ch, '\'' | '"' | '(' | ')' | ';' | ','))
.to_string()
})
.filter(|token| !token.is_empty())
.collect();
for index in 0..tokens.len() {
let token = tokens[index].as_str();
if token != "gh" && !token.ends_with("/gh") {
continue;
}
let Some(area) = tokens.get(index + 1).map(String::as_str) else {
continue;
};
let Some(action) = tokens.get(index + 2).map(String::as_str) else {
continue;
};
if !matches!(area, "pr" | "run") {
continue;
}
if !matches!(
action,
"checks" | "view" | "status" | "list" | "watch" | "rerun"
) {
continue;
}
let mut label = format!("gh {area} {action}");
if let Some(target) = tokens
.iter()
.skip(index + 3)
.map(String::as_str)
.find(|token| !token.starts_with('-') && *token != "&&" && *token != ";")
{
label.push(' ');
label.push_str(target);
}
return Some(label);
}
None
}
pub(super) fn history_cell_to_text(cell: &HistoryCell, width: u16) -> String {
cell.transcript_lines(width)
.into_iter()
@@ -172,4 +251,19 @@ mod tests {
let text = "ab";
assert_eq!(slice_text(text, 1, 5), "b");
}
#[test]
fn concise_shell_command_label_prefers_gh_pr_checks_over_wrappers() {
let label = concise_shell_command_label(
"cd /tmp/repo && sleep 15 && gh pr checks 1611 --repo Hmbown/DeepSeek-TUI",
80,
);
assert_eq!(label, "gh pr checks 1611");
}
#[test]
fn concise_shell_command_label_falls_back_to_actionable_segment() {
let label = concise_shell_command_label("cd /tmp/repo && cargo test --workspace", 80);
assert_eq!(label, "cargo test --workspace");
}
}