diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 7ce1b0adac..c653388026 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -844,9 +844,22 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: break; } let position = detail_idx + 1; - let position_style = Style::default().fg(p.overlay0); + let is_active = app.is_active_pane(detail.ws_idx, detail.tab_idx, detail.pane_id); + let position_style = if is_active { + Style::default().fg(p.text).bg(p.surface_dim) + } else { + Style::default().fg(p.overlay0) + }; let (icon, icon_style) = state_icon(detail.state, detail.seen, app.status_indicators, p); + + if is_active { + let buf = frame.buffer_mut(); + for x in detail_content_area.x..detail_content_area.x + detail_content_area.width { + buf[(x, y)].set_style(Style::default().bg(p.surface_dim)); + } + } + frame.render_widget( Paragraph::new(Line::from(vec![ Span::styled(format!("{position:<2}"), position_style), @@ -1575,8 +1588,8 @@ fn render_sidebar_toggle( #[cfg(test)] mod tests { use super::*; - use crate::{detect::Agent, workspace::Workspace}; - use ratatui::{backend::TestBackend, Terminal}; + use crate::{detect::Agent, layout::PaneId, workspace::Workspace}; + use ratatui::{backend::TestBackend, layout::Direction, Terminal}; fn row_text(buffer: &ratatui::buffer::Buffer, row: u16, width: u16) -> String { (0..width) @@ -2162,6 +2175,105 @@ rows = [[{ token = "git_status", fg = "#123456" }]] assert_eq!(buffer[(detail_area.x, detail_area.y + 1)].symbol(), "2"); } + /// Two agent panes in one workspace plus a second workspace, so the + /// assertions can tell pane-level highlighting apart from workspace-level. + fn collapsed_agent_app() -> (crate::app::state::AppState, PaneId, PaneId) { + let mut app = crate::app::state::AppState::test_new(); + let mut first = Workspace::test_new("one"); + let second_pane = first.test_split(Direction::Horizontal); + let first_pane = first.tabs[0].root_pane; + app.workspaces = vec![first, Workspace::test_new("two")]; + app.ensure_test_terminals(); + + let terminal_ids: Vec<_> = app + .workspaces + .iter() + .flat_map(|ws| ws.tabs.iter()) + .flat_map(|tab| tab.panes.values()) + .map(|pane| pane.attached_terminal_id.clone()) + .collect(); + for terminal_id in terminal_ids { + app.terminals.get_mut(&terminal_id).unwrap().detected_agent = Some(Agent::Claude); + } + + (app, first_pane, second_pane) + } + + fn collapsed_agent_row_styles( + app: &crate::app::state::AppState, + area: Rect, + detail_area: Rect, + rows: u16, + ) -> Vec> { + let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)) + .expect("test terminal should initialize"); + terminal + .draw(|frame| render_sidebar_collapsed(app, frame, area)) + .expect("collapsed sidebar should render"); + let buffer = terminal.backend().buffer(); + (0..rows) + .map(|row| { + (detail_area.x..detail_area.x + detail_area.width) + .map(|x| buffer[(x, detail_area.y + row)].style()) + .collect() + }) + .collect() + } + + #[test] + fn collapsed_sidebar_highlights_only_the_focused_agent_pane() { + let (mut app, first_pane, second_pane) = collapsed_agent_app(); + app.active = Some(0); + app.workspaces[0].tabs[0].layout.focus_pane(second_pane); + assert!(app.is_active_pane(0, 0, second_pane)); + assert!(!app.is_active_pane(0, 0, first_pane)); + + let area = Rect::new(0, 0, 4, 14); + let (_, _, detail_area) = collapsed_sidebar_sections(area); + let rows = collapsed_agent_row_styles(&app, area, detail_area, 3); + + let highlighted: Vec<_> = rows + .iter() + .filter(|cells| { + cells + .iter() + .all(|style| style.bg == Some(app.palette.surface_dim)) + }) + .collect(); + assert_eq!( + highlighted.len(), + 1, + "only the focused agent pane should be highlighted, across the whole row" + ); + assert_eq!(highlighted[0][0].fg, Some(app.palette.text)); + + let muted = rows + .iter() + .filter(|cells| cells[0].fg == Some(app.palette.overlay0)) + .count(); + assert_eq!( + muted, 2, + "the sibling pane in the active workspace and the other workspace stay muted" + ); + } + + #[test] + fn collapsed_sidebar_does_not_highlight_agents_without_active_workspace() { + let (mut app, _, _) = collapsed_agent_app(); + app.active = None; + + let area = Rect::new(0, 0, 4, 14); + let (_, _, detail_area) = collapsed_sidebar_sections(area); + let rows = collapsed_agent_row_styles(&app, area, detail_area, 3); + + for cells in rows { + assert_eq!(cells[0].fg, Some(app.palette.overlay0)); + for style in cells { + assert_ne!(style.bg, Some(app.palette.surface_dim)); + } + } + } + #[test] fn collapsed_sidebar_keeps_workspace_status_visible_for_two_digit_positions() { let mut app = crate::app::state::AppState::test_new();