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
29 changes: 26 additions & 3 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -832,8 +832,9 @@ pub struct SettingsSnapshot {
notification_sound_paths: codexbar::settings::NotificationSoundPaths,
high_usage_threshold: f64,
critical_usage_threshold: f64,
provider_usage_thresholds:
std::collections::HashMap<String, codexbar::settings::UsageThresholdOverride>,
/// Per-provider quota warnings keyed by CLI name; Global providers are absent.
provider_quota_warnings:
std::collections::HashMap<String, codexbar::settings::ProviderQuotaWarnings>,
predictive_pace_warning_enabled: bool,
credential_expiry_notifications_enabled: bool,
session_quota_notifications_enabled: bool,
Expand Down Expand Up @@ -882,6 +883,11 @@ pub struct SettingsSnapshot {
claude_swap_enabled: bool,
claude_swap_executable_path: String,
codex_spark_usage_visible: bool,
codex_openai_web_extras: bool,
codex_historical_tracking: bool,
codex_external_oauth_sources_allowed: bool,
codex_local_session_cost_ledger_enabled: bool,
codex_openai_web_battery_saver_enabled: bool,
disable_keychain_access: bool,
wayfinder_gateway_url: String,
provider_metrics: std::collections::HashMap<String, &'static str>,
Expand Down Expand Up @@ -943,6 +949,18 @@ impl From<Settings> for SettingsSnapshot {
let claude_swap_enabled = settings.claude_swap_enabled();
let claude_swap_executable_path = settings.claude_swap_executable_path().to_string();
let codex_spark_usage_visible = settings.codex_spark_usage_visible();
let codex_openai_web_extras = settings.codex_openai_web_extras();
let codex_historical_tracking = settings.codex_historical_tracking();
let codex_local_session_cost_ledger_enabled =
settings.local_session_cost_ledger_enabled(ProviderId::Codex);
let codex_openai_web_battery_saver_enabled =
settings.openai_web_battery_saver_enabled(ProviderId::Codex);
let provider_quota_warnings = settings
.provider_configs
.iter()
.filter(|(_, config)| !config.quota_warnings.is_empty())
.map(|(id, config)| (id.cli_name().to_string(), config.quota_warnings))
.collect();
let wayfinder_gateway_url = settings.gateway_url(ProviderId::Wayfinder).to_string();

let provider_order = settings.provider_display_order_names();
Expand Down Expand Up @@ -978,7 +996,7 @@ impl From<Settings> for SettingsSnapshot {
notification_sound_paths: settings.notification_sound_paths,
high_usage_threshold: settings.high_usage_threshold,
critical_usage_threshold: settings.critical_usage_threshold,
provider_usage_thresholds: settings.provider_usage_thresholds,
provider_quota_warnings,
predictive_pace_warning_enabled: settings.predictive_pace_warning_enabled,
credential_expiry_notifications_enabled: settings
.credential_expiry_notifications_enabled,
Expand Down Expand Up @@ -1030,6 +1048,11 @@ impl From<Settings> for SettingsSnapshot {
claude_swap_enabled,
claude_swap_executable_path,
codex_spark_usage_visible,
codex_openai_web_extras,
codex_historical_tracking,
codex_external_oauth_sources_allowed: settings.codex_external_oauth_sources_allowed,
codex_local_session_cost_ledger_enabled,
codex_openai_web_battery_saver_enabled,
disable_keychain_access: settings.disable_keychain_access,
wayfinder_gateway_url,
provider_metrics,
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,12 @@ pub fn get_provider_detail(
detail.pace = snapshot.pace.clone();
detail.open_ai_api_usage = snapshot.open_ai_api_usage.clone();
}
detail.last_error = snapshot.error.clone();
// Shown verbatim behind "Show details"; redact again so every cache
// path (refresh, seed, restore) reaches the pane scrubbed.
detail.last_error = snapshot
.error
.as_deref()
.map(codexbar::core::SecretRedactor::redact);
detail.error_state = Some(snapshot.error_state);
detail.has_snapshot = true;
}
Expand Down
109 changes: 103 additions & 6 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ pub struct SettingsUpdate {
pub notification_sound_paths: Option<codexbar::settings::NotificationSoundPaths>,
pub high_usage_threshold: Option<f64>,
pub critical_usage_threshold: Option<f64>,
pub provider_usage_thresholds:
Option<std::collections::HashMap<String, codexbar::settings::UsageThresholdOverride>>,
/// Map of provider CLI name → that provider's full quota warning settings
/// (replaces the stored value; an empty object means Global).
pub provider_quota_warnings:
Option<std::collections::HashMap<String, codexbar::settings::ProviderQuotaWarnings>>,
pub predictive_pace_warning_enabled: Option<bool>,
pub credential_expiry_notifications_enabled: Option<bool>,
pub session_quota_notifications_enabled: Option<bool>,
Expand Down Expand Up @@ -74,6 +76,11 @@ pub struct SettingsUpdate {
pub claude_swap_enabled: Option<bool>,
pub claude_swap_executable_path: Option<String>,
pub codex_spark_usage_visible: Option<bool>,
pub codex_openai_web_extras: Option<bool>,
pub codex_historical_tracking: Option<bool>,
pub codex_external_oauth_sources_allowed: Option<bool>,
pub codex_local_session_cost_ledger_enabled: Option<bool>,
pub codex_openai_web_battery_saver_enabled: Option<bool>,
pub disable_keychain_access: Option<bool>,
/// Map of provider CLI name → metric preference label.
pub provider_metrics: Option<std::collections::HashMap<String, String>>,
Expand Down Expand Up @@ -115,6 +122,8 @@ impl SettingsUpdate {
fn refreshes_provider_data(&self) -> bool {
self.enabled_providers.is_some()
|| self.claude_allow_reading_claude_code_credentials.is_some()
|| self.codex_openai_web_extras.is_some()
|| self.codex_external_oauth_sources_allowed.is_some()
|| self.alibaba_token_plan_region.is_some()
|| self.copilot_seat_credit_entitlement.is_some()
|| self.weekly_progress_work_days.is_some()
Expand All @@ -131,7 +140,7 @@ impl SettingsUpdate {
|| self.cost_usage_enabled.is_some()
|| self.high_usage_threshold.is_some()
|| self.critical_usage_threshold.is_some()
|| self.provider_usage_thresholds.is_some()
|| self.provider_quota_warnings.is_some()
|| self.show_as_used.is_some()
|| self.reset_time_relative.is_some()
|| self.show_reset_when_exhausted.is_some()
Expand Down Expand Up @@ -160,6 +169,7 @@ impl SettingsUpdate {
|| self.overview_layout.is_some()
|| self.provider_metrics.is_some()
|| self.provider_hidden_usage_item_ids.is_some()
|| self.provider_quota_warnings.is_some()
|| self.preferred_currency_code.is_some()
|| self.codex_spark_usage_visible.is_some()
|| self.copilot_seat_credit_entitlement.is_some()
Expand Down Expand Up @@ -389,9 +399,8 @@ impl SettingsUpdate {
if let Some(v) = self.critical_usage_threshold {
settings.critical_usage_threshold = v.clamp(0.0, 100.0);
}
if let Some(values) = self.provider_usage_thresholds.clone() {
settings.provider_usage_thresholds =
codexbar::settings::normalize_usage_threshold_overrides(values);
if let Some(values) = self.provider_quota_warnings.clone() {
apply_provider_quota_warnings(settings, values);
}
if let Some(v) = self.predictive_pace_warning_enabled {
settings.predictive_pace_warning_enabled = v;
Expand Down Expand Up @@ -474,6 +483,21 @@ impl SettingsUpdate {
if let Some(v) = self.codex_spark_usage_visible {
settings.set_codex_spark_usage_visible(v);
}
if let Some(v) = self.codex_openai_web_extras {
settings.set_codex_openai_web_extras(v);
}
if let Some(v) = self.codex_historical_tracking {
settings.set_codex_historical_tracking(v);
}
if let Some(v) = self.codex_external_oauth_sources_allowed {
settings.codex_external_oauth_sources_allowed = v;
}
if let Some(v) = self.codex_local_session_cost_ledger_enabled {
settings.set_local_session_cost_ledger_enabled(codexbar::core::ProviderId::Codex, v);
}
if let Some(v) = self.codex_openai_web_battery_saver_enabled {
settings.set_openai_web_battery_saver_enabled(codexbar::core::ProviderId::Codex, v);
}
if let Some(v) = self.disable_keychain_access {
settings.disable_keychain_access = v;
if v {
Expand Down Expand Up @@ -593,6 +617,17 @@ fn apply_provider_metrics(
}
}

fn apply_provider_quota_warnings(
settings: &mut Settings,
values: std::collections::HashMap<String, codexbar::settings::ProviderQuotaWarnings>,
) {
for (provider, warnings) in values {
if let Ok(provider_id) = super::parse_provider_arg(&provider) {
settings.set_quota_warnings(provider_id, warnings);
}
}
}

fn apply_provider_hidden_usage_item_ids(
settings: &mut Settings,
values: std::collections::HashMap<String, Vec<String>>,
Expand Down Expand Up @@ -780,6 +815,68 @@ mod tests {
);
}

#[test]
fn codex_options_and_quota_warnings_round_trip_through_the_bridge() {
let patch: SettingsUpdate = serde_json::from_value(serde_json::json!({
"codexOpenaiWebExtras": false,
"codexHistoricalTracking": true,
"codexExternalOauthSourcesAllowed": true,
"codexLocalSessionCostLedgerEnabled": true,
"codexOpenaiWebBatterySaverEnabled": true,
"providerQuotaWarnings": {
"codex": {
"session": { "high": 60.0, "critical": 85.0 },
"weekly": { "enabled": false }
},
"not-a-provider": { "session": { "high": 10.0 } }
}
}))
.expect("bridge patch deserializes");
assert!(patch.notifies_float_bar());
assert!(patch.refreshes_tray_presentation());
assert!(patch.refreshes_provider_data());

let mut settings = Settings::default();
assert!(
settings.codex_openai_web_extras(),
"Windows default stays on"
);
patch.apply_to(&mut settings).expect("apply");

let snapshot = serde_json::to_value(crate::commands::bridge::SettingsSnapshot::from(
settings.clone(),
))
.expect("snapshot serializes");
assert_eq!(snapshot["codexOpenaiWebExtras"], false);
assert_eq!(snapshot["codexHistoricalTracking"], true);
assert_eq!(snapshot["codexExternalOauthSourcesAllowed"], true);
assert_eq!(snapshot["codexLocalSessionCostLedgerEnabled"], true);
assert_eq!(snapshot["codexOpenaiWebBatterySaverEnabled"], true);
assert_eq!(
snapshot["providerQuotaWarnings"],
serde_json::json!({
"codex": {
"session": { "high": 60.0, "critical": 85.0 },
"weekly": { "enabled": false }
}
})
);
assert!(snapshot.get("providerUsageThresholds").is_none());
assert!(!settings.quota_warning_enabled(codexbar::core::ProviderId::Codex, "weekly"));

// An empty object returns the provider to Global.
let reset: SettingsUpdate = serde_json::from_value(serde_json::json!({
"providerQuotaWarnings": { "codex": {} }
}))
.expect("reset patch");
reset.apply_to(&mut settings).expect("apply reset");
assert!(
settings
.quota_warnings(codexbar::core::ProviderId::Codex)
.is_empty()
);
}

#[test]
fn apply_advanced_settings_sets_claude_code_credentials_consent() {
let mut settings = Settings::default();
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ fn main() {
let force_start_visible = std::env::var_os("CODEXBAR_START_VISIBLE").is_some();
let mut settings = codexbar::settings::Settings::load();
settings.apply_display_mode_fold_migration();
settings.apply_quota_warning_migration();
let launch = launch_behavior(
force_start_visible,
settings.start_minimized,
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop-tauri/src/components/FormControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,18 @@ export function SettingsGroup({
title,
footer,
children,
className,
}: {
title?: string;
footer?: React.ReactNode;
children: React.ReactNode;
/** Extra classes on the `section` (e.g. a dimmed or provider-detail variant). */
className?: string;
}) {
const id = useId();
return (
<section
className="settings-group"
className={className ? `settings-group ${className}` : "settings-group"}
aria-labelledby={title ? `${id}-title` : undefined}
>
{title && (
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop-tauri/src/components/MenuCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1200,14 +1200,15 @@ describe("MenuCard", () => {
it.each([
["global thresholds on a % left bar", false, {}, ["5%", "20%"]],
["global thresholds on a % used bar", true, {}, ["80%", "95%"]],
["a provider session override", false, { "claude:session": { high: 60 } }, ["5%", "40%"]],
["a Custom session quota warning", false, { claude: { session: { high: 60 } } }, ["5%", "40%"]],
["a session quota warning set to Off", false, { claude: { session: { enabled: false } } }, []],
])("draws quota warning markers from %s", async (_case, showAsUsed, overrides, expected) => {
const { container } = renderCard(provider(null, 30), {
showAsUsed,
usageThresholds: {
highUsageThreshold: 80,
criticalUsageThreshold: 95,
providerUsageThresholds: overrides,
providerQuotaWarnings: overrides,
},
});

Expand All @@ -1218,7 +1219,9 @@ describe("MenuCard", () => {
),
).toEqual(expected);
expect(container.querySelector(".menu-metric__progress")?.getAttribute("aria-valuetext")).toBe(
`${showAsUsed ? 30 : 70}%. Quota warnings: ${expected.join(", ")}`,
expected.length
? `${showAsUsed ? 30 : 70}%. Quota warnings: ${expected.join(", ")}`
: `${showAsUsed ? 30 : 70}%`,
);
});

Expand Down
58 changes: 58 additions & 0 deletions apps/desktop-tauri/src/floatbar/FloatBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,64 @@ describe("FloatBar", () => {
});
});

it("follows a provider's Custom quota warnings and drops the tone when they are Off", async () => {
// 50% used is below the global 70% high threshold, but above a Custom 40%.
tauriMocks.getCachedProviders.mockResolvedValue([snapshot("claude", "Claude", 50)]);
const customWarnings: Partial<SettingsSnapshot> = {
providerQuotaWarnings: { claude: { session: { enabled: true, high: 40, critical: 90 } } },
};
tauriMocks.getSettingsSnapshot.mockResolvedValue(settings(customWarnings));
const custom = renderFloatBar(bootstrap(customWarnings));
await waitFor(() => {
expect(custom.container.querySelector(".floatbar__pill--warn")).not.toBeNull();
});
custom.unmount();

tauriMocks.getCachedProviders.mockResolvedValue([snapshot("claude", "Claude", 95)]);
const offWarnings: Partial<SettingsSnapshot> = {
providerQuotaWarnings: { claude: { session: { enabled: false } } },
};
tauriMocks.getSettingsSnapshot.mockResolvedValue(settings(offWarnings));
const off = renderFloatBar(bootstrap(offWarnings));
await waitFor(() => {
expect(off.container.querySelector(".floatbar__pill--ok")).not.toBeNull();
});
});

it("picks the quota warning window by card lane, not by window length", async () => {
// A monthly primary lane is the Session window for cards and toasts, so
// Session Off drops its tone even though the window is longer than a week.
const monthly = { ...rateWindow(95), windowMinutes: 30 * 24 * 60 };
tauriMocks.getCachedProviders.mockResolvedValue([
{ ...snapshot("claude", "Claude", 95), primary: monthly, selectedMetric: monthly },
]);
const sessionOff: Partial<SettingsSnapshot> = {
providerQuotaWarnings: { claude: { session: { enabled: false } } },
};
tauriMocks.getSettingsSnapshot.mockResolvedValue(settings(sessionOff));
const off = renderFloatBar(bootstrap(sessionOff));
await waitFor(() => {
expect(off.container.querySelector(".floatbar__pill--ok")).not.toBeNull();
});
off.unmount();

// A selected secondary lane with no window length is the Weekly window.
tauriMocks.getCachedProviders.mockResolvedValue([
snapshot("claude", "Claude", 10, {
informational: true,
secondary: { used: 50 },
}),
]);
const weeklyCustom: Partial<SettingsSnapshot> = {
providerQuotaWarnings: { claude: { weekly: { high: 40, critical: 90 } } },
};
tauriMocks.getSettingsSnapshot.mockResolvedValue(settings(weeklyCustom));
const weekly = renderFloatBar(bootstrap(weeklyCustom));
await waitFor(() => {
expect(weekly.container.querySelector(".floatbar__pill--warn")).not.toBeNull();
});
});

it("applies critical tone when the provider is exhausted", async () => {
tauriMocks.getCachedProviders.mockResolvedValue([
snapshot("claude", "Claude", 100, { exhausted: true }),
Expand Down
Loading