diff --git a/CHANGELOG.md b/CHANGELOG.md index aeed216403d..800ec2ca03c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## [Windows] Unreleased +- Add **Color by provider** to Settings > Menu Bar (off by default): tints the tray usage bars and percent text with the shown provider's brand or accent color, keeps pace colors and the error colors unchanged, and falls back to the usual colors when a tint would be hard to see on the tray background (upstream #4321). - OpenAI API: each Admin refresh also reads the last hour of 1-minute usage from the Admin Usage API (input plus output tokens, cached input included, Batch API traffic kept out of limit headroom) and carries a Tokens/min grid with a reporting frontier, so buckets that arrive late show as pending instead of zero. The backend can also load 15-minute, 1-hour or 6-hour windows, read per-project rate-limit caps, and measure headroom against manual per-model organization limits stored in settings. The per-refresh request can be turned off, and a failure never affects the daily usage. The chart, menu bar token and Settings controls come in a later release (upstream #4495). - **Claude**: show the plan renewal or expiry date from claude.ai billing under the quota rows when your cookie session supplies it, and make saved limit-reset credits, Extra usage (including a prepaid balance), and plan dates independently selectable under Visible usage items. Existing settings keep all three visible; Admin API spend is unaffected (upstream #4324, #4484). - Devin: use Devin's official transparent hexagon mark for the provider icon in Settings and the CLI dashboard, instead of the settings-gear badge and the letter fallback (upstream #4458). diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 2fa559fb0fe..ad1a3b3de09 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -1016,6 +1016,7 @@ pub struct SettingsSnapshot { highlight_menu_cards_on_hover: bool, workday_tick_style: codexbar::settings::WorkdayTickStyle, menu_bar_color_pace: bool, + menu_bar_color_by_provider: bool, menu_bar_pace_reserve_positive: bool, providers_sorted_alphabetically: bool, show_as_used: bool, @@ -1185,6 +1186,7 @@ impl From for SettingsSnapshot { highlight_menu_cards_on_hover: settings.highlight_menu_cards_on_hover, workday_tick_style: settings.workday_tick_style, menu_bar_color_pace: settings.menu_bar_color_pace, + menu_bar_color_by_provider: settings.menu_bar_color_by_provider, menu_bar_pace_reserve_positive: settings.menu_bar_pace_reserve_positive, providers_sorted_alphabetically: settings.providers_sorted_alphabetically, show_as_used: settings.show_as_used, diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index fdcaf18209f..9967e17feef 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -44,6 +44,7 @@ pub struct SettingsUpdate { pub highlight_menu_cards_on_hover: Option, pub workday_tick_style: Option, pub menu_bar_color_pace: Option, + pub menu_bar_color_by_provider: Option, pub menu_bar_pace_reserve_positive: Option, pub providers_sorted_alphabetically: Option, pub show_as_used: Option, @@ -185,6 +186,7 @@ impl SettingsUpdate { || self.menu_bar_shows_percent.is_some() || self.menu_bar_icon_style.is_some() || self.menu_bar_color_pace.is_some() + || self.menu_bar_color_by_provider.is_some() || self.show_as_used.is_some() || self.reset_time_relative.is_some() || self.menu_bar_display_mode.is_some() @@ -395,6 +397,9 @@ impl SettingsUpdate { if let Some(v) = self.menu_bar_color_pace { settings.menu_bar_color_pace = v; } + if let Some(v) = self.menu_bar_color_by_provider { + settings.menu_bar_color_by_provider = v; + } if let Some(v) = self.menu_bar_pace_reserve_positive { settings.menu_bar_pace_reserve_positive = v; } @@ -1140,6 +1145,26 @@ mod tests { assert!(!settings.menu_bar_color_pace); } + #[test] + fn apply_display_settings_updates_color_by_provider() { + let mut settings = Settings::default(); + assert!(!settings.menu_bar_color_by_provider); + + SettingsUpdate { + menu_bar_color_by_provider: Some(true), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(settings.menu_bar_color_by_provider); + + SettingsUpdate { + menu_bar_color_by_provider: Some(false), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(!settings.menu_bar_color_by_provider); + } + #[test] fn apply_display_settings_updates_pace_sign() { let mut settings = Settings::default(); diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs index d8f0ac430fb..052a214ef6d 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs @@ -5,25 +5,32 @@ use chrono::{DateTime, Utc}; use codexbar::core::{IconLane, ProviderId, UsagePace}; use codexbar::settings::{Language, MetricPreference, Settings, TrayIconMode}; use codexbar::tray::{ - TrayPaceColor, render_bar_icon_rgba_with_pace, render_percent_icon_rgba_with_pace, - render_stacked_bar_icon_rgba, + TrayPaceColor, render_bar_icon_rgba_tinted, render_percent_icon_rgba_tinted, + render_stacked_bar_icon_rgba_tinted, }; +/// Opt-in provider accent for a tray meter (`menu_bar_color_by_provider`). +type ProviderRgb = (u8, u8, u8); + #[derive(Debug, Clone, Copy, PartialEq)] enum TrayIconPlan { Bars { primary_percent: f64, secondary_percent: Option, has_error: bool, + provider_tint: Option, }, Percent { percent: f64, has_error: bool, + provider_tint: Option, }, Stacked { top_percent: f64, bottom_percent: f64, has_error: bool, + top_tint: Option, + bottom_tint: Option, }, } @@ -79,6 +86,7 @@ impl<'a> TrayPresentationPlan<'a> { let prefer_highest = settings.menu_bar_shows_highest_usage; let selected = pick_tray_provider(&healthy, prefer_highest); let icon_pace = tray_pace_color(settings, selected, now); + let single_tint = provider_tint(settings, selected); let (icon, status_rows) = match settings.tray_icon_mode { TrayIconMode::Stacked => { @@ -88,6 +96,8 @@ impl<'a> TrayPresentationPlan<'a> { top_percent: selected_tray_percents(top, settings).0, bottom_percent: selected_tray_percents(bottom, settings).0, has_error, + top_tint: provider_tint(settings, Some(top)), + bottom_tint: provider_tint(settings, Some(bottom)), }, vec![ TrayStatusRow { @@ -114,7 +124,11 @@ impl<'a> TrayPresentationPlan<'a> { .collect(); ( resolve_single_provider_icon_plan( - settings, percents.0, percents.1, has_error, + settings, + percents.0, + percents.1, + has_error, + single_tint, ), rows, ) @@ -133,7 +147,13 @@ impl<'a> TrayPresentationPlan<'a> { }) .collect(); ( - resolve_single_provider_icon_plan(settings, percents.0, percents.1, has_error), + resolve_single_provider_icon_plan( + settings, + percents.0, + percents.1, + has_error, + single_tint, + ), rows, ) } @@ -149,7 +169,13 @@ impl<'a> TrayPresentationPlan<'a> { .into_iter() .collect(); ( - resolve_single_provider_icon_plan(settings, percents.0, percents.1, has_error), + resolve_single_provider_icon_plan( + settings, + percents.0, + percents.1, + has_error, + single_tint, + ), rows, ) } @@ -175,20 +201,32 @@ impl<'a> TrayPresentationPlan<'a> { primary_percent, secondary_percent, has_error, - } => render_bar_icon_rgba_with_pace( + provider_tint, + } => render_bar_icon_rgba_tinted( primary_percent, secondary_percent, has_error, self.icon_pace, + provider_tint, ), - TrayIconPlan::Percent { percent, has_error } => { - render_percent_icon_rgba_with_pace(percent, has_error, self.icon_pace) - } + TrayIconPlan::Percent { + percent, + has_error, + provider_tint, + } => render_percent_icon_rgba_tinted(percent, has_error, self.icon_pace, provider_tint), TrayIconPlan::Stacked { top_percent, bottom_percent, has_error, - } => render_stacked_bar_icon_rgba(top_percent, bottom_percent, has_error), + top_tint, + bottom_tint, + } => render_stacked_bar_icon_rgba_tinted( + top_percent, + bottom_percent, + has_error, + top_tint, + bottom_tint, + ), } } @@ -213,21 +251,50 @@ fn resolve_single_provider_icon_plan( primary_percent: f64, secondary_percent: Option, has_error: bool, + provider_tint: Option, ) -> TrayIconPlan { if settings.menu_bar_icon_style == codexbar::settings::MenuBarIconStyle::Percent { TrayIconPlan::Percent { percent: primary_percent, has_error, + provider_tint, } } else { TrayIconPlan::Bars { primary_percent, secondary_percent, has_error, + provider_tint, } } } +/// Accent tint for the meter of `snapshot` (upstream "Color by provider"). +/// +/// Opt-in through `menu_bar_color_by_provider`. Uses the effective accent, so +/// a per-provider override applies too. A malformed override yields no tint. +/// Legibility against the tray canvas is checked by the renderer. +fn provider_tint( + settings: &Settings, + snapshot: Option<&ProviderUsageSnapshot>, +) -> Option { + if !settings.menu_bar_color_by_provider { + return None; + } + let id = ProviderId::from_cli_name(&snapshot?.provider_id)?; + parse_hex_rgb(&settings.effective_accent_color(id)) +} + +/// Parse `#RRGGBB` (the leading `#` is optional and case is ignored). +fn parse_hex_rgb(value: &str) -> Option { + let hex = value.trim().strip_prefix('#').unwrap_or(value.trim()); + if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + let channel = |range: std::ops::Range| u8::from_str_radix(&hex[range], 16).ok(); + Some((channel(0..2)?, channel(2..4)?, channel(4..6)?)) +} + /// Pace tint for the provider that drives a single-provider tray icon. /// /// Opt-in through `menu_bar_color_pace`; on-track and unknown stages keep the diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs index 9dabb12bad0..267af3f98e0 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -108,6 +108,7 @@ fn single_plan_uses_highest_provider_for_icon_and_summary() { primary_percent: 72.0, secondary_percent: None, has_error: false, + provider_tint: None, } ); assert_eq!( @@ -184,6 +185,8 @@ fn stacked_plan_resolves_distinct_preferences_once() { top_percent: 72.0, bottom_percent: 30.0, has_error: false, + top_tint: None, + bottom_tint: None, } ); assert_eq!( @@ -238,6 +241,8 @@ fn stacked_plan_falls_back_around_stale_and_duplicate_preferences() { top_percent: 30.0, bottom_percent: 72.0, has_error: false, + top_tint: None, + bottom_tint: None, } ); assert_eq!(plan.status_rows[0].snapshot.provider_id, "codex"); @@ -268,6 +273,7 @@ fn one_provider_stacked_mode_falls_back_to_single_provider_bars() { primary_percent: 65.0, secondary_percent: Some(30.0), has_error: false, + provider_tint: None, } ); assert_eq!(plan.status_rows.len(), 1); @@ -293,6 +299,7 @@ fn one_healthy_provider_never_uses_stacked_renderer() { TrayIconPlan::Percent { percent: 30.0, has_error: false, + provider_tint: None, } ); assert_eq!(plan.status_rows.len(), 1); @@ -316,6 +323,7 @@ fn all_errors_produce_error_styled_zero_percent_plan() { TrayIconPlan::Percent { percent: 0.0, has_error: true, + provider_tint: None, } ); assert!(plan.status_rows.is_empty()); @@ -345,6 +353,7 @@ fn plan_uses_selected_metric_and_remaining_display_mode() { primary_percent: 85.0, secondary_percent: Some(80.0), has_error: false, + provider_tint: None, } ); } @@ -366,7 +375,7 @@ fn render_icon_delegates_to_resolved_stacked_renderer() { assert_eq!( plan.render_icon(), - render_stacked_bar_icon_rgba(72.0, 40.0, false) + render_stacked_bar_icon_rgba_tinted(72.0, 40.0, false, None, None) ); } @@ -951,6 +960,7 @@ fn icon_style_not_the_legacy_percent_flag_picks_the_renderer() { TrayIconPlan::Percent { percent: 30.0, has_error: false, + provider_tint: None, } ); @@ -990,3 +1000,171 @@ fn retired_minimal_display_mode_no_longer_forces_most_used_provider() { let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None); assert_eq!(plan.status_rows[0].snapshot.provider_id, "codex"); } + +const CODEX_BRAND: (u8, u8, u8) = (0x49, 0xA3, 0xB0); +const CLAUDE_BRAND: (u8, u8, u8) = (0xCC, 0x7C, 0x5E); + +fn color_by_provider_settings(style: codexbar::settings::MenuBarIconStyle) -> Settings { + Settings { + menu_bar_color_by_provider: true, + tray_icon_mode: TrayIconMode::Single, + menu_bar_icon_style: style, + show_as_used: true, + ..Settings::default() + } +} + +fn pixel_at(rgba: &[u8], x: u32, y: u32) -> [u8; 4] { + let index = ((y * codexbar::tray::TRAY_ICON_SIZE + x) * 4) as usize; + [ + rgba[index], + rgba[index + 1], + rgba[index + 2], + rgba[index + 3], + ] +} + +#[test] +fn color_by_provider_defaults_off_and_keeps_usage_colors() { + assert!(!Settings::default().menu_bar_color_by_provider); + let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)]; + let plain = Settings { + tray_icon_mode: TrayIconMode::Single, + show_as_used: true, + ..Settings::default() + }; + let plan = TrayPresentationPlan::resolve(&plain, &snapshots, None); + assert!(matches!( + plan.icon, + TrayIconPlan::Bars { + provider_tint: None, + .. + } + )); +} + +#[test] +fn color_by_provider_tints_single_meter_with_brand_color() { + let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)]; + let settings = color_by_provider_settings(codexbar::settings::MenuBarIconStyle::Bars); + let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None); + assert!(matches!( + plan.icon, + TrayIconPlan::Bars { + provider_tint: Some(CODEX_BRAND), + .. + } + )); + // 30% of the single meter fills x 4..11; x=8 sits in the fill. + let (rgba, _, _) = plan.render_icon(); + assert_eq!( + pixel_at(&rgba, 8, 16), + [CODEX_BRAND.0, CODEX_BRAND.1, CODEX_BRAND.2, 255] + ); +} + +#[test] +fn color_by_provider_tints_percent_icon_text() { + let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)]; + let settings = color_by_provider_settings(codexbar::settings::MenuBarIconStyle::Percent); + let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None); + assert!(matches!( + plan.icon, + TrayIconPlan::Percent { + provider_tint: Some(CODEX_BRAND), + .. + } + )); + let (rgba, _, _) = plan.render_icon(); + let tinted = rgba + .as_chunks::<4>() + .0 + .iter() + .filter(|px| px[..3] == [CODEX_BRAND.0, CODEX_BRAND.1, CODEX_BRAND.2]) + .count(); + assert!(tinted > 0, "percent glyphs should use the brand color"); +} + +#[test] +fn color_by_provider_uses_the_effective_accent_override() { + let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)]; + let mut settings = color_by_provider_settings(codexbar::settings::MenuBarIconStyle::Bars); + settings.set_accent_color(ProviderId::Codex, Some("#FF0000")); + let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None); + assert!(matches!( + plan.icon, + TrayIconPlan::Bars { + provider_tint: Some((255, 0, 0)), + .. + } + )); +} + +#[test] +fn color_by_provider_malformed_override_gives_no_tint() { + let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)]; + let mut settings = color_by_provider_settings(codexbar::settings::MenuBarIconStyle::Bars); + settings.set_accent_color(ProviderId::Codex, Some("not-a-color")); + let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None); + assert!(matches!( + plan.icon, + TrayIconPlan::Bars { + provider_tint: None, + .. + } + )); +} + +#[test] +fn color_by_provider_colors_each_stacked_row_by_its_provider() { + let snapshots = vec![ + fake_snapshot("codex", "Codex", 30.0), + fake_snapshot("claude", "Claude", 72.0), + ]; + let settings = Settings { + menu_bar_color_by_provider: true, + tray_icon_mode: TrayIconMode::Stacked, + stacked_tray_top_provider: Some("codex".to_string()), + stacked_tray_bottom_provider: Some("claude".to_string()), + show_as_used: true, + ..Settings::default() + }; + let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None); + assert!(matches!( + plan.icon, + TrayIconPlan::Stacked { + top_tint: Some(CODEX_BRAND), + bottom_tint: Some(CLAUDE_BRAND), + .. + } + )); + let (rgba, width, _) = plan.render_icon(); + let pixel = |x: u32, y: u32| { + let index = ((y * width + x) * 4) as usize; + [ + rgba[index], + rgba[index + 1], + rgba[index + 2], + rgba[index + 3], + ] + }; + // Top row covers y 6..14, bottom row y 18..26 of the stacked icon. + assert_eq!( + pixel(8, 8), + [CODEX_BRAND.0, CODEX_BRAND.1, CODEX_BRAND.2, 255] + ); + assert_eq!( + pixel(8, 20), + [CLAUDE_BRAND.0, CLAUDE_BRAND.1, CLAUDE_BRAND.2, 255] + ); +} + +#[test] +fn parse_hex_rgb_accepts_only_six_digit_hex() { + assert_eq!(parse_hex_rgb("#49A3B0"), Some(CODEX_BRAND)); + assert_eq!(parse_hex_rgb("49a3b0"), Some(CODEX_BRAND)); + assert_eq!(parse_hex_rgb(" #FF5733 "), Some((255, 87, 51))); + assert_eq!(parse_hex_rgb("#49A3"), None); + assert_eq!(parse_hex_rgb("#GG0000"), None); + assert_eq!(parse_hex_rgb(""), None); +} diff --git a/apps/desktop-tauri/src/App.test.tsx b/apps/desktop-tauri/src/App.test.tsx index 1ec854a634c..48bb48268af 100644 --- a/apps/desktop-tauri/src/App.test.tsx +++ b/apps/desktop-tauri/src/App.test.tsx @@ -96,6 +96,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { quotaWarningMarkersVisible: true, workdayTickStyle: "subtle", menuBarColorPace: false, + menuBarColorByProvider: false, menuBarPaceReservePositive: false, providersSortedAlphabetically: false, showAsUsed: true, diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index d791015972f..420f61d01bf 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -163,6 +163,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { quotaWarningMarkersVisible: true, workdayTickStyle: "subtle", menuBarColorPace: false, + menuBarColorByProvider: false, menuBarPaceReservePositive: false, providersSortedAlphabetically: false, showAsUsed: true, diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 57c0f2296ab..a1c31e2847a 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -786,6 +786,8 @@ export const ALL_LOCALE_KEYS = [ "ShowPercentInTrayHelper", "ColorPaceInTray", "ColorPaceInTrayHelper", + "ColorByProviderInTray", + "ColorByProviderInTrayHelper", "PaceSignLabel", "PaceSignHelper", "PaceSignAheadPositive", diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 03a19969886..160011178ff 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -162,6 +162,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { quotaWarningMarkersVisible: true, workdayTickStyle: "subtle", menuBarColorPace: false, + menuBarColorByProvider: false, menuBarPaceReservePositive: false, providersSortedAlphabetically: false, showAsUsed: true, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx index bc2506ec275..3ab255e405b 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx @@ -72,6 +72,7 @@ const settings: SettingsSnapshot = { quotaWarningMarkersVisible: true, workdayTickStyle: "subtle", menuBarColorPace: false, + menuBarColorByProvider: false, menuBarPaceReservePositive: false, providersSortedAlphabetically: false, showAsUsed: false, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx index 33e10e183bc..cc5fa550fab 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx @@ -40,6 +40,7 @@ const settings: SettingsSnapshot = { quotaWarningMarkersVisible: true, workdayTickStyle: "subtle", menuBarColorPace: false, + menuBarColorByProvider: false, menuBarPaceReservePositive: false, providersSortedAlphabetically: false, showAsUsed: false, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx index 179d6ad0675..500d219563d 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx @@ -75,6 +75,7 @@ const settings: SettingsSnapshot = { quotaWarningMarkersVisible: true, workdayTickStyle: "subtle", menuBarColorPace: false, + menuBarColorByProvider: false, menuBarPaceReservePositive: false, providersSortedAlphabetically: false, showAsUsed: false, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.test.tsx index 815194abc64..ac2f77c6834 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.test.tsx @@ -34,6 +34,7 @@ const baseSettings = { menuBarShowsPercent: false, menuBarIconStyle: "bars", menuBarColorPace: false, + menuBarColorByProvider: false, promoteTrayIcon: false, mergedOverviewSelectedProviders: [], } as unknown as SettingsSnapshot; @@ -63,6 +64,7 @@ describe("MenuBarTab layout", () => { expect(fieldLabels(screen.getByRole("region", { name: "MenuBarIconSectionTitle" }))).toEqual([ "MenuBarIconStyleLabel", "ColorPaceInTray", + "ColorByProviderInTray", "PaceSignLabel", "PromoteTrayIconLabel", ]); @@ -110,6 +112,22 @@ describe("MenuBarTab Icon section", () => { expect(set).toHaveBeenCalledWith({ menuBarColorPace: true }); }); + it("updates Color by provider and keeps it independent of the pace toggle", () => { + const { set } = renderTab(); + const colorByProvider = screen.getByRole("checkbox", { name: "ColorByProviderInTray" }) as HTMLInputElement; + expect(colorByProvider.checked).toBe(false); + fireEvent.click(colorByProvider); + expect(set).toHaveBeenCalledWith({ menuBarColorByProvider: true }); + expect(set).not.toHaveBeenCalledWith(expect.objectContaining({ menuBarColorPace: expect.anything() })); + }); + + it("shows a saved Color by provider as checked and not disabled by stacked icons", () => { + renderTab({ menuBarColorByProvider: true, trayIconMode: "stacked" }); + const colorByProvider = screen.getByRole("checkbox", { name: "ColorByProviderInTray" }) as HTMLInputElement; + expect(colorByProvider.checked).toBe(true); + expect(colorByProvider.disabled).toBe(false); + }); + it("saves the Pace sign with ahead-of-pace positive as the default", () => { const { set } = renderTab(); const paceSign = select("PaceSignLabel"); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx index a38f62114c8..77dbfac0118 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx @@ -96,6 +96,14 @@ export default function MenuBarTab({ onChange={(v) => set({ menuBarColorPace: v })} /> + + set({ menuBarColorByProvider: v })} + /> +