aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIan Ker-Seymer <i.kerseymer@gmail.com>2026-08-05 17:55:58 -0400
committerGitHub <noreply@github.com>2026-08-06 00:55:58 +0300
commit2d011c39b4f4cc5edd482ed8493c663463d18701 (patch)
tree0d362a0602868cfac102635ce68a75d6bb111445 /src
parent2863b715132fe29e53089e06f105943d1df0b3b4 (diff)
fix(sidebar): highlight active agent in collapsed sidebar (#2382)
The collapsed sidebar renders agent rows with an unconditional Style::default().fg(p.overlay0), so the active agent is indistinguishable from the others. The workspace list directly above it, and render_agent_detail in the expanded sidebar, both mark the active entry. Reuse the existing active treatment: resolve the active entry with the same is_active_pane call the expanded agent panel uses, then paint the row background with surface_dim and the position number with text, the same tokens the collapsed workspace rows already use. Assisted-By: devx/717e5259-915f-4a7b-af07-6abe46da5b90
Diffstat (limited to 'src')
-rw-r--r--src/ui/sidebar.rs118
1 files changed, 115 insertions, 3 deletions
diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs
index 7ce1b0ad..c6533880 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<Vec<ratatui::style::Style>> {
+ 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();