From 8f265e204f3872a754084f35d3b5938cc20ee257 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 06:13:08 -0700 Subject: [PATCH] fix(tui): use muted selection highlights in dark themes --- CHANGELOG.md | 3 ++ crates/tui/CHANGELOG.md | 3 ++ crates/tui/src/theme_qa_audit.rs | 18 +++++++++++ crates/tui/src/tui/context_menu.rs | 4 +-- crates/tui/src/tui/session_picker.rs | 7 +++-- crates/tui/src/tui/views/help.rs | 10 +++---- crates/tui/src/tui/views/mod.rs | 45 ++++++++++++++++++++++++++-- crates/tui/src/tui/widgets/mod.rs | 10 +++---- 8 files changed, 83 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bb75eb..0c6b7e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Sidebar hover popovers (#3088).** Streaming turns now keep sidebar hover popovers responsive while continuing to throttle transcript/body mouse motion. +- **Dark-theme selection contrast (#3074, thanks @drpars).** Session, config, + help, context-menu, and approval selections now use the muted selection + background instead of the bright accent color. - **Cursor-style activity metadata rows (#3146).** Dense successful tool-run summaries now render as a single muted `Explored ...` / `Updated metadata` row, include short command-family labels for successful generic verifier diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index acce0059..a039edcd 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Sidebar hover popovers (#3088).** Streaming turns now keep sidebar hover popovers responsive while continuing to throttle transcript/body mouse motion. +- **Dark-theme selection contrast (#3074, thanks @drpars).** Session, config, + help, context-menu, and approval selections now use the muted selection + background instead of the bright accent color. - **Cursor-style activity metadata rows (#3146).** Dense successful tool-run summaries now render as a single muted `Explored ...` / `Updated metadata` row, include short command-family labels for successful generic verifier diff --git a/crates/tui/src/theme_qa_audit.rs b/crates/tui/src/theme_qa_audit.rs index b23af32b..18601e05 100644 --- a/crates/tui/src/theme_qa_audit.rs +++ b/crates/tui/src/theme_qa_audit.rs @@ -221,6 +221,24 @@ mod tests { } } + #[test] + fn selected_row_text_contrasts_on_selection_bg() { + for theme in ALL_THEMES { + let name = theme.name; + let Some(fg) = rgb(theme.text_body) else { + continue; + }; + let Some(bg) = rgb(theme.selection_bg) else { + continue; + }; + let cr = contrast_ratio(fg, bg); + assert!( + cr >= 4.5, + "{name}: selected-row text contrast {cr:.1}:1 is below 4.5:1 (fg={fg:?}, bg={bg:?})" + ); + } + } + #[test] fn surface_layers_are_distinct() { for theme in ALL_THEMES { diff --git a/crates/tui/src/tui/context_menu.rs b/crates/tui/src/tui/context_menu.rs index 20543551..f1aada8e 100644 --- a/crates/tui/src/tui/context_menu.rs +++ b/crates/tui/src/tui/context_menu.rs @@ -188,8 +188,8 @@ impl ModalView for ContextMenuView { let text = trim_to_width(&format!("{label}{description}"), inner_width); let style = if idx == self.selected { Style::default() - .fg(palette::TEXT_PRIMARY) - .bg(palette::DEEPSEEK_BLUE) + .fg(palette::SELECTION_TEXT) + .bg(palette::SELECTION_BG) .add_modifier(Modifier::BOLD) } else { Style::default() diff --git a/crates/tui/src/tui/session_picker.rs b/crates/tui/src/tui/session_picker.rs index 489438a2..637d1bd3 100644 --- a/crates/tui/src/tui/session_picker.rs +++ b/crates/tui/src/tui/session_picker.rs @@ -668,7 +668,7 @@ fn build_list_lines( let style = if idx == selected { Style::default() .fg(palette::SELECTION_TEXT) - .bg(palette::DEEPSEEK_BLUE) + .bg(palette::SELECTION_BG) .add_modifier(Modifier::BOLD) } else { Style::default().fg(palette::TEXT_PRIMARY) @@ -1086,7 +1086,7 @@ mod tests { } #[test] - fn build_list_lines_selected_row_uses_strong_highlight() { + fn build_list_lines_selected_row_uses_muted_selection_highlight() { let sessions = vec![ test_session(1, "first session"), test_session(2, "second session"), @@ -1109,7 +1109,8 @@ mod tests { .expect("selected row should have a span"); assert_eq!(span.style.fg, Some(palette::SELECTION_TEXT)); - assert_eq!(span.style.bg, Some(palette::DEEPSEEK_BLUE)); + assert_eq!(span.style.bg, Some(palette::SELECTION_BG)); + assert_ne!(span.style.bg, Some(palette::DEEPSEEK_BLUE)); assert!(span.style.add_modifier.contains(Modifier::BOLD)); } diff --git a/crates/tui/src/tui/views/help.rs b/crates/tui/src/tui/views/help.rs index 4124fcf5..778264e8 100644 --- a/crates/tui/src/tui/views/help.rs +++ b/crates/tui/src/tui/views/help.rs @@ -451,7 +451,7 @@ impl ModalView for HelpView { let style = if is_selected { Style::default() .fg(palette::SELECTION_TEXT) - .bg(palette::DEEPSEEK_BLUE) + .bg(palette::SELECTION_BG) .add_modifier(Modifier::BOLD) } else { Style::default().fg(palette::TEXT_PRIMARY) @@ -699,7 +699,7 @@ mod tests { let cell = &buf[(x, y)]; row.push_str(cell.symbol()); row_has_highlight |= - cell.bg == palette::DEEPSEEK_BLUE && cell.fg == palette::SELECTION_TEXT; + cell.bg == palette::SELECTION_BG && cell.fg == palette::SELECTION_TEXT; } if row_has_highlight && row.contains(&selected_label) { highlighted_label = true; @@ -714,7 +714,7 @@ mod tests { } #[test] - fn selected_help_row_uses_stronger_highlight() { + fn selected_help_row_uses_selection_highlight() { let view = HelpView::new(); let area = Rect::new(0, 0, 96, 32); let mut buf = Buffer::empty(area); @@ -724,7 +724,7 @@ mod tests { for y in area.top()..area.bottom() { for x in area.left()..area.right() { let cell = &buf[(x, y)]; - if cell.bg == palette::DEEPSEEK_BLUE && cell.fg == palette::SELECTION_TEXT { + if cell.bg == palette::SELECTION_BG && cell.fg == palette::SELECTION_TEXT { found_highlight = true; break; } @@ -733,7 +733,7 @@ mod tests { assert!( found_highlight, - "selected row should use a strong blue highlight" + "selected row should use the semantic selection highlight" ); } diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 9b513751..892d7107 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -1484,8 +1484,8 @@ impl ModalView for ConfigView { let selected = *idx == self.selected; let style = if selected { Style::default() - .fg(ratatui::style::Color::White) - .bg(palette::DEEPSEEK_BLUE) + .fg(palette::SELECTION_TEXT) + .bg(palette::SELECTION_BG) .add_modifier(ratatui::style::Modifier::BOLD) } else { Style::default().fg(palette::TEXT_PRIMARY) @@ -2088,6 +2088,7 @@ mod tests { }; use crate::config::Config; use crate::localization::{Locale, MessageId, tr}; + use crate::palette; use crate::settings::Settings; use crate::tools::subagent::{ SubAgentAssignment, SubAgentResult, SubAgentStatus, SubAgentType, @@ -2598,6 +2599,46 @@ base_url = "https://api.xiaomimimo.com/v1" ); } + #[test] + fn config_view_selected_row_uses_muted_selection_highlight() { + let mut view = create_config_view(Locale::En); + view.selected = view + .rows + .iter() + .position(|row| row.key == "theme") + .expect("theme row"); + view.adjust_scroll(8); + let area = Rect::new(0, 0, 100, 24); + let mut buf = Buffer::empty(area); + + view.render(area, &mut buf); + + let y = view + .last_row_hitboxes + .borrow() + .iter() + .find_map(|(y, idx)| (*idx == view.selected).then_some(*y)) + .expect("selected config row should have a hitbox"); + let highlighted_cells = (area.x..area.x.saturating_add(area.width)) + .filter(|&x| { + let cell = &buf[(x, y)]; + !cell.symbol().trim().is_empty() + && cell.bg == palette::SELECTION_BG + && cell.fg == palette::SELECTION_TEXT + }) + .count(); + + assert!( + highlighted_cells >= 4, + "selected config row should render readable selection text" + ); + assert!( + !(area.x..area.x.saturating_add(area.width)) + .any(|x| buf[(x, y)].bg == palette::DEEPSEEK_BLUE), + "selected config row should not use the bright accent background" + ); + } + #[test] fn config_view_keeps_scope_column_aligned_for_long_keys() { let mut view = create_config_view(Locale::ZhHans); diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 595d48d4..a50e9236 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -1454,7 +1454,7 @@ fn approval_palette(risk: RiskLevel) -> ApprovalColors { fn approval_selected_style() -> Style { Style::default() .fg(palette::SELECTION_TEXT) - .bg(palette::DEEPSEEK_BLUE) + .bg(palette::SELECTION_BG) .add_modifier(Modifier::BOLD) } @@ -4072,21 +4072,21 @@ mod tests { let selected_row = (area.y..area.y.saturating_add(area.height)) .find(|&y| { (area.x..area.x.saturating_add(area.width)) - .any(|x| buf[(x, y)].bg == palette::DEEPSEEK_BLUE) + .any(|x| buf[(x, y)].bg == palette::SELECTION_BG) }) - .expect("selected approval row should use blue background"); + .expect("selected approval row should use selection background"); let highlighted_cells = (area.x..area.x.saturating_add(area.width)) .filter(|&x| { let cell = &buf[(x, selected_row)]; !cell.symbol().trim().is_empty() - && cell.bg == palette::DEEPSEEK_BLUE + && cell.bg == palette::SELECTION_BG && cell.fg == palette::SELECTION_TEXT }) .count(); assert!( highlighted_cells >= 4, - "selected destructive option should render visible blue/white text" + "selected destructive option should render visible selection text" ); }