Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ pub struct SettingsSnapshot {
claude_daily_routines_usage_visible: bool,
claude_allow_reading_claude_code_credentials: bool,
alibaba_token_plan_region: String,
copilot_seat_credit_entitlement: Option<f64>,
weekly_progress_work_days: Option<u8>,
cost_summary_display_style: &'static str,
open_codex_usage_logs_enabled: bool,
Expand Down Expand Up @@ -685,6 +686,8 @@ impl From<Settings> for SettingsSnapshot {
.cloned()
.collect();

let copilot_seat_credit_entitlement = settings.seat_credit_entitlement(ProviderId::Copilot);

let provider_metrics = settings
.provider_metrics
.into_iter()
Expand Down Expand Up @@ -762,6 +765,7 @@ impl From<Settings> for SettingsSnapshot {
claude_allow_reading_claude_code_credentials: settings
.claude_allow_reading_claude_code_credentials,
alibaba_token_plan_region: settings.alibaba_token_plan_region,
copilot_seat_credit_entitlement,
weekly_progress_work_days: settings.weekly_progress_work_days,
cost_summary_display_style: cost_summary_display_style_label(
settings.cost_summary_display_style,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ pub(crate) fn build_fetch_context(
manual_cookie_header: cookie_header,
api_key,
workspace_id: (!workspace_id.is_empty()).then_some(workspace_id),
seat_credit_entitlement: settings.seat_credit_entitlement(id),
api_region: (!api_region.is_empty()).then_some(api_region),
gateway_url,
auto_prefer_web,
Expand Down
49 changes: 44 additions & 5 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ pub struct SettingsUpdate {
pub promote_tray_icon: Option<bool>,
pub claude_daily_routines_usage_visible: Option<bool>,
pub alibaba_token_plan_region: Option<String>,
/// 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>,
pub cost_summary_display_style: Option<String>,
pub open_codex_usage_logs_enabled: Option<bool>,
Expand All @@ -87,6 +90,7 @@ impl SettingsUpdate {
|| self.claude_daily_routines_usage_visible.is_some()
|| self.claude_allow_reading_claude_code_credentials.is_some()
|| self.alibaba_token_plan_region.is_some()
|| self.copilot_seat_credit_entitlement.is_some()
|| self.weekly_progress_work_days.is_some()
}

Expand Down Expand Up @@ -123,6 +127,7 @@ impl SettingsUpdate {
|| self.menu_bar_display_mode.is_some()
|| self.provider_metrics.is_some()
|| self.codex_spark_usage_visible.is_some()
|| self.copilot_seat_credit_entitlement.is_some()
|| self.enabled_providers.is_some()
|| self.ui_language.is_some()
}
Expand Down Expand Up @@ -285,7 +290,7 @@ impl SettingsUpdate {
Ok(self)
}

fn apply_advanced_settings(self, settings: &mut Settings) -> Self {
fn apply_advanced_settings(self, settings: &mut Settings) -> Result<Self, String> {
if let Some(v) = self.enable_animations {
settings.enable_animations = v;
}
Expand Down Expand Up @@ -364,6 +369,17 @@ impl SettingsUpdate {
region.as_str(),
);
}
if let Some(value) = self.copilot_seat_credit_entitlement {
if let Some(value) = value
&& (!value.is_finite() || value <= 0.0)
{
return Err(
"Copilot seat AI-credit allowance must be a finite number greater than zero"
.to_string(),
);
}
settings.set_seat_credit_entitlement(codexbar::core::ProviderId::Copilot, value);
}
if let Some(v) = self.weekly_progress_work_days {
settings.weekly_progress_work_days = if (2..=6).contains(&v) { Some(v) } else { None };
}
Expand All @@ -374,7 +390,7 @@ impl SettingsUpdate {
{
settings.cost_summary_display_style = v;
}
self
Ok(self)
}

fn float_bar_patch(&self) -> crate::floatbar::SettingsPatch {
Expand Down Expand Up @@ -403,12 +419,19 @@ impl SettingsUpdate {
.apply_general_settings(settings)?
.apply_display_settings(settings)
.apply_notification_settings(settings)?
.apply_advanced_settings(settings);
.apply_advanced_settings(settings)?;
float_bar_patch.apply(settings);
Ok(float_bar_patch)
}
}

fn deserialize_double_option<'de, D>(deserializer: D) -> Result<Option<Option<f64>>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Some(Option::<f64>::deserialize(deserializer)?))
}

fn normalize_custom_sessions_dirs(dirs: Vec<String>) -> Vec<String> {
let mut seen = HashSet::new();
let mut out = Vec::new();
Expand Down Expand Up @@ -567,17 +590,33 @@ mod tests {
claude_allow_reading_claude_code_credentials: Some(true),
..Default::default()
}
.apply_advanced_settings(&mut settings);
.apply_advanced_settings(&mut settings)
.unwrap();
assert!(settings.claude_allow_reading_claude_code_credentials);

SettingsUpdate {
claude_allow_reading_claude_code_credentials: Some(false),
..Default::default()
}
.apply_advanced_settings(&mut settings);
.apply_advanced_settings(&mut settings)
.unwrap();
assert!(!settings.claude_allow_reading_claude_code_credentials);
}

#[test]
fn copilot_seat_credit_update_distinguishes_missing_clear_and_value() {
let missing: SettingsUpdate = serde_json::from_str("{}").unwrap();
assert_eq!(missing.copilot_seat_credit_entitlement, None);

let clear: SettingsUpdate =
serde_json::from_str(r#"{"copilotSeatCreditEntitlement":null}"#).unwrap();
assert_eq!(clear.copilot_seat_credit_entitlement, Some(None));

let value: SettingsUpdate =
serde_json::from_str(r#"{"copilotSeatCreditEntitlement":300}"#).unwrap();
assert_eq!(value.copilot_seat_credit_entitlement, Some(Some(300.0)));
}

#[test]
fn display_settings_that_affect_tray_trigger_presentation_refresh() {
assert!(
Expand Down
97 changes: 93 additions & 4 deletions apps/desktop-tauri/src-tauri/src/usage_metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use codexbar::settings::{MetricPreference, Settings};

use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot};

const COPILOT_SEAT_CREDIT_WINDOW_ID: &str = "copilot-seat-credits";

pub(crate) fn selected_usage_window(
snapshot: &ProviderUsageSnapshot,
settings: &Settings,
Expand All @@ -16,9 +18,16 @@ pub(crate) fn selected_usage_window(
.map(|id| settings.get_provider_metric(id))
.unwrap_or_default();

preferred_window(snapshot, provider, preference)
.or_else(|| automatic_window(snapshot, provider))
.unwrap_or_else(|| snapshot.primary.clone())
if let Some(selected) = preferred_window(snapshot, provider, preference) {
return selected;
}
// A configured Copilot seat allowance is an Automatic-only fallback.
// Preserve an explicit metric choice when its corresponding lane is not
// available instead of silently replacing it with seat-credit progress.
if provider == Some(ProviderId::Copilot) && preference != MetricPreference::Automatic {
return snapshot.primary.clone();
}
automatic_window(snapshot, provider).unwrap_or_else(|| snapshot.primary.clone())
}

/// Select the primary tray metric and, when there are multiple meaningful core
Expand Down Expand Up @@ -112,21 +121,40 @@ fn automatic_window(
}
}
if snapshot.primary.is_informational {
return weekly.cloned();
if let Some(weekly) = weekly {
return Some(weekly.clone());
}
}
}

if snapshot.primary.is_informational
&& provider != Some(ProviderId::Copilot)
&& snapshot.secondary.is_none()
{
return None;
}

let policy = automatic_metric_policy(provider);
let mut windows = Vec::with_capacity(4 + snapshot.extra_rate_windows.len());
windows.push(&snapshot.primary);
windows.extend(snapshot.secondary.iter());
windows.extend(snapshot.model_specific.iter());
windows.extend(snapshot.tertiary.iter());
let has_real_core_window = std::iter::once(&snapshot.primary)
.chain(snapshot.secondary.iter())
.chain(snapshot.model_specific.iter())
.chain(snapshot.tertiary.iter())
.any(|window| !window.is_informational);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if policy.uses_extra_windows {
windows.extend(
snapshot
.extra_rate_windows
.iter()
.filter(|extra| {
provider != Some(ProviderId::Copilot)
|| !has_real_core_window
|| extra.id != COPILOT_SEAT_CREDIT_WINDOW_ID
})
.map(|extra| &extra.window),
);
}
Expand Down Expand Up @@ -350,6 +378,67 @@ mod tests {
);
}

#[test]
fn copilot_automatic_uses_seat_credit_progress_without_metered_quota() {
let mut snapshot = snapshot();
snapshot.provider_id = "copilot".to_string();
snapshot.primary = RateWindowSnapshot {
is_informational: true,
..window(0.0)
};
snapshot.secondary = None;
snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot {
id: COPILOT_SEAT_CREDIT_WINDOW_ID.to_string(),
title: "Credits used".to_string(),
window: window(35.0),
}];

assert_eq!(
selected_usage_window(&snapshot, &Settings::default()).used_percent,
35.0
);
}

#[test]
fn copilot_automatic_keeps_metered_quota_authoritative_over_seat_credits() {
let mut snapshot = snapshot();
snapshot.provider_id = "copilot".to_string();
snapshot.primary = window(20.0);
snapshot.secondary = None;
snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot {
id: COPILOT_SEAT_CREDIT_WINDOW_ID.to_string(),
title: "Credits used".to_string(),
window: window(90.0),
}];

assert_eq!(
selected_usage_window(&snapshot, &Settings::default()).used_percent,
20.0
);
}

#[test]
fn copilot_explicit_session_does_not_fall_back_to_seat_credits() {
let mut snapshot = snapshot();
snapshot.provider_id = "copilot".to_string();
snapshot.primary = RateWindowSnapshot {
is_informational: true,
..window(0.0)
};
snapshot.secondary = None;
snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot {
id: COPILOT_SEAT_CREDIT_WINDOW_ID.to_string(),
title: "Credits used".to_string(),
window: window(35.0),
}];
let mut settings = Settings::default();
settings.set_provider_metric(ProviderId::Copilot, MetricPreference::Session);

let selected = selected_usage_window(&snapshot, &settings);
assert!(selected.is_informational);
assert_eq!(selected.used_percent, 0.0);
}

#[test]
fn cursor_automatic_uses_semantic_monthly_lane_and_keeps_grok_bot_explicit() {
let mut snapshot = snapshot();
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ export const ALL_LOCALE_KEYS = [
"ProviderClaudeAllowReadingClaudeCodeCredentialsHelp",
"ProviderCodexSparkUsage",
"ProviderCodexSparkUsageHelp",
"CopilotSeatCreditTitle",
"CopilotSeatCreditHelper",
"CopilotSeatCreditInvalid",
"CodexAccountsTitle",
"ClaudeAccountsTitle",
"ClaudeAccountsHint",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { UsageSourceSection } from "./sections/UsageSourceSection";
import { shouldShowCookieSource } from "./sections/usageSourcePolicy";
import { RegionSection } from "./sections/RegionSection";
import { CodexUsageOptions } from "./sections/credentials/CodexUsageOptions";
import { CopilotSeatCreditOptions } from "./sections/credentials/CopilotSeatCreditOptions";
import { CodexAccountsSection } from "./sections/credentials/CodexAccountsSection";
import { ClaudeAccountsSection } from "./sections/credentials/ClaudeAccountsSection";
import { GrokAccountsSection } from "./sections/credentials/GrokAccountsSection";
Expand All @@ -52,6 +53,7 @@ interface Props {
cookieDomain?: string | null;
resetTimeRelative: boolean;
providerMetrics: SettingsSnapshot["providerMetrics"];
copilotSeatCreditEntitlement: SettingsSnapshot["copilotSeatCreditEntitlement"];
/** Per-provider accent color overrides (CLI name → hex color). */
providerAccentColors: SettingsSnapshot["providerAccentColors"];
wayfinderGatewayUrl: string;
Expand All @@ -71,6 +73,7 @@ export function ProviderDetailPane({
cookieDomain = null,
resetTimeRelative,
providerMetrics,
copilotSeatCreditEntitlement,
providerAccentColors,
wayfinderGatewayUrl,
settingsDisabled,
Expand Down Expand Up @@ -350,6 +353,14 @@ export function ProviderDetailPane({
/>
<CredentialsDispatcher providerId={detail.id} t={t} />
{detail.id === "codex" && <CodexUsageOptions t={t} />}
{detail.id === "copilot" && (
<CopilotSeatCreditOptions
value={copilotSeatCreditEntitlement}
disabled={settingsDisabled}
t={t}
onChange={onSettingsChange}
/>
)}
<CredentialStorageSection
status={credentialStatus}
busy={busy}
Expand Down
Loading