fix(tui): attention bar respects session filter (+ tests) — resolves #194 #195
1 changed files with 172 additions and 9 deletions
|
|
@ -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<usize> {
|
||||
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
|
||||
|
|
@ -1173,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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue