Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,10 @@ pub struct SettingsSnapshot {
switcher_shows_icons: bool,
menu_bar_shows_highest_usage: bool,
menu_bar_shows_percent: bool,
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle,
merged_overview_selected_providers: Vec<String>,
quota_warning_markers_visible: bool,
workday_tick_style: codexbar::settings::WorkdayTickStyle,
menu_bar_color_pace: bool,
providers_sorted_alphabetically: bool,
show_as_used: bool,
Expand Down Expand Up @@ -982,6 +986,10 @@ impl From<Settings> for SettingsSnapshot {
switcher_shows_icons: settings.switcher_shows_icons,
menu_bar_shows_highest_usage: settings.menu_bar_shows_highest_usage,
menu_bar_shows_percent: settings.menu_bar_shows_percent,
menu_bar_icon_style: settings.menu_bar_icon_style,
merged_overview_selected_providers: settings.merged_overview_selected_providers,
quota_warning_markers_visible: settings.quota_warning_markers_visible,
workday_tick_style: settings.workday_tick_style,
menu_bar_color_pace: settings.menu_bar_color_pace,
providers_sorted_alphabetically: settings.providers_sorted_alphabetically,
show_as_used: settings.show_as_used,
Expand Down
126 changes: 122 additions & 4 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub struct SettingsUpdate {
pub switcher_shows_icons: Option<bool>,
pub menu_bar_shows_highest_usage: Option<bool>,
pub menu_bar_shows_percent: Option<bool>,
pub menu_bar_icon_style: Option<codexbar::settings::MenuBarIconStyle>,
pub merged_overview_selected_providers: Option<Vec<String>>,
pub quota_warning_markers_visible: Option<bool>,
pub workday_tick_style: Option<codexbar::settings::WorkdayTickStyle>,
pub menu_bar_color_pace: Option<bool>,
pub providers_sorted_alphabetically: Option<bool>,
pub show_as_used: Option<bool>,
Expand Down Expand Up @@ -91,7 +95,9 @@ pub struct SettingsUpdate {
/// Optional user-entered Copilot seat AI-credit allowance; `null` clears it.
#[serde(default, deserialize_with = "deserialize_double_option")]
pub copilot_seat_credit_entitlement: Option<Option<f64>>,
pub weekly_progress_work_days: Option<u8>,
/// Work-week length (2..=7); `null` or an out-of-range value selects Automatic.
#[serde(default, deserialize_with = "deserialize_double_option")]
pub weekly_progress_work_days: Option<Option<u8>>,
pub cost_summary_display_style: Option<String>,
pub open_codex_usage_logs_enabled: Option<bool>,
pub hide_native_codex_cost_when_open_codex_present: Option<bool>,
Expand Down Expand Up @@ -139,6 +145,7 @@ impl SettingsUpdate {
|| self.switcher_shows_icons.is_some()
|| self.menu_bar_shows_highest_usage.is_some()
|| self.menu_bar_shows_percent.is_some()
|| self.menu_bar_icon_style.is_some()
|| self.menu_bar_color_pace.is_some()
|| self.show_as_used.is_some()
|| self.reset_time_relative.is_some()
Expand Down Expand Up @@ -295,6 +302,28 @@ impl SettingsUpdate {
}
if let Some(v) = self.menu_bar_shows_percent {
settings.menu_bar_shows_percent = v;
// Legacy callers that still send the flag drive the new key too.
if self.menu_bar_icon_style.is_none() {
settings.menu_bar_icon_style = if v {
codexbar::settings::MenuBarIconStyle::Percent
} else {
codexbar::settings::MenuBarIconStyle::Bars
};
}
}
if let Some(v) = self.menu_bar_icon_style {
settings.menu_bar_icon_style = v;
// Keep the legacy flag in step for one release so a downgrade keeps the look.
settings.menu_bar_shows_percent = v == codexbar::settings::MenuBarIconStyle::Percent;
}
if let Some(ids) = self.merged_overview_selected_providers.clone() {
settings.set_merged_overview_selected_providers(ids);
}
if let Some(v) = self.quota_warning_markers_visible {
settings.quota_warning_markers_visible = v;
}
if let Some(v) = self.workday_tick_style {
settings.workday_tick_style = v;
}
if let Some(v) = self.menu_bar_color_pace {
settings.menu_bar_color_pace = v;
Expand Down Expand Up @@ -437,7 +466,8 @@ impl SettingsUpdate {
);
}
if let Some(v) = self.weekly_progress_work_days {
settings.weekly_progress_work_days = if (2..=6).contains(&v) { Some(v) } else { None };
settings.weekly_progress_work_days =
codexbar::settings::normalize_weekly_progress_work_days(v);
}
if let Some(v) = self
.cost_summary_display_style
Expand Down Expand Up @@ -494,11 +524,14 @@ impl SettingsUpdate {
}
}

fn deserialize_double_option<'de, D>(deserializer: D) -> Result<Option<Option<f64>>, D::Error>
/// Present key → `Some(value)`, explicit `null` → `Some(None)`; a missing key
/// stays `None` through `#[serde(default)]`.
fn deserialize_double_option<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Ok(Some(Option::<f64>::deserialize(deserializer)?))
Ok(Some(Option::<T>::deserialize(deserializer)?))
}

fn normalize_custom_sessions_dirs(dirs: Vec<String>) -> Vec<String> {
Expand Down Expand Up @@ -859,6 +892,91 @@ mod tests {
assert!(!settings.menu_bar_color_pace);
}

#[test]
fn menu_bar_icon_style_update_repaints_tray_and_mirrors_legacy_flag() {
let mut settings = Settings::default();
let update: SettingsUpdate =
serde_json::from_str(r#"{"menuBarIconStyle":"percent"}"#).unwrap();
assert!(update.refreshes_tray_presentation());
update.apply_display_settings(&mut settings);
assert_eq!(
settings.menu_bar_icon_style,
codexbar::settings::MenuBarIconStyle::Percent
);
assert!(settings.menu_bar_shows_percent);

let update: SettingsUpdate =
serde_json::from_str(r#"{"menuBarIconStyle":"bars"}"#).unwrap();
update.apply_display_settings(&mut settings);
assert_eq!(
settings.menu_bar_icon_style,
codexbar::settings::MenuBarIconStyle::Bars
);
assert!(!settings.menu_bar_shows_percent);
}

#[test]
fn menu_pane_keys_apply_from_camel_case_patch() {
let mut settings = Settings::default();
let update: SettingsUpdate = serde_json::from_str(
r#"{"quotaWarningMarkersVisible":false,"workdayTickStyle":"high_contrast",
"mergedOverviewSelectedProviders":["claude","Codex","claude","gemini","cursor","copilot","zai","kimi"],
"weeklyProgressWorkDays":7}"#,
)
.unwrap();
update
.apply_display_settings(&mut settings)
.apply_advanced_settings(&mut settings);
assert!(!settings.quota_warning_markers_visible);
assert_eq!(
settings.workday_tick_style,
codexbar::settings::WorkdayTickStyle::HighContrast
);
assert_eq!(
settings.merged_overview_selected_providers,
["claude", "codex", "gemini", "cursor", "copilot", "zai"]
.map(String::from)
.to_vec()
);
assert_eq!(settings.weekly_progress_work_days, Some(7));

let update: SettingsUpdate =
serde_json::from_str(r#"{"weeklyProgressWorkDays":8}"#).unwrap();
update
.apply_display_settings(&mut settings)
.apply_advanced_settings(&mut settings);
assert_eq!(settings.weekly_progress_work_days, None);

settings.weekly_progress_work_days = Some(5);
let update: SettingsUpdate =
serde_json::from_str(r#"{"weeklyProgressWorkDays":null}"#).unwrap();
assert!(update.refreshes_provider_data());
update
.apply_display_settings(&mut settings)
.apply_advanced_settings(&mut settings);
assert_eq!(
settings.weekly_progress_work_days, None,
"null selects Automatic"
);
}

#[test]
fn settings_snapshot_emits_snake_case_style_values() {
let settings = Settings {
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Percent,
workday_tick_style: codexbar::settings::WorkdayTickStyle::HighContrast,
..Settings::default()
};
let json = serde_json::to_value(crate::commands::SettingsSnapshot::from(settings)).unwrap();
assert_eq!(json["menuBarIconStyle"], "percent");
assert_eq!(json["workdayTickStyle"], "high_contrast");
assert_eq!(json["quotaWarningMarkersVisible"], true);
assert_eq!(
json["mergedOverviewSelectedProviders"],
serde_json::json!([])
);
}

#[test]
fn providers_sidebar_sort_applies_without_touching_order_or_tray() {
let mut settings = Settings::default();
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ fn main() {
let proof_config = proof_harness::ProofConfig::from_env();
let is_proof_mode = proof_config.is_some();
let force_start_visible = std::env::var_os("CODEXBAR_START_VISIBLE").is_some();
let settings = codexbar::settings::Settings::load();
let mut settings = codexbar::settings::Settings::load();
settings.apply_display_mode_fold_migration();
let launch = launch_behavior(
force_start_visible,
settings.start_minimized,
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,8 @@ pub(crate) fn refresh_tray_presentation(app: &AppHandle) {
/// Update the tray icon pixels and tooltip text to reflect current provider usage.
///
/// Behaviour mirrors egui's `choose_tray_update_plan` (rust/src/native_ui/app.rs):
/// - If `menu_bar_shows_highest_usage` is on OR `menu_bar_display_mode == "minimal"`,
/// render the bar from the healthy provider with the highest session usage.
/// - If `menu_bar_shows_highest_usage` is on (legacy Display mode "minimal" is
/// folded into it on load), render the bar from the healthy provider with the highest session usage.
/// - Otherwise render from the first enabled healthy provider (catalog order).
/// - When any provider exposes a weekly/secondary window, the icon shows both
/// bars from the same picked provider.
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop-tauri/src-tauri/src/tray_presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ impl<'a> TrayPresentationPlan<'a> {
.filter(|snapshot| snapshot.error.is_none())
.collect::<Vec<_>>();
let has_error = healthy.is_empty() && !snapshots.is_empty();
let prefer_highest =
settings.menu_bar_shows_highest_usage || settings.menu_bar_display_mode == "minimal";
// The retired Display mode "minimal" is folded into this flag once on load.
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);

Expand Down Expand Up @@ -202,7 +202,7 @@ fn resolve_single_provider_icon_plan(
secondary_percent: Option<f64>,
has_error: bool,
) -> TrayIconPlan {
if settings.menu_bar_shows_percent {
if settings.menu_bar_icon_style == codexbar::settings::MenuBarIconStyle::Percent {
TrayIconPlan::Percent {
percent: primary_percent,
has_error,
Expand Down
72 changes: 68 additions & 4 deletions apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ fn one_provider_stacked_mode_falls_back_to_single_provider_bars() {
fn one_healthy_provider_never_uses_stacked_renderer() {
let settings = Settings {
tray_icon_mode: TrayIconMode::Stacked,
menu_bar_shows_percent: true,
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Percent,
show_as_used: true,
..Settings::default()
};
Expand All @@ -295,7 +295,7 @@ fn one_healthy_provider_never_uses_stacked_renderer() {
#[test]
fn all_errors_produce_error_styled_zero_percent_plan() {
let settings = Settings {
menu_bar_shows_percent: true,
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Percent,
..Settings::default()
};
let mut snapshot = fake_snapshot("codex", "Codex", 30.0);
Expand Down Expand Up @@ -737,7 +737,7 @@ fn tray_pace_color_is_opt_in_and_follows_canonical_stage() {
};
let colored_percent_settings = Settings {
menu_bar_color_pace: true,
menu_bar_shows_percent: true,
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Percent,
..Settings::default()
};
let render = |settings: &Settings, snapshots: &[ProviderUsageSnapshot]| {
Expand All @@ -762,7 +762,7 @@ fn tray_pace_color_is_opt_in_and_follows_canonical_stage() {

let percent_normal = render(
&Settings {
menu_bar_shows_percent: true,
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Percent,
..Settings::default()
},
&unpaced,
Expand Down Expand Up @@ -808,3 +808,67 @@ fn stacked_tray_icon_keeps_usage_colours_when_pace_tint_is_enabled() {
TrayPresentationPlan::resolve(&unpaced_settings, &unpaced, None).render_icon()
);
}

#[test]
fn icon_style_not_the_legacy_percent_flag_picks_the_renderer() {
let snapshots = vec![fake_snapshot("codex", "Codex", 30.0)];
let bars = Settings {
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Bars,
menu_bar_shows_percent: true,
show_as_used: true,
..Settings::default()
};
assert!(matches!(
TrayPresentationPlan::resolve(&bars, &snapshots, None).icon,
TrayIconPlan::Bars { .. }
));
let percent = Settings {
menu_bar_icon_style: codexbar::settings::MenuBarIconStyle::Percent,
menu_bar_shows_percent: false,
show_as_used: true,
..Settings::default()
};
assert_eq!(
TrayPresentationPlan::resolve(&percent, &snapshots, None).icon,
TrayIconPlan::Percent {
percent: 30.0,
has_error: false,
}
);

// The two styles draw different tray pixels for the same usage.
let (bar_pixels, bar_w, bar_h) =
TrayPresentationPlan::resolve(&bars, &snapshots, None).render_icon();
let (percent_pixels, percent_w, percent_h) =
TrayPresentationPlan::resolve(&percent, &snapshots, None).render_icon();
assert_eq!((bar_w, bar_h), (percent_w, percent_h));
let changed = bar_pixels
.as_chunks::<4>()
.0
.iter()
.zip(percent_pixels.as_chunks::<4>().0.iter())
.filter(|(a, b)| a != b)
.count();
assert!(
changed > 0,
"bars and percent icons rendered identical pixels"
);
}

#[test]
fn retired_minimal_display_mode_no_longer_forces_most_used_provider() {
// After the one-shot fold, turning "Show most-used provider" off must win.
let settings = Settings {
tray_icon_mode: TrayIconMode::Single,
menu_bar_display_mode: "minimal".to_string(),
menu_bar_shows_highest_usage: false,
show_as_used: true,
..Settings::default()
};
let snapshots = vec![
fake_snapshot("codex", "Codex", 30.0),
fake_snapshot("claude", "Claude", 72.0),
];
let plan = TrayPresentationPlan::resolve(&settings, &snapshots, None);
assert_eq!(plan.status_rows[0].snapshot.provider_id, "codex");
}
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ function settings(overrides: Partial<SettingsSnapshot> = {}): SettingsSnapshot {
switcherShowsIcons: true,
menuBarShowsHighestUsage: false,
menuBarShowsPercent: false,
menuBarIconStyle: "bars",
mergedOverviewSelectedProviders: [],
quotaWarningMarkersVisible: true,
workdayTickStyle: "subtle",
menuBarColorPace: false,
providersSortedAlphabetically: false,
showAsUsed: true,
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop-tauri/src/components/MenuCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
CostSummaryDisplayStyle,
ProviderChartData,
ProviderUsageSnapshot,
WorkdayTickStyle,
} from "../types/bridge";
import { getProviderChartData } from "../lib/tauri";
import { useLocale } from "../hooks/useLocale";
Expand Down Expand Up @@ -36,7 +37,9 @@ export interface MenuCardDisplayOptions {
compactOverview?: boolean;
costSummaryDisplayStyle?: CostSummaryDisplayStyle;
usageThresholds?: UsageThresholdSettings | null;
quotaWarningMarkersVisible?: boolean;
weeklyProgressWorkDays?: number | null;
workdayTickStyle?: WorkdayTickStyle;
}

interface MenuCardProps {
Expand Down Expand Up @@ -146,7 +149,9 @@ export default function MenuCard({
compactOverview = false,
costSummaryDisplayStyle,
usageThresholds = null,
quotaWarningMarkersVisible = true,
weeklyProgressWorkDays = null,
workdayTickStyle = "subtle",
} = display;
const { t, language } = useLocale();
const [chartData, setChartData] = useState<ProviderChartData | null>(null);
Expand Down Expand Up @@ -221,7 +226,9 @@ export default function MenuCard({
compactOverview,
monthlyLimitBlockNow,
usageThresholds,
quotaWarningMarkersVisible,
weeklyProgressWorkDays,
workdayTickStyle,
};

const cardClassName = [
Expand Down
Loading