From 2e73634ab07c2c64c9538242c762802128557332 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Wed, 27 May 2026 04:45:40 -0500 Subject: [PATCH] fix(tui): make sidebar failure clearing order-aware --- crates/tui/src/tui/sidebar.rs | 139 ++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index c28c5a83..da99f31f 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -770,7 +770,7 @@ fn active_tool_rows(app: &App) -> Vec { if !stale_running.is_empty() { rows.push(collapsed_stale_running_row(stale_running)); } - editorial_tool_rows(rows, usize::MAX) + editorial_tool_rows(rows, usize::MAX, ToolRowOrder::OldestFirst) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -837,7 +837,7 @@ fn recent_tool_rows(app: &App, limit: usize) -> Vec { .filter_map(sidebar_tool_row_from_cell) .take(RECENT_TOOL_SCAN_LIMIT) .collect(); - editorial_tool_rows(rows, limit) + editorial_tool_rows(rows, limit, ToolRowOrder::NewestFirst) } fn push_tool_rows( @@ -1142,7 +1142,17 @@ fn background_task_duplicates_live_tool( }) } -fn editorial_tool_rows(rows: Vec, limit: usize) -> Vec { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ToolRowOrder { + OldestFirst, + NewestFirst, +} + +fn editorial_tool_rows( + rows: Vec, + limit: usize, + order_mode: ToolRowOrder, +) -> Vec { #[derive(Clone)] struct Candidate { rank: u8, @@ -1155,7 +1165,7 @@ fn editorial_tool_rows(rows: Vec, limit: usize) -> Vec = Vec::new(); let mut shell_wait_groups: Vec<(usize, SidebarToolRow, usize, String)> = Vec::new(); let mut seen_success: Vec = Vec::new(); - let mut seen_tool_names_succeeded: Vec = Vec::new(); + let mut seen_success_tool_names: Vec = Vec::new(); let mut seen_failures: Vec = Vec::new(); let mut visible_failure_count: usize = 0; const MAX_VISIBLE_FAILURES: usize = 2; @@ -1166,6 +1176,11 @@ fn editorial_tool_rows(rows: Vec, limit: usize) -> Vec, limit: usize) -> Vec, limit: usize) -> Vec= MAX_VISIBLE_FAILURES { - visible_failure_count += 1; 3 } else { visible_failure_count += 1; @@ -1257,11 +1285,7 @@ fn editorial_tool_rows(rows: Vec, limit: usize) -> Vec SidebarToolRow { + SidebarToolRow { + name: name.to_string(), + status, + summary: String::new(), + duration_ms: None, + } + } + fn lines_to_text(lines: &[Line<'static>]) -> Vec { lines .iter() @@ -1979,6 +2012,62 @@ mod tests { .collect() } + #[test] + fn editorial_rows_keep_newer_failure_when_older_success_is_seen_later() { + let rows = vec![ + sidebar_tool_row("gh issue create", ToolStatus::Failed), + sidebar_tool_row("gh issue create", ToolStatus::Success), + ]; + + let rendered = editorial_tool_rows(rows, 4, ToolRowOrder::NewestFirst); + + assert!( + rendered + .iter() + .any(|row| row.name == "gh issue create" && row.status == ToolStatus::Failed), + "newest-first rows must keep a failure newer than a later-seen success: {rendered:?}" + ); + } + + #[test] + fn editorial_rows_hide_older_failure_after_newer_success() { + let rows = vec![ + sidebar_tool_row("gh issue create", ToolStatus::Success), + sidebar_tool_row("gh issue create", ToolStatus::Failed), + ]; + + let rendered = editorial_tool_rows(rows, 4, ToolRowOrder::NewestFirst); + + assert!( + !rendered + .iter() + .any(|row| row.name == "gh issue create" && row.status == ToolStatus::Failed), + "newest-first rows should hide stale failures older than success: {rendered:?}" + ); + } + + #[test] + fn editorial_rows_reclaim_failure_slot_after_oldest_first_success() { + let rows = vec![ + sidebar_tool_row("gh issue create", ToolStatus::Failed), + sidebar_tool_row("grep_files", ToolStatus::Failed), + sidebar_tool_row("gh issue create", ToolStatus::Success), + sidebar_tool_row("cargo test", ToolStatus::Failed), + ]; + + let rendered = editorial_tool_rows(rows, 2, ToolRowOrder::OldestFirst); + + assert_eq!( + rendered + .iter() + .filter(|row| row.status == ToolStatus::Failed) + .map(|row| row.name.as_str()) + .collect::>(), + vec!["grep_files", "cargo test"], + "success should clear its stale failure and free a visible failure slot" + ); + } + #[test] fn auto_sidebar_does_not_reserve_empty_work_when_other_panels_are_active() { let panels = auto_sidebar_panels(AutoSidebarState {