From 5f26879bcd4190750c79cbff92454e400166ca98 Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Sun, 13 Sep 2026 20:17:42 +0400 Subject: [PATCH] fix(ui): keep muted sidebar and tab labels readable (#4062) refs #2692 --- src/client/shell/agent_sidebar.rs | 12 +--- src/client/shell/sidebar.rs | 4 +- src/client/shell/tabs.rs | 5 +- .../tests/agents_worktrees_notifications.rs | 61 +++++++++++++++++++ .../shell/tests/keybindings_settings.rs | 33 ++++++++++ src/client/shell/tests/mod.rs | 35 +++++++++++ src/ui/sidebar.rs | 4 +- 7 files changed, 134 insertions(+), 20 deletions(-) diff --git a/src/client/shell/agent_sidebar.rs b/src/client/shell/agent_sidebar.rs index d6fe3352..e7ebabf3 100644 --- a/src/client/shell/agent_sidebar.rs +++ b/src/client/shell/agent_sidebar.rs @@ -332,16 +332,8 @@ pub(super) fn render_agent_row( .fg(palette.subtext0) .add_modifier(Modifier::BOLD) }; - let status_style = Style::default() - .fg(status_color(row.status, palette)) - .add_modifier(if row.focused { - Modifier::empty() - } else { - Modifier::DIM - }); - let secondary = Style::default() - .fg(palette.overlay0) - .add_modifier(Modifier::DIM); + let status_style = Style::default().fg(status_color(row.status, palette)); + let secondary = Style::default().fg(palette.overlay0); let icon = ( status_icon(row.status, config.status_indicators), Style::default().fg(status_color(row.status, palette)), diff --git a/src/client/shell/sidebar.rs b/src/client/shell/sidebar.rs index c002fd62..161aecd5 100644 --- a/src/client/shell/sidebar.rs +++ b/src/client/shell/sidebar.rs @@ -703,9 +703,7 @@ pub(in crate::client::shell) fn render_workspace_rows( status_icon(status, indicators), Style::default().fg(status_color(status, palette)), ), - Style::default() - .fg(status_color(status, palette)) - .add_modifier(Modifier::DIM), + Style::default().fg(status_color(status, palette)), workspace_style, secondary_style, Style::default().fg(palette.overlay1), diff --git a/src/client/shell/tabs.rs b/src/client/shell/tabs.rs index cf179d80..9e9901ff 100644 --- a/src/client/shell/tabs.rs +++ b/src/client/shell/tabs.rs @@ -112,10 +112,7 @@ pub(crate) fn render_tab_bar( } else if tab.custom_label { Style::default().fg(palette.overlay1).bg(palette.surface0) } else { - Style::default() - .fg(palette.overlay0) - .bg(palette.surface0) - .add_modifier(Modifier::DIM) + Style::default().fg(palette.overlay0).bg(palette.surface0) }; let padding = width.saturating_sub(display_width(&name)); let left = padding / 2; diff --git a/src/client/shell/tests/agents_worktrees_notifications.rs b/src/client/shell/tests/agents_worktrees_notifications.rs index fb47ab87..0b936903 100644 --- a/src/client/shell/tests/agents_worktrees_notifications.rs +++ b/src/client/shell/tests/agents_worktrees_notifications.rs @@ -571,6 +571,67 @@ fn agent_sidebar_honors_priority_symbols_tokens_and_stable_hits() { assert_eq!(compact.cells[row_start].bg, compact.cells[row_start + 2].bg); } +#[test] +fn muted_agent_sidebar_rows_do_not_stack_terminal_faint() { + let mut projected = snapshot(); + projected.tabs[0].label = "second".into(); + projected.tabs[0].custom_label = true; + projected.agents = vec![ClientShellAgent { + pane_id: "pane_1".into(), + workspace_id: "ws_1".into(), + tab_id: "tab_1".into(), + name: Some("reviewer".into()), + display_agent: None, + agent: Some("pi".into()), + title: None, + terminal_title: None, + terminal_title_stripped: None, + agent_status: AgentStatus::Working, + state_change_seq: 1, + state_labels: Vec::new(), + tokens: Vec::new(), + focused: true, + }]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); + state.set_snapshot(Box::new(projected)); + state.set_pane_surface(surface()); + let frame = state.compose(106, 30).expect("agent sidebar frame"); + let row = state.hits.agents.first().expect("agent row hit").0; + let buffer = frame.to_ratatui_buffer().expect("agent sidebar buffer"); + + for (label, needle) in [("tab", "second"), ("agent", "reviewer"), ("separator", "·")] { + let (x, y) = cell_symbol_position(&frame, row, needle); + let cell = buffer.cell((x, y)).expect("muted sidebar cell"); + assert!( + !cell.modifier.contains(Modifier::DIM), + "{label} cell at ({x},{y}) should not stack terminal faint: {cell:?}" + ); + } +} + +#[test] +fn workspace_state_text_does_not_stack_terminal_faint() { + use crate::config::SpaceSidebarToken; + + let mut config = Config::default(); + config.ui.sidebar.spaces.rows = vec![ + vec![SpaceSidebarToken::StateIcon, SpaceSidebarToken::Workspace], + vec![SpaceSidebarToken::StateText], + ]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + state.set_snapshot(Box::new(snapshot())); + state.set_pane_surface(surface()); + let frame = state.compose(106, 30).expect("workspace sidebar frame"); + let rect = state.hits.workspaces.first().expect("workspace hit").rect; + let buffer = frame.to_ratatui_buffer().expect("workspace sidebar buffer"); + let (x, y) = cell_symbol_position(&frame, rect, "idle"); + let cell = buffer.cell((x, y)).expect("workspace state text cell"); + assert!( + !cell.modifier.contains(Modifier::DIM), + "workspace state text at ({x},{y}) should not stack terminal faint: {cell:?}" + ); +} + #[test] fn active_agent_view_controls_sidebar_order_and_focus_indices() { let mut projected = snapshot(); diff --git a/src/client/shell/tests/keybindings_settings.rs b/src/client/shell/tests/keybindings_settings.rs index 1654d0df..e1b43a01 100644 --- a/src/client/shell/tests/keybindings_settings.rs +++ b/src/client/shell/tests/keybindings_settings.rs @@ -164,6 +164,39 @@ fn tab_bar_renders_endpoint_status_ellipses_and_clamps_to_useful_scroll() { assert!(!top.contains("ZOOM · host")); } +#[test] +fn inactive_auto_named_tab_label_does_not_stack_terminal_faint() { + let mut projected = snapshot(); + projected.tabs.push(ClientShellTab { + tab_id: "tab_2".into(), + workspace_id: "ws_1".into(), + number: 2, + label: "beta".into(), + custom_label: false, + zoomed: false, + focused: false, + agent_status: AgentStatus::Idle, + }); + let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); + state.set_snapshot(Box::new(projected)); + state.set_pane_surface(surface()); + let frame = state.compose(106, 20).expect("tab bar frame"); + let rect = state + .hits + .tabs + .iter() + .find(|(_, tab_id)| tab_id == "tab_2") + .expect("inactive tab hit") + .0; + let buffer = frame.to_ratatui_buffer().expect("tab bar buffer"); + let (x, y) = cell_symbol_position(&frame, rect, "beta"); + let cell = buffer.cell((x, y)).expect("inactive tab cell"); + assert!( + !cell.modifier.contains(Modifier::DIM), + "inactive tab label at ({x},{y}) should not stack terminal faint: {cell:?}" + ); +} + #[test] fn configured_prefix_is_client_owned_and_renders_its_bar() { let config = toml::from_str::( diff --git a/src/client/shell/tests/mod.rs b/src/client/shell/tests/mod.rs index 497ce2c6..a308c1f9 100644 --- a/src/client/shell/tests/mod.rs +++ b/src/client/shell/tests/mod.rs @@ -133,6 +133,41 @@ fn surface() -> PaneSurfaceFrame { } } +fn frame_rows(frame: &FrameData) -> Vec { + frame + .cells + .chunks(frame.width as usize) + .map(|row| row.iter().map(|cell| cell.symbol.as_str()).collect()) + .collect() +} + +/// Absolute cell position of `needle` inside `area`, for style assertions. +fn cell_symbol_position(frame: &FrameData, area: Rect, needle: &str) -> (u16, u16) { + let rows = frame_rows(frame); + for y in area.y..area.bottom().min(frame.height) { + let row = &rows[y as usize]; + let slice = row + .chars() + .skip(area.x as usize) + .take(area.width as usize) + .collect::(); + if let Some(byte) = slice.find(needle) { + let column = slice[..byte].chars().count() as u16 + area.x; + return (column, y); + } + } + let visible = (area.y..area.bottom().min(frame.height)) + .map(|y| { + rows[y as usize] + .chars() + .skip(area.x as usize) + .take(area.width as usize) + .collect::() + }) + .collect::>(); + panic!("symbol {needle:?} not found in {area:?}: {visible:?}"); +} + fn pane_scroll_result( offset_from_bottom: u64, max_offset_from_bottom: u64, diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 178bdb1f..cbf6f1a9 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -219,9 +219,7 @@ pub(crate) fn resolved_token_spans( let previous = &resolved[visible_indices[position - 1]]; spans.push(Span::styled( tokens::separator(previous, token), - Style::default() - .fg(palette.overlay0) - .add_modifier(Modifier::DIM), + Style::default().fg(palette.overlay0), )); } match &token.kind {