From 000e9ec6d360907c2103b53dfaf3550eef74f5ec Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:48:10 +0700 Subject: [PATCH 1/4] Add stackedBars menu bar icon style Add a MenuBarIconStyle::StackedBars option that draws session and weekly meters as two equal-height rows on the single tray icon. Reuses the existing bar row drawing and pace tint, falls back to the single-bar icon when no weekly window exists, and is accepted by preferences import. --- .../src-tauri/src/commands/settings.rs | 11 ++ .../src-tauri/src/tray_presentation.rs | 39 ++++-- .../src-tauri/src/tray_presentation_tests.rs | 109 +++++++++++++++ rust/src/settings/preferences_document.rs | 4 +- .../settings/preferences_document/tests.rs | 19 ++- rust/src/settings/tests.rs | 37 +++++- rust/src/settings/types.rs | 6 + rust/src/tray/mod.rs | 1 + rust/src/tray/render.rs | 124 ++++++++++++++++++ 9 files changed, 338 insertions(+), 12 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index fdcaf18209f..4d0e5af9578 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -1179,6 +1179,17 @@ mod tests { codexbar::settings::MenuBarIconStyle::Bars ); assert!(!settings.menu_bar_shows_percent); + + // Progress bars stacked is not the percent style, so the legacy flag stays off. + let update: SettingsUpdate = + serde_json::from_str(r#"{"menuBarIconStyle":"stackedBars"}"#).unwrap(); + assert!(update.refreshes_tray_presentation()); + update.apply_display_settings(&mut settings); + assert_eq!( + settings.menu_bar_icon_style, + codexbar::settings::MenuBarIconStyle::StackedBars + ); + assert!(!settings.menu_bar_shows_percent); } #[test] diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs index d8f0ac430fb..0b68c2d91bd 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs @@ -6,7 +6,7 @@ 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, + render_stacked_bar_icon_rgba, render_stacked_bars_icon_rgba_with_pace, }; #[derive(Debug, Clone, Copy, PartialEq)] @@ -16,6 +16,12 @@ enum TrayIconPlan { secondary_percent: Option, has_error: bool, }, + /// The same session and weekly windows as `Bars`, drawn as equal rows. + StackedBars { + primary_percent: f64, + secondary_percent: Option, + has_error: bool, + }, Percent { percent: f64, has_error: bool, @@ -157,7 +163,9 @@ impl<'a> TrayPresentationPlan<'a> { let icon_pace = match icon { TrayIconPlan::Stacked { .. } => None, - TrayIconPlan::Bars { .. } | TrayIconPlan::Percent { .. } => icon_pace, + TrayIconPlan::Bars { .. } + | TrayIconPlan::StackedBars { .. } + | TrayIconPlan::Percent { .. } => icon_pace, }; Self { @@ -184,6 +192,16 @@ impl<'a> TrayPresentationPlan<'a> { TrayIconPlan::Percent { percent, has_error } => { render_percent_icon_rgba_with_pace(percent, has_error, self.icon_pace) } + TrayIconPlan::StackedBars { + primary_percent, + secondary_percent, + has_error, + } => render_stacked_bars_icon_rgba_with_pace( + primary_percent, + secondary_percent, + has_error, + self.icon_pace, + ), TrayIconPlan::Stacked { top_percent, bottom_percent, @@ -214,17 +232,22 @@ fn resolve_single_provider_icon_plan( secondary_percent: Option, has_error: bool, ) -> TrayIconPlan { - if settings.menu_bar_icon_style == codexbar::settings::MenuBarIconStyle::Percent { - TrayIconPlan::Percent { + use codexbar::settings::MenuBarIconStyle; + match settings.menu_bar_icon_style { + MenuBarIconStyle::Percent => TrayIconPlan::Percent { percent: primary_percent, has_error, - } - } else { - TrayIconPlan::Bars { + }, + MenuBarIconStyle::StackedBars => TrayIconPlan::StackedBars { primary_percent, secondary_percent, has_error, - } + }, + MenuBarIconStyle::Bars => TrayIconPlan::Bars { + primary_percent, + secondary_percent, + has_error, + }, } } 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..3b87320ba90 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -990,3 +990,112 @@ 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"); } + +#[test] +fn stacked_bars_style_plans_the_same_windows_as_bars_as_equal_rows() { + let snapshots = vec![fake_snapshot_with( + "codex", + "Codex", + 30.0, + Some(65.0), + None, + None, + )]; + let bars_settings = Settings { + menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Bars, + show_as_used: true, + ..Settings::default() + }; + let stacked_settings = Settings { + menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::StackedBars, + show_as_used: true, + ..Settings::default() + }; + + let TrayIconPlan::Bars { + primary_percent, + secondary_percent, + has_error, + } = TrayPresentationPlan::resolve(&bars_settings, &snapshots, None).icon + else { + panic!("bars style should plan Bars"); + }; + let stacked = TrayPresentationPlan::resolve(&stacked_settings, &snapshots, None); + assert_eq!( + stacked.icon, + TrayIconPlan::StackedBars { + primary_percent, + secondary_percent, + has_error, + } + ); + assert_eq!( + stacked.render_icon(), + render_stacked_bars_icon_rgba_with_pace( + primary_percent, + secondary_percent, + has_error, + None + ) + ); + assert_ne!( + stacked.render_icon(), + TrayPresentationPlan::resolve(&bars_settings, &snapshots, None).render_icon() + ); +} + +#[test] +fn stacked_bars_style_with_one_window_draws_the_bars_fallback() { + let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)]; + let bars_settings = Settings { + menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Bars, + show_as_used: true, + ..Settings::default() + }; + let stacked_settings = Settings { + menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::StackedBars, + ..bars_settings.clone() + }; + let bars = TrayPresentationPlan::resolve(&bars_settings, &snapshots, None); + let stacked = TrayPresentationPlan::resolve(&stacked_settings, &snapshots, None); + assert!(matches!( + stacked.icon, + TrayIconPlan::StackedBars { + secondary_percent: None, + .. + } + )); + assert_eq!(stacked.render_icon(), bars.render_icon()); +} + +#[test] +fn stacked_bars_style_keeps_the_pace_tint_on_both_rows() { + let settings = Settings { + menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::StackedBars, + ..tint_settings() + }; + let snapshots = vec![paced_snapshot( + "synthetic", + paced_window(70.0, Some(300), 150), + )]; + let plan = TrayPresentationPlan::resolve_at(&settings, &snapshots, None, pace_now()); + assert_eq!(plan.icon_pace, Some(TrayPaceColor::Ahead)); + + let TrayIconPlan::StackedBars { + primary_percent, + secondary_percent, + has_error, + } = plan.icon + else { + panic!("stacked bars style should plan StackedBars"); + }; + assert_eq!( + plan.render_icon(), + render_stacked_bars_icon_rgba_with_pace( + primary_percent, + secondary_percent, + has_error, + Some(TrayPaceColor::Ahead), + ) + ); +} diff --git a/rust/src/settings/preferences_document.rs b/rust/src/settings/preferences_document.rs index b40d73808fc..5fecdb0e6dc 100644 --- a/rust/src/settings/preferences_document.rs +++ b/rust/src/settings/preferences_document.rs @@ -92,7 +92,9 @@ const ALLOWED_PREFERENCES: &[(&str, Validator)] = &[ ("hide_personal_info", typed::), ("menu_bar_shows_highest_usage", typed::), ("menu_bar_shows_percent", typed::), - ("menu_bar_icon_style", |v| one_of(v, &["bars", "percent"])), + ("menu_bar_icon_style", |v| { + one_of(v, &["bars", "percent", "stackedBars"]) + }), ( "merged_overview_selected_providers", merged_overview_selection, diff --git a/rust/src/settings/preferences_document/tests.rs b/rust/src/settings/preferences_document/tests.rs index 450019f781e..9bd0af8f1a8 100644 --- a/rust/src/settings/preferences_document/tests.rs +++ b/rust/src/settings/preferences_document/tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::settings::{ - Language, ProviderConfig, ProviderQuotaWarnings, QuotaWarningWindow, ThemePreference, - TrayIconMode, + Language, MenuBarIconStyle, ProviderConfig, ProviderQuotaWarnings, QuotaWarningWindow, + ThemePreference, TrayIconMode, }; use serde_json::json; use std::collections::BTreeMap; @@ -181,6 +181,21 @@ fn round_trips_every_allowlisted_preference() { assert_eq!(target.global_shortcut, "Ctrl+Alt+K"); } +#[test] +fn menu_bar_icon_style_import_accepts_stacked_bars_and_rejects_junk() { + let document = + parse(json!({ "menu_bar_icon_style": "stackedBars" })).expect("stackedBars imports"); + let mut target = Settings::default(); + document.apply_to(&mut target).expect("apply"); + assert_eq!(target.menu_bar_icon_style, MenuBarIconStyle::StackedBars); + + let rejected = parse(json!({ "menu_bar_icon_style": "stacked_bars" })); + assert!( + rejected.is_err(), + "unknown style must be rejected on import" + ); +} + #[test] fn export_is_deterministic_and_sorted() { let first = PreferencesDocument::from_settings(&customized_settings()) diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index 82f1548ce43..54a4d0207f9 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -2080,7 +2080,11 @@ fn stored_menu_bar_icon_style_wins_over_the_legacy_flag() { #[test] fn menu_bar_icon_style_round_trips() { - for style in [MenuBarIconStyle::Bars, MenuBarIconStyle::Percent] { + for style in [ + MenuBarIconStyle::Bars, + MenuBarIconStyle::Percent, + MenuBarIconStyle::StackedBars, + ] { let settings = Settings { menu_bar_icon_style: style, // The legacy flag disagrees on purpose: the stored key must win. @@ -2094,6 +2098,37 @@ fn menu_bar_icon_style_round_trips() { } } +#[test] +fn stacked_bars_icon_style_loads_from_a_settings_file() { + assert_eq!( + MenuBarIconStyle::StackedBars.as_str(), + "stackedBars", + "the id is the camelCase string the bridge and the file both use" + ); + assert_eq!( + MenuBarIconStyle::parse("stackedBars"), + Some(MenuBarIconStyle::StackedBars) + ); + assert_eq!( + MenuBarIconStyle::parse("STACKEDBARS"), + Some(MenuBarIconStyle::StackedBars) + ); + assert_eq!(MenuBarIconStyle::parse("stacked_bars"), None); + + let stored: Settings = serde_json::from_str( + r#"{ "enabled_providers": [], "menu_bar_shows_percent": false, "menu_bar_icon_style": "stackedBars" }"#, + ) + .expect("stackedBars file loads"); + assert_eq!(stored.menu_bar_icon_style, MenuBarIconStyle::StackedBars); + + // Unknown ids keep the legacy fallback, so a bad value never breaks the file. + let unknown: Settings = serde_json::from_str( + r#"{ "enabled_providers": [], "menu_bar_shows_percent": false, "menu_bar_icon_style": "stacked_bars" }"#, + ) + .expect("unknown style still loads"); + assert_eq!(unknown.menu_bar_icon_style, MenuBarIconStyle::Bars); +} + #[test] fn merged_overview_selection_defaults_empty_and_round_trips() { assert!( diff --git a/rust/src/settings/types.rs b/rust/src/settings/types.rs index 06ce0763dee..abc6994fa0f 100644 --- a/rust/src/settings/types.rs +++ b/rust/src/settings/types.rs @@ -629,6 +629,10 @@ pub enum MenuBarIconStyle { Bars, /// Provider icon plus the percent text. Percent, + /// Session and weekly meters drawn as two equal-height rows (upstream + /// "Progress bars stacked", #4436). Older builds read it as `Bars`. + #[serde(rename = "stackedBars")] + StackedBars, } impl MenuBarIconStyle { @@ -637,6 +641,7 @@ impl MenuBarIconStyle { match value.trim().to_ascii_lowercase().as_str() { "bars" => Some(Self::Bars), "percent" => Some(Self::Percent), + "stackedbars" => Some(Self::StackedBars), _ => None, } } @@ -645,6 +650,7 @@ impl MenuBarIconStyle { match self { Self::Bars => "bars", Self::Percent => "percent", + Self::StackedBars => "stackedBars", } } } diff --git a/rust/src/tray/mod.rs b/rust/src/tray/mod.rs index 52a8d64ec4b..0e8e9f2aa02 100755 --- a/rust/src/tray/mod.rs +++ b/rust/src/tray/mod.rs @@ -9,4 +9,5 @@ pub use icon::LoadingPattern; pub use render::{ TRAY_ICON_SIZE, TrayPaceColor, render_bar_icon_rgba, render_bar_icon_rgba_with_pace, render_percent_icon_rgba, render_percent_icon_rgba_with_pace, render_stacked_bar_icon_rgba, + render_stacked_bars_icon_rgba_with_pace, }; diff --git a/rust/src/tray/render.rs b/rust/src/tray/render.rs index bb516bc7ee9..6037558e2ec 100644 --- a/rust/src/tray/render.rs +++ b/rust/src/tray/render.rs @@ -159,6 +159,28 @@ pub fn render_stacked_bar_icon_rgba( (image.into_raw(), TRAY_ICON_SIZE, TRAY_ICON_SIZE) } +/// Render the session and weekly meters of one provider as two equal-height +/// rows (upstream "Progress bars stacked", #4436), with the same optional pace +/// tint as [`render_bar_icon_rgba_with_pace`]. +/// +/// With no weekly window the output is exactly the single thick bar from +/// [`render_bar_icon_rgba_with_pace`], so a provider with one window looks the +/// same under either bar style. +pub fn render_stacked_bars_icon_rgba_with_pace( + session_percent: f64, + weekly_percent: Option, + has_error: bool, + pace_color: Option, +) -> (Vec, u32, u32) { + let Some(weekly) = weekly_percent else { + return render_bar_icon_rgba_with_pace(session_percent, None, has_error, pace_color); + }; + let mut image = new_icon_canvas(has_error); + draw_bar_row(&mut image, 6, 14, session_percent, has_error, pace_color); + draw_bar_row(&mut image, 18, 26, weekly, has_error, pace_color); + (image.into_raw(), TRAY_ICON_SIZE, TRAY_ICON_SIZE) +} + /// Render a compact numeric percent tray icon as raw RGBA bytes. pub fn render_percent_icon_rgba(percent: f64, has_error: bool) -> (Vec, u32, u32) { render_percent_icon_rgba_with_pace(percent, has_error, None) @@ -417,4 +439,106 @@ mod tests { assert_eq!(pixel(&normal, 8, 12), pixel(&stacked, 8, 8)); assert_eq!(pixel(&normal, 8, 12)[0], pixel(&normal, 8, 12)[1]); } + + fn pixel_at(rgba: &[u8], x: u32, y: u32) -> [u8; 4] { + let index = ((y * TRAY_ICON_SIZE + x) * 4) as usize; + [ + rgba[index], + rgba[index + 1], + rgba[index + 2], + rgba[index + 3], + ] + } + + /// Rows in `rows` whose pixel at column `x` is part of a bar (track or fill), + /// i.e. not the icon background gap. + fn painted_rows(rgba: &[u8], x: u32, rows: std::ops::Range) -> Vec { + rows.filter(|&y| pixel_at(rgba, x, y) != [60, 60, 70, 255]) + .collect() + } + + /// Horizontal extent `(first_x, end_x)` of usage-coloured pixels (neither the + /// track nor the background) across `rows`, or `None` when nothing is filled. + fn filled_span(rgba: &[u8], rows: std::ops::Range) -> Option<(u32, u32)> { + let mut span: Option<(u32, u32)> = None; + for y in rows { + for x in BAR_LEFT..BAR_RIGHT { + let px = pixel_at(rgba, x, y); + if px != [80, 80, 90, 255] && px != [60, 60, 70, 255] { + span = Some(match span { + None => (x, x + 1), + Some((first, end)) => (first.min(x), end.max(x + 1)), + }); + } + } + } + span + } + + #[test] + fn stacked_bars_draws_two_equal_height_rows() { + let (rgba, width, height) = + render_stacked_bars_icon_rgba_with_pace(100.0, Some(0.0), false, None); + assert_eq!((width, height), (TRAY_ICON_SIZE, TRAY_ICON_SIZE)); + + let (r, g, b) = UsageLevel::Critical.color(); + // Session row is filled to the end; weekly row is empty track. + assert_eq!(pixel_at(&rgba, 8, 8), [r, g, b, 255]); + assert_eq!(pixel_at(&rgba, 8, 20), [80, 80, 90, 255]); + // Gap between the rows keeps the icon background. + assert_eq!(pixel_at(&rgba, 8, 15), [60, 60, 70, 255]); + + // Both rows are painted over the same 8-pixel height, separated by a 4-pixel gap. + let top: Vec = (6..14).collect(); + let bottom: Vec = (18..26).collect(); + // Rows 2..30 are the inset canvas; the outer border is transparent. + assert_eq!( + painted_rows(&rgba, 20, 2..30), + [top.clone(), bottom.clone()].concat() + ); + assert_eq!(top.len(), bottom.len()); + } + + #[test] + fn stacked_bars_fill_widths_follow_each_window() { + let (rgba, _, _) = render_stacked_bars_icon_rgba_with_pace(50.0, Some(100.0), false, None); + // Session at 50% fills half of the 24-px track (x 4..16); weekly fills all of it. + assert_eq!(filled_span(&rgba, 6..14), Some((BAR_LEFT, BAR_LEFT + 12))); + assert_eq!(filled_span(&rgba, 18..26), Some((BAR_LEFT, BAR_RIGHT))); + // Nothing fills the gap between the rows. + assert_eq!(filled_span(&rgba, 14..18), None); + } + + #[test] + fn stacked_bars_without_weekly_matches_single_bars_fallback() { + let (stacked, _, _) = render_stacked_bars_icon_rgba_with_pace(42.0, None, false, None); + let (bars, _, _) = render_bar_icon_rgba_with_pace(42.0, None, false, None); + assert_eq!(stacked, bars); + + let (stacked_err, _, _) = render_stacked_bars_icon_rgba_with_pace(42.0, None, true, None); + let (bars_err, _, _) = render_bar_icon_rgba_with_pace(42.0, None, true, None); + assert_eq!(stacked_err, bars_err); + } + + #[test] + fn stacked_bars_applies_pace_tint_to_both_rows() { + let (rgba, _, _) = render_stacked_bars_icon_rgba_with_pace( + 100.0, + Some(100.0), + false, + Some(TrayPaceColor::Behind), + ); + assert_eq!(pixel_at(&rgba, 8, 8), [76, 175, 80, 255]); + assert_eq!(pixel_at(&rgba, 8, 20), [76, 175, 80, 255]); + } + + #[test] + fn stacked_bars_error_state_desaturates_both_rows() { + let (rgba, _, _) = render_stacked_bars_icon_rgba_with_pace(100.0, Some(100.0), true, None); + for (x, y) in [(8, 8), (8, 20)] { + let px = pixel_at(&rgba, x, y); + assert_eq!(px[0], px[1]); + assert_eq!(px[1], px[2]); + } + } } From 302519a7a5202e8f49f8f579ffb8c67885c3d968 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:48:12 +0700 Subject: [PATCH 2/4] Add progress bars stacked option to menu bar settings Expose the stackedBars style in the Menu Bar icon style dropdown with a localized label in all bundled locales. --- apps/desktop-tauri/src/i18n/keys.ts | 1 + .../surfaces/settings/tabs/MenuBarTab.test.tsx | 18 ++++++++++++++++++ .../src/surfaces/settings/tabs/MenuBarTab.tsx | 1 + apps/desktop-tauri/src/types/bridge.ts | 2 +- rust/src/locale.rs | 1 + rust/src/locale/en-US.ftl | 1 + rust/src/locale/es-MX.ftl | 1 + rust/src/locale/ja-JP.ftl | 1 + rust/src/locale/ko-KR.ftl | 1 + rust/src/locale/pt-BR.ftl | 1 + rust/src/locale/ru-RU.ftl | 1 + rust/src/locale/tr-TR.ftl | 1 + rust/src/locale/uk-UA.ftl | 1 + rust/src/locale/zh-CN.ftl | 1 + rust/src/locale/zh-TW.ftl | 1 + 15 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 57c0f2296ab..95f8c1a3c79 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -1597,6 +1597,7 @@ export const ALL_LOCALE_KEYS = [ "MenuBarIconStyleHelper", "MenuBarIconStyleBars", "MenuBarIconStylePercent", + "MenuBarIconStyleStackedBars", "CombinedIconSectionTitle", "CombinedIconStyleLabel", "CombinedIconStyleSwitcher", 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..583a97b92d3 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.test.tsx @@ -99,6 +99,24 @@ describe("MenuBarTab Icon section", () => { expect(set).toHaveBeenCalledWith({ menuBarIconStyle: "percent" }); }); + it("offers Progress bars stacked after the other menu bar styles and saves it", () => { + const { set } = renderTab(); + const options = Array.from(select("MenuBarIconStyleLabel").options).map((o) => [o.value, o.textContent]); + expect(options).toEqual([ + ["bars", "MenuBarIconStyleBars"], + ["percent", "MenuBarIconStylePercent"], + ["stackedBars", "MenuBarIconStyleStackedBars"], + ]); + fireEvent.change(select("MenuBarIconStyleLabel"), { target: { value: "stackedBars" } }); + expect(set).toHaveBeenCalledWith({ menuBarIconStyle: "stackedBars" }); + }); + + it("shows a saved Progress bars stacked style", () => { + renderTab({ menuBarIconStyle: "stackedBars" }); + expect(select("MenuBarIconStyleLabel").value).toBe("stackedBars"); + expect(select("MenuBarIconStyleLabel").disabled).toBe(false); + }); + it("disables the Menu bar style while the icon is stacked", () => { renderTab({ trayIconMode: "stacked" }); expect(select("MenuBarIconStyleLabel").disabled).toBe(true); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx index a38f62114c8..103e04f3dd0 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/MenuBarTab.tsx @@ -84,6 +84,7 @@ export default function MenuBarTab({ options={[ { value: "bars", label: t("MenuBarIconStyleBars") }, { value: "percent", label: t("MenuBarIconStylePercent") }, + { value: "stackedBars", label: t("MenuBarIconStyleStackedBars") }, ]} onChange={(v) => set({ menuBarIconStyle: v as MenuBarIconStyle })} /> diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 427b0362fec..b5d2073dcbc 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -79,7 +79,7 @@ export type OverviewLayout = "detailed" | "compact"; /** How cost is rendered on provider MenuCards (#2976). */ export type CostSummaryDisplayStyle = "compact" | "detailed" | "hidden"; /** Settings > Menu Bar > Menu bar style (`menu_bar_icon_style`). */ -export type MenuBarIconStyle = "bars" | "percent"; +export type MenuBarIconStyle = "bars" | "percent" | "stackedBars"; /** Settings > Menu > Workday tick appearance (`workday_tick_style`). */ export type WorkdayTickStyle = "hidden" | "subtle" | "high_contrast"; export type FloatBarOrientation = "horizontal" | "vertical"; diff --git a/rust/src/locale.rs b/rust/src/locale.rs index a6b2a385b96..8a9e456b52f 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -1886,6 +1886,7 @@ locale_keys! { MenuBarIconStyleHelper, MenuBarIconStyleBars, MenuBarIconStylePercent, + MenuBarIconStyleStackedBars, CombinedIconSectionTitle, CombinedIconStyleLabel, CombinedIconStyleSwitcher, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index b83dbe84c07..0425159d832 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -1556,6 +1556,7 @@ MenuBarIconStyleLabel = Menu bar style MenuBarIconStyleHelper = How the tray icon is drawn. MenuBarIconStyleBars = Meter bars MenuBarIconStylePercent = Icon & percent +MenuBarIconStyleStackedBars = Progress bars stacked CombinedIconSectionTitle = Combined icon CombinedIconStyleLabel = Combined icon style CombinedIconStyleSwitcher = Switcher diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index b8bce674f9f..0887a2e1acf 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -1553,6 +1553,7 @@ MenuBarIconStyleLabel = Estilo de la barra de menú MenuBarIconStyleHelper = Cómo se dibuja el icono de la bandeja. MenuBarIconStyleBars = Barras de medidor MenuBarIconStylePercent = Icono y porcentaje +MenuBarIconStyleStackedBars = Barras de progreso apiladas CombinedIconSectionTitle = Icono combinado CombinedIconStyleLabel = Estilo del icono combinado CombinedIconStyleSwitcher = Selector diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index b79d46f0b25..2675fe25605 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -1554,6 +1554,7 @@ MenuBarIconStyleLabel = メニューバーのスタイル MenuBarIconStyleHelper = トレイアイコンの描画方法。 MenuBarIconStyleBars = メーターバー MenuBarIconStylePercent = アイコンとパーセント +MenuBarIconStyleStackedBars = 進度バーのスタック CombinedIconSectionTitle = 統合アイコン CombinedIconStyleLabel = 統合アイコンのスタイル CombinedIconStyleSwitcher = 切り替え diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 23b1fad68b8..875b93fe1ff 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -1553,6 +1553,7 @@ MenuBarIconStyleLabel = 메뉴 바 스타일 MenuBarIconStyleHelper = 트레이 아이콘을 그리는 방식입니다. MenuBarIconStyleBars = 미터 막대 MenuBarIconStylePercent = 아이콘 및 퍼센트 +MenuBarIconStyleStackedBars = 진행률 막대 겹침 CombinedIconSectionTitle = 통합 아이콘 CombinedIconStyleLabel = 통합 아이콘 스타일 CombinedIconStyleSwitcher = 전환기 diff --git a/rust/src/locale/pt-BR.ftl b/rust/src/locale/pt-BR.ftl index 76bcab0ed79..2873726f7e2 100644 --- a/rust/src/locale/pt-BR.ftl +++ b/rust/src/locale/pt-BR.ftl @@ -1558,6 +1558,7 @@ MenuBarIconStyleLabel = Estilo da barra de menus MenuBarIconStyleHelper = Como o ícone da bandeja é desenhado. MenuBarIconStyleBars = Barras de medidor MenuBarIconStylePercent = Ícone e porcentagem +MenuBarIconStyleStackedBars = Barras de progresso empilhadas CombinedIconSectionTitle = Ícone combinado CombinedIconStyleLabel = Estilo do ícone combinado CombinedIconStyleSwitcher = Alternador diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index c8673d5fb85..977f89e1903 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -1554,6 +1554,7 @@ MenuBarIconStyleLabel = Стиль значка MenuBarIconStyleHelper = Как отображается значок в трее. MenuBarIconStyleBars = Шкалы MenuBarIconStylePercent = Значок и процент +MenuBarIconStyleStackedBars = Столбцы прогресса штабелем CombinedIconSectionTitle = Общий значок CombinedIconStyleLabel = Стиль общего значка CombinedIconStyleSwitcher = Переключатель diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index 71698e3c5ba..0971843f6cf 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -1554,6 +1554,7 @@ MenuBarIconStyleLabel = Menü çubuğu stili MenuBarIconStyleHelper = Tepsi simgesinin nasıl çizileceği. MenuBarIconStyleBars = Ölçüm çubukları MenuBarIconStylePercent = Simge ve yüzde +MenuBarIconStyleStackedBars = İstiflenmiş ilerleme çubukları CombinedIconSectionTitle = Birleşik simge CombinedIconStyleLabel = Birleşik simge stili CombinedIconStyleSwitcher = Değiştirici diff --git a/rust/src/locale/uk-UA.ftl b/rust/src/locale/uk-UA.ftl index faa9aefef54..8c1d7413cca 100644 --- a/rust/src/locale/uk-UA.ftl +++ b/rust/src/locale/uk-UA.ftl @@ -1552,6 +1552,7 @@ MenuBarIconStyleLabel = Стиль значка MenuBarIconStyleHelper = Як відображається значок у треї. MenuBarIconStyleBars = Шкали MenuBarIconStylePercent = Значок і відсоток +MenuBarIconStyleStackedBars = Індикатори прогресу штабелем CombinedIconSectionTitle = Спільний значок CombinedIconStyleLabel = Стиль спільного значка CombinedIconStyleSwitcher = Перемикач diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 5711873033e..583c88ec4b6 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -1554,6 +1554,7 @@ MenuBarIconStyleLabel = 菜单栏样式 MenuBarIconStyleHelper = 托盘图标的绘制方式。 MenuBarIconStyleBars = 计量条 MenuBarIconStylePercent = 图标和百分比 +MenuBarIconStyleStackedBars = 进度条堆叠 CombinedIconSectionTitle = 合并图标 CombinedIconStyleLabel = 合并图标样式 CombinedIconStyleSwitcher = 切换器 diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index b519c31e713..790fbd1a70b 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -1554,6 +1554,7 @@ MenuBarIconStyleLabel = 選單欄樣式 MenuBarIconStyleHelper = 系統匣圖示的繪製方式。 MenuBarIconStyleBars = 計量條 MenuBarIconStylePercent = 圖示與百分比 +MenuBarIconStyleStackedBars = 進度條堆疊 CombinedIconSectionTitle = 合併圖示 CombinedIconStyleLabel = 合併圖示樣式 CombinedIconStyleSwitcher = 切換器 From 9383f39c24be918edf9c39b10ee2c153997411be Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:48:17 +0700 Subject: [PATCH 3/4] Note progress bars stacked in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeed216403d..2a97708f005 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## [Windows] Unreleased +- Tray: add a **Progress bars stacked** menu bar style that draws the session and weekly meters as two equal-height rows on the single tray icon, with the same windows and pace tint as **Meter bars**; older settings files keep their style (upstream #4436). - 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). From 34052fb9bb6577d3906065f96ac646370a791be9 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 10:01:55 +0700 Subject: [PATCH 4/4] Clarify stacked bars label in Korean and Russian --- rust/src/locale/ko-KR.ftl | 2 +- rust/src/locale/ru-RU.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 875b93fe1ff..e2dc5395f7d 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -1553,7 +1553,7 @@ MenuBarIconStyleLabel = 메뉴 바 스타일 MenuBarIconStyleHelper = 트레이 아이콘을 그리는 방식입니다. MenuBarIconStyleBars = 미터 막대 MenuBarIconStylePercent = 아이콘 및 퍼센트 -MenuBarIconStyleStackedBars = 진행률 막대 겹침 +MenuBarIconStyleStackedBars = 진행률 막대 세로로 쌓기 CombinedIconSectionTitle = 통합 아이콘 CombinedIconStyleLabel = 통합 아이콘 스타일 CombinedIconStyleSwitcher = 전환기 diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index 977f89e1903..ff9b7feb6e0 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -1554,7 +1554,7 @@ MenuBarIconStyleLabel = Стиль значка MenuBarIconStyleHelper = Как отображается значок в трее. MenuBarIconStyleBars = Шкалы MenuBarIconStylePercent = Значок и процент -MenuBarIconStyleStackedBars = Столбцы прогресса штабелем +MenuBarIconStyleStackedBars = Полосы прогресса друг над другом CombinedIconSectionTitle = Общий значок CombinedIconStyleLabel = Стиль общего значка CombinedIconStyleSwitcher = Переключатель