From 154df5c735d9cadf897367a99491b24a5de360fc Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Thu, 25 Jun 2026 21:35:10 +0200 Subject: [PATCH 1/2] fix(tui): attention bar respects session filter Compute has_attention from filtered_panes() instead of the unfiltered attention_count() so an error pane in another session no longer lights the bar for the session you're viewing. Removes the now-unused attention_count(). (The refinement from PR #194's feature commit that was not in the version merged via #191.) Co-Authored-By: Claude Opus 4.8 --- crates/colibri-glasspane-tui/src/main.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index 60fff66..d492d00 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -129,14 +129,6 @@ impl App { panes } - fn attention_count(&self) -> usize { - let snap = match &self.snapshot { - Some(s) => s, - None => return 0, - }; - snap.panes.iter().filter(|p| needs_attention(p)).count() - } - /// Build a list of filtered-pane indices that need attention. fn attention_indices(&self) -> Vec { self.filtered_panes() @@ -264,7 +256,7 @@ impl App { // ── rendering ── fn render(&mut self, f: &mut Frame) { - let has_attention = self.attention_count() > 0; + let has_attention = self.filtered_panes().iter().any(|p| needs_attention(p)); let has_detail = self.detail_pane.is_some(); let mut constraints = vec![Constraint::Length(3)]; // header / attention bar constraints.push(Constraint::Min(0)); // table -- 2.45.3 From 1b4d95db9dafa158f8ceac81889f858416e65fa7 Mon Sep 17 00:00:00 2001 From: 123kupola Date: Thu, 25 Jun 2026 21:18:36 +0200 Subject: [PATCH 2/2] test(tui): add coverage for attention navigation + cross-session isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four new tests closing the last attention-tier coverage gaps: - jump_next_attention_skips_healthy_panes: Panes [ok, err, ok, stalled, ok] — proves n jumps 0→1→3→wrap→1, skipping healthy panes. Forward wrapping. - jump_prev_attention_wraps_backwards: Same layout — proves N jumps 4→3→1→wrap→3. Backward wrapping. - attention_bar_ignores_other_session_panes: Error pane in session s2, viewing session s1 — bar must NOT appear. Proves the filtered_panes()-based has_attention fix from commit 4d95f11. - jump_next_attention_reports_when_no_attention_panes: All healthy panes — status message set to 'no attention', selection unchanged. 18 tests, workspace green (0 failures). --- crates/colibri-glasspane-tui/src/main.rs | 171 +++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index d492d00..87ddb77 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -1165,4 +1165,175 @@ mod tests { "should show normal header when no attention: {text}" ); } + + #[test] + fn jump_next_attention_skips_healthy_panes() { + // Panes: [ok, error, ok, stalled, ok] + // Attention at indices 1 (Error) and 3 (Stalled). + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-25T12:00:00Z", + vec![ + colibri_glasspane::Pane { + id: "pane-0".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-1".into(), agent: "zot".into(), + state: AgentState::Error, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-2".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-3".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: true, + }, + colibri_glasspane::Pane { + id: "pane-4".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + ], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + + // Start at index 0 → next attention is 1. + app.table_state.select(Some(0)); + app.jump_next_attention(); + assert_eq!(app.table_state.selected(), Some(1), "jump from 0 → 1"); + + // From 1 → next is 3. + app.jump_next_attention(); + assert_eq!(app.table_state.selected(), Some(3), "jump from 1 → 3"); + + // From 3 → wrap to 1. + app.jump_next_attention(); + assert_eq!(app.table_state.selected(), Some(1), "wrap 3 → 1"); + } + + #[test] + fn jump_prev_attention_wraps_backwards() { + // Same panes: attention at 1 and 3. + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-25T12:00:00Z", + vec![ + colibri_glasspane::Pane { + id: "pane-0".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-1".into(), agent: "zot".into(), + state: AgentState::Error, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-2".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-3".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: true, + }, + colibri_glasspane::Pane { + id: "pane-4".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }, + ], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + + // Start at index 4 → prev attention is 3. + app.table_state.select(Some(4)); + app.jump_prev_attention(); + assert_eq!(app.table_state.selected(), Some(3), "jump from 4 ← 3"); + + // From 3 → prev is 1. + app.jump_prev_attention(); + assert_eq!(app.table_state.selected(), Some(1), "jump from 3 ← 1"); + + // From 1 → wrap to 3. + app.jump_prev_attention(); + assert_eq!(app.table_state.selected(), Some(3), "wrap 1 ← 3"); + } + + #[test] + fn attention_bar_ignores_other_session_panes() { + // Error pane in session "s2", but we're viewing session "s1". + // The attention bar must NOT appear because filtered_panes() + // excludes s2 panes when session_filter = Some("s1"). + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-25T12:00:00Z", + vec![ + colibri_glasspane::Pane { + id: "pane-s1-ok".into(), agent: "zot".into(), + state: AgentState::Working, + session_id: Some("s1".into()), + last_event_at: None, cwd: None, stalled: false, + }, + colibri_glasspane::Pane { + id: "pane-s2-err".into(), agent: "zot".into(), + state: AgentState::Error, + session_id: Some("s2".into()), + last_event_at: None, cwd: None, stalled: false, + }, + ], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + app.rebuild_session_list(); + // Navigate to session s1 (rebuild selects first alphabetically = s1). + app.session_filter = Some("s1".into()); + + let text = render_text(&mut app, 80, 24); + assert!( + !text.contains("ATTENTION"), + "should not show attention bar for error in other session: {text}" + ); + assert!( + text.contains("pane-s1-ok"), + "should show healthy pane from current session: {text}" + ); + assert!( + !text.contains("pane-s2-err"), + "should not show pane from other session: {text}" + ); + } + + #[test] + fn jump_next_attention_reports_when_no_attention_panes() { + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-25T12:00:00Z", + vec![colibri_glasspane::Pane { + id: "pane-ok".into(), agent: "zot".into(), + state: AgentState::Working, session_id: None, + last_event_at: None, cwd: None, stalled: false, + }], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + + app.table_state.select(Some(0)); + app.jump_next_attention(); + // Should not have moved. + assert_eq!(app.table_state.selected(), Some(0)); + // Status message should be set. + assert!(app.status_msg.is_some()); + let msg = &app.status_msg.as_ref().unwrap().0; + assert!(msg.contains("no attention"), "expected 'no attention' in: {msg}"); + } } -- 2.45.3