From bb8c64af42dc7b72f889cc646f5a2e2c49bd0ab5 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 23 Oct 2026 13:33:32 +0700 Subject: [PATCH 1/4] Use the Cursor unified weekly pool as the main meter When a unified X Subscription pool has a 7-day window and a future reset, it replaces the monthly primary and the Auto and API lanes, and the Grok Bot extra is dropped. Its label is Weekly. Trial-only, expired, non-unified and non-weekly pools keep the monthly layout. On-demand cost is unchanged. --- rust/src/providers/cursor/api.rs | 49 +++++++- rust/src/providers/cursor/api/tests.rs | 158 +++++++++++++++++++++++++ rust/src/providers/cursor/mod.rs | 53 +++++++++ 3 files changed, 257 insertions(+), 3 deletions(-) diff --git a/rust/src/providers/cursor/api.rs b/rust/src/providers/cursor/api.rs index 7158ac55b38..cab4746186d 100755 --- a/rust/src/providers/cursor/api.rs +++ b/rust/src/providers/cursor/api.rs @@ -13,6 +13,10 @@ pub(super) const GROK_BOT_TITLE: &str = "Grok Bot"; /// Optional monthly additional spend for unified personal memberships. const UNIFIED_USAGE_PATH: &str = "/api/dashboard/v2/usage-status"; const UNIFIED_USAGE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); +/// A 7-day shared pool; monthly billing cycles never span exactly this length. +const WEEKLY_POOL_WINDOW_MINUTES: u32 = 7 * 24 * 60; +/// Primary label once the shared weekly pool drives the main meter (upstream `"Weekly"`). +pub(super) const SHARED_WEEKLY_POOL_LABEL: &str = "Weekly"; #[derive(Debug)] pub struct CursorUsageResult { @@ -23,6 +27,28 @@ pub struct CursorUsageResult { pub(super) email: Option, pub(super) plan_type: Option, pub(super) grok_bot: Option, + /// Overrides the primary label; `None` keeps the shared spec label. + pub(super) primary_label: Option<&'static str>, +} + +/// Applies the sand usage feed to a built result. The Grok Bot allowance is +/// best-effort. A usable unified weekly pool replaces the monthly primary, +/// Auto and API lanes, and the Grok Bot extra it would duplicate. On-demand +/// cost is untouched. +fn apply_sand_usage( + result: &mut CursorUsageResult, + sand: Option<&SandUsageStatus>, + now: DateTime, +) { + result.grok_bot = sand.and_then(|status| status.to_window(now)); + let Some(pool) = sand.and_then(|status| status.shared_weekly_pool(now)) else { + return; + }; + result.primary = pool; + result.secondary = None; + result.model_specific = None; + result.grok_bot = None; + result.primary_label = Some(SHARED_WEEKLY_POOL_LABEL); } /// Cursor API client @@ -78,7 +104,7 @@ impl CursorApi { .resolve_team_budget(&usage_summary, user_info.as_ref(), cookie_header) .await; let mut result = self.build_result_with_team_budget(usage_summary, user_info, team_budget); - result.grok_bot = sand_result.ok().flatten(); + apply_sand_usage(&mut result, sand_result.ok().flatten().as_ref(), Utc::now()); if unified_personal && let Some(cost) = self.fetch_unified_monthly_cost(cookie_header).await { // Upstream #4593: the monthly billing read replaces the summary's @@ -157,7 +183,7 @@ impl CursorApi { async fn fetch_sand_usage( &self, cookie_header: &str, - ) -> Result, ProviderError> { + ) -> Result, ProviderError> { let url = format!("{}/api/dashboard/get-sand-usage-status", self.base_url); let response = self .client @@ -177,7 +203,7 @@ impl CursorApi { .json() .await .map_err(|e| ProviderError::Parse(e.to_string()))?; - Ok(status.to_window(Utc::now())) + Ok(Some(status)) } async fn fetch_user_info(&self, cookie_header: &str) -> Result { @@ -316,6 +342,7 @@ impl CursorApi { email, plan_type, grok_bot: None, + primary_label: None, } } @@ -514,9 +541,25 @@ struct SandUsageStatus { has_non_zero_included_limit: Option, included_limit_zero: Option, sand_trial_expires_at: Option, + /// The X Subscription shares this weekly pool across Cursor and Grok products. + is_unified: Option, } impl SandUsageStatus { + /// Upstream `CursorStatusSnapshot.toUsageSnapshot` (#4695): a unified pool + /// with a 7-day window and a reset still in the future becomes the main + /// meter. Trial-only, expired, non-unified and other-length pools return + /// `None`, so the monthly layout stays. + fn shared_weekly_pool(&self, now: DateTime) -> Option { + if self.is_unified != Some(true) { + return None; + } + let window = self.to_window(now)?.window; + (window.window_minutes == Some(WEEKLY_POOL_WINDOW_MINUTES) + && window.resets_at.is_some_and(|reset| reset > now)) + .then_some(window) + } + fn to_window(&self, now: DateTime) -> Option { let has_limit = self .included_limit_zero diff --git a/rust/src/providers/cursor/api/tests.rs b/rust/src/providers/cursor/api/tests.rs index 64b0610b496..6d1915fb23a 100644 --- a/rust/src/providers/cursor/api/tests.rs +++ b/rust/src/providers/cursor/api/tests.rs @@ -17,6 +17,7 @@ fn sand_usage_maps_to_weekly_extra_window() { has_non_zero_included_limit: Some(true), included_limit_zero: None, sand_trial_expires_at: None, + is_unified: None, }; let row = status .to_window("2026-08-20T00:00:00Z".parse().unwrap()) @@ -36,6 +37,7 @@ fn sand_usage_hides_accounts_without_included_allowance() { has_non_zero_included_limit: Some(false), included_limit_zero: None, sand_trial_expires_at: None, + is_unified: None, }; assert!( status @@ -53,6 +55,7 @@ fn sand_usage_maps_paid_allowance_from_explicit_zero_flag() { has_non_zero_included_limit: Some(false), included_limit_zero: Some(false), sand_trial_expires_at: None, + is_unified: None, }; let row = status .to_window("2026-08-20T00:00:00Z".parse().unwrap()) @@ -73,6 +76,7 @@ fn sand_usage_maps_an_active_trial_without_a_recurring_reset() { has_non_zero_included_limit: Some(false), included_limit_zero: Some(true), sand_trial_expires_at: Some("2026-08-28T00:00:00Z".into()), + is_unified: None, }; let row = status .to_window("2026-08-20T00:00:00Z".parse().unwrap()) @@ -91,6 +95,7 @@ fn sand_usage_hides_expired_trial() { has_non_zero_included_limit: Some(false), included_limit_zero: Some(true), sand_trial_expires_at: Some("2026-08-19T00:00:00Z".into()), + is_unified: None, }; assert!( status @@ -495,6 +500,159 @@ fn unified_summary_spend_is_kept_when_monthly_usage_is_not_valid() { assert_eq!(cost.resets_at, parse_iso_date("2026-10-25T00:00:00Z")); } +const WEEKLY_NOW: &str = "2026-10-16T06:50:00Z"; +/// Upstream #4545 X Subscription shape: the monthly plan is exhausted while the +/// unified weekly pool is barely used. +const X_SUBSCRIPTION_SUMMARY: &str = r#"{ + "billingCycleStart": "2026-10-01T00:00:00.000Z", + "billingCycleEnd": "2026-11-01T00:00:00.000Z", + "membershipType": "enterprise", + "individualUsage": { + "plan": { + "enabled": true, "used": 4615, "limit": 4615, "remaining": 0, + "breakdown": { "included": 4615, "bonus": 2288, "total": 6903 }, + "autoPercentUsed": 62.860238353196095, "apiPercentUsed": 0, + "totalPercentUsed": 100 + }, + "onDemand": { "enabled": true, "used": 447, "limit": null, "remaining": null } + } +}"#; + +/// A sand feed whose weekly window runs 2026-10-15 to 2026-10-22 (10080 minutes). +fn weekly_sand(extra: &str) -> SandUsageStatus { + let tail = if extra.is_empty() { + String::new() + } else { + format!(",{extra}") + }; + let json = format!( + r#"{{"currentPeriodStart":"2026-10-15T10:27:36.178Z", + "nextResetTimestampUtc":"2026-10-22T10:27:36.178Z", + "usagePercent":10.205850487540628,"hasNonZeroIncludedLimit":true{tail}}}"# + ); + serde_json::from_str(&json).expect("sand fixture should parse") +} + +fn result_with_sand(sand: &SandUsageStatus, now: &str) -> CursorUsageResult { + let mut result = + api().build_result_with_team_budget(parse_summary(X_SUBSCRIPTION_SUMMARY), None, None); + apply_sand_usage( + &mut result, + Some(sand), + parse_iso_date(now).expect("fixture time"), + ); + result +} + +#[test] +fn unified_weekly_pool_with_a_future_reset_takes_the_primary_meter() { + let result = result_with_sand(&weekly_sand(r#""isUnified":true"#), WEEKLY_NOW); + + assert!((result.primary.used_percent - 10.205850487540628).abs() < 0.000001); + assert_eq!(result.primary.window_minutes, Some(10080)); + assert_eq!( + result.primary.resets_at, + parse_iso_date("2026-10-22T10:27:36.178Z") + ); + assert_eq!(result.primary_label, Some("Weekly")); + assert!(result.secondary.is_none(), "monthly Auto lane is hidden"); + assert!( + result.model_specific.is_none(), + "monthly API lane is hidden" + ); + assert!(result.grok_bot.is_none(), "Grok Bot duplicates the pool"); + let cost = result.cost.expect("on-demand spend stays"); + assert!((cost.used - 4.47).abs() < 1e-9); +} + +#[test] +fn unified_weekly_pool_without_a_usable_reset_keeps_the_monthly_layout() { + // Each case stays on the previous layout: the monthly plan percent, the + // monthly Auto lane, and no "Weekly" label. + let cases = [ + ("non-unified", r#""isUnified":false"#, WEEKLY_NOW), + ("missing flag", "", WEEKLY_NOW), + ( + "trial only", + r#""isUnified":true,"includedLimitZero":true,"sandTrialExpiresAt":"2026-10-28T00:00:00Z""#, + WEEKLY_NOW, + ), + // The reset (2026-10-22) is already past at this clock. + ( + "expired reset", + r#""isUnified":true"#, + "2026-10-23T00:00:00Z", + ), + ]; + for (name, extra, now) in cases { + let result = result_with_sand(&weekly_sand(extra), now); + assert!( + (result.primary.used_percent - 100.0).abs() < 1e-9, + "{name}: monthly plan percent" + ); + assert_eq!(result.primary_label, None, "{name}"); + assert!(result.secondary.is_some(), "{name}: monthly Auto lane"); + } +} + +#[test] +fn unified_pool_that_is_not_seven_days_keeps_the_monthly_layout() { + let sand: SandUsageStatus = serde_json::from_str( + r#"{"currentPeriodStart":"2026-10-20T00:00:00Z", + "nextResetTimestampUtc":"2026-10-22T00:00:00Z", + "usagePercent":10,"hasNonZeroIncludedLimit":true,"isUnified":true}"#, + ) + .expect("sand fixture should parse"); + let result = result_with_sand(&sand, WEEKLY_NOW); + assert!((result.primary.used_percent - 100.0).abs() < 1e-9); + assert_eq!(result.primary_label, None); + assert!(result.secondary.is_some()); +} + +#[tokio::test] +async fn unified_weekly_pool_is_read_from_the_sand_feed_over_http() { + let now = Utc::now(); + let reset = now + chrono::Duration::days(5); + let start = reset - chrono::Duration::days(7); + let sand_body = format!( + r#"{{"currentPeriodStart":"{}","nextResetTimestampUtc":"{}", + "usagePercent":10.5,"hasNonZeroIncludedLimit":true,"isUnified":true}}"#, + start.to_rfc3339(), + reset.to_rfc3339() + ); + let mut server = mockito::Server::new_async().await; + let _summary = server + .mock("GET", "/api/usage-summary") + .with_status(200) + .with_body(X_SUBSCRIPTION_SUMMARY) + .create_async() + .await; + let _me = server + .mock("GET", "/api/auth/me") + .with_status(500) + .create_async() + .await; + let sand = server + .mock("POST", "/api/dashboard/get-sand-usage-status") + .with_status(200) + .with_body(sand_body) + .expect(1) + .create_async() + .await; + + let result = CursorApi::with_base_url(&server.url()) + .fetch_usage_with_cookie_header("auth=fixture") + .await + .expect("usage-summary should succeed"); + sand.assert_async().await; + + assert!((result.primary.used_percent - 10.5).abs() < 1e-9); + assert_eq!(result.primary.window_minutes, Some(10080)); + assert_eq!(result.primary_label, Some("Weekly")); + assert!(result.secondary.is_none()); + assert!(result.grok_bot.is_none()); +} + #[tokio::test] async fn unified_personal_usage_reads_monthly_spend_and_failure_keeps_summary_spend() { let now = Utc::now(); diff --git a/rust/src/providers/cursor/mod.rs b/rust/src/providers/cursor/mod.rs index 17ef7a0eda4..e2e17e3945a 100755 --- a/rust/src/providers/cursor/mod.rs +++ b/rust/src/providers/cursor/mod.rs @@ -269,6 +269,7 @@ impl Provider for CursorProvider { email, plan_type, grok_bot, + primary_label, } = result; let mut usage = Self::build_usage_snapshot( primary, @@ -278,6 +279,9 @@ impl Provider for CursorProvider { plan_type, token_report.as_ref(), ); + if let Some(label) = primary_label { + usage = usage.with_primary_label(label); + } if let Some(grok_bot) = grok_bot { usage.extra_rate_windows.push(grok_bot); } @@ -617,4 +621,53 @@ mod tests { usage.assert_async().await; allowed.assert_async().await; } + + #[tokio::test] + async fn unified_weekly_pool_is_the_weekly_primary_without_grok_bot_extra() { + let mut server = mockito::Server::new_async().await; + let reset = chrono::Utc::now() + chrono::Duration::days(5); + let start = reset - chrono::Duration::days(7); + let sand_body = format!( + r#"{{"currentPeriodStart":"{}","nextResetTimestampUtc":"{}", + "usagePercent":10.5,"hasNonZeroIncludedLimit":true,"isUnified":true}}"#, + start.to_rfc3339(), + reset.to_rfc3339() + ); + let _summary = server + .mock("GET", "/api/usage-summary") + .with_status(200) + .with_body(SUMMARY) + .create_async() + .await; + let _me = server + .mock("GET", "/api/auth/me") + .with_status(500) + .create_async() + .await; + let sand = server + .mock("POST", "/api/dashboard/get-sand-usage-status") + .with_status(200) + .with_body(sand_body) + .expect(1) + .create_async() + .await; + let harness = Harness::new(&server); + + let result = harness.fetch(COOKIE_A).await; + + sand.assert_async().await; + let usage = result.usage; + assert!((usage.primary.used_percent - 10.5).abs() < 1e-9); + assert_eq!(usage.primary.window_minutes, Some(10080)); + assert_eq!(usage.primary_label.as_deref(), Some("Weekly")); + assert!(usage.secondary.is_none()); + assert!(usage.model_specific.is_none()); + assert!( + usage + .extra_rate_windows + .iter() + .all(|extra| extra.id != api::GROK_BOT_WINDOW_ID), + "Grok Bot must not repeat the promoted pool" + ); + } } From 86be2561a503dbebe570c3b3202877878d349f6a Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 23 Oct 2026 13:33:36 +0700 Subject: [PATCH 2/4] Add Cursor weekly pool proof scenario Synthetic cursor-weekly fixture (primary Weekly 10.2 percent, five-day reset, on-demand spend) with tray and flyout scenarios that check the label and that no Grok Bot row appears. --- .../scripts/proof-scenarios.json | 21 +++++++++ .../src/proof/fixtures/scenarios.ts | 47 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/apps/desktop-tauri/scripts/proof-scenarios.json b/apps/desktop-tauri/scripts/proof-scenarios.json index ad5dfad7617..dbdec308b05 100644 --- a/apps/desktop-tauri/scripts/proof-scenarios.json +++ b/apps/desktop-tauri/scripts/proof-scenarios.json @@ -195,6 +195,27 @@ { "screenshot": "flyout-default", "clip": "panel" } ] }, + { + "name": "tray-cursor-weekly", + "query": "window=main&fixture=cursor-weekly", + "size": [310, 1032], + "steps": [ + { "click": { "selector": ".provider-grid__item", "label": "Cursor" } }, + { "expectText": ["Cursor", "Weekly", "cursor.user@example.com"] }, + { "expectNoText": "Grok Bot" }, + { "screenshot": "tray-cursor-weekly", "clip": "panel" } + ] + }, + { + "name": "flyout-cursor-weekly", + "query": "window=flyout&fixture=cursor-weekly&theme=dark", + "size": [310, 1032], + "steps": [ + { "expectText": ["Cursor", "Weekly"] }, + { "expectNoText": "Grok Bot" }, + { "screenshot": "flyout-cursor-weekly", "clip": "panel" } + ] + }, { "name": "settings-general-dark", "query": "window=settings&tab=general&theme=dark", diff --git a/apps/desktop-tauri/src/proof/fixtures/scenarios.ts b/apps/desktop-tauri/src/proof/fixtures/scenarios.ts index 18c59befa10..0e512d04838 100644 --- a/apps/desktop-tauri/src/proof/fixtures/scenarios.ts +++ b/apps/desktop-tauri/src/proof/fixtures/scenarios.ts @@ -33,6 +33,7 @@ export const PROOF_SCENARIO_NAMES = [ "overview-all-accounts-private", "errors", "empty", + "cursor-weekly", "openai-live-spend", "claude-plan-hidden", "codex-tool-activity", @@ -183,6 +184,38 @@ function cursorSnapshot(now: Date, updatedMinutesAgo = 3): ProviderUsageSnapshot ); } +/** + * X Subscription Cursor account (upstream #4695): the unified weekly pool is the + * primary "Weekly" meter. The monthly Auto/API lanes and the Grok Bot extra are + * gone; on-demand spend stays in the cost section. + */ +function cursorWeeklySnapshot(now: Date): ProviderUsageSnapshot { + return providerSnapshot( + { + providerId: "cursor", + displayName: "Cursor", + primary: rateWindow(now, 10.2, WEEK_MINUTES, 5 * DAY_MINUTES), + primaryLabel: "Weekly", + updatedAt: isoBefore(now, 3), + }, + { + planName: "Cursor Enterprise", + accountEmail: "cursor.user@example.com", + sourceLabel: "web", + cost: { + used: 4.47, + limit: null, + remaining: null, + currencyCode: "USD", + period: "On-demand (billing cycle)", + resetsAt: isoAfter(now, 12 * DAY_MINUTES), + formattedUsed: "$4.47", + formattedLimit: null, + }, + }, + ); +} + function geminiSnapshot(now: Date): ProviderUsageSnapshot { return providerSnapshot( { @@ -494,6 +527,19 @@ function errorsScenario(now: Date): ProofScenario { }; } +/** Cursor card on the unified weekly pool, with on-demand spend in the cost section. */ +function cursorWeeklyScenario(now: Date): ProofScenario { + return { + name: "cursor-weekly", + settings: baseSettings({ enabledProviders: ["cursor"] }), + catalog: PROOF_PROVIDER_CATALOG, + providers: [cursorWeeklySnapshot(now)], + codexAccounts: emptyCodexAccounts(), + claudeAccounts: [], + localSpend: [], + }; +} + function emptyScenario(): ProofScenario { return { name: "empty", @@ -1048,6 +1094,7 @@ const BUILDERS: Record ProofScenario> = { "overview-all-accounts-private": (now) => overviewAllAccountsScenario(now, true), errors: errorsScenario, empty: emptyScenario, + "cursor-weekly": cursorWeeklyScenario, "openai-live-spend": openAiLiveSpendScenario, "claude-plan-hidden": claudePlanHiddenScenario, "codex-tool-activity": codexToolActivityScenario, From f2f3445471932d5745769d937ef66eb1a25c989a Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 23 Oct 2026 23:45:10 +0700 Subject: [PATCH 3/4] Assert the full Cursor weekly card in the proof scenarios --- apps/desktop-tauri/scripts/proof-scenarios.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop-tauri/scripts/proof-scenarios.json b/apps/desktop-tauri/scripts/proof-scenarios.json index dbdec308b05..ddc7d524977 100644 --- a/apps/desktop-tauri/scripts/proof-scenarios.json +++ b/apps/desktop-tauri/scripts/proof-scenarios.json @@ -201,8 +201,8 @@ "size": [310, 1032], "steps": [ { "click": { "selector": ".provider-grid__item", "label": "Cursor" } }, - { "expectText": ["Cursor", "Weekly", "cursor.user@example.com"] }, - { "expectNoText": "Grok Bot" }, + { "expectText": ["Cursor", "Weekly", "cursor.user@example.com", "10% used", "$4.47"] }, + { "expectNoText": ["Grok Bot", "Auto", "API"] }, { "screenshot": "tray-cursor-weekly", "clip": "panel" } ] }, @@ -211,8 +211,8 @@ "query": "window=flyout&fixture=cursor-weekly&theme=dark", "size": [310, 1032], "steps": [ - { "expectText": ["Cursor", "Weekly"] }, - { "expectNoText": "Grok Bot" }, + { "expectText": ["Cursor", "Weekly", "10% used", "$4.47"] }, + { "expectNoText": ["Grok Bot", "Auto", "API"] }, { "screenshot": "flyout-cursor-weekly", "clip": "panel" } ] }, From 1589b424feeeff609113b6638f7db12b51adac3c Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Sat, 24 Oct 2026 00:15:59 +0700 Subject: [PATCH 4/4] Read the weekly pool for a saved Cursor Grok Bot metric --- .../src-tauri/src/tray_presentation.rs | 4 +--- .../src-tauri/src/tray_presentation_tests.rs | 21 +++++++++++++++++++ .../src-tauri/src/usage_metric.rs | 21 ++++++++++++++++--- .../src-tauri/src/usage_metric/tests.rs | 16 ++++++++++++++ rust/src/core/provider/id.rs | 4 ++++ rust/src/providers/cursor/mod.rs | 6 +++++- 6 files changed, 65 insertions(+), 7 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs index 46fb2de7a29..7487e0275cb 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs @@ -545,9 +545,7 @@ pub(crate) fn named_extra_status( .map(|extra| extra.title.as_str()) .filter(|title| !title.trim().is_empty()) .unwrap_or(declared.label); - let reading = extra - .map(|extra| &extra.window) - .filter(|window| !window.is_informational) + let reading = crate::usage_metric::named_extra_reading(snapshot, declared) .map(|window| { let label = crate::commands::compact_tray_status_label(window, language, show_as_used); if snapshot.quota_estimated { 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 5f9b7f70092..e4da5003b90 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -1592,6 +1592,27 @@ fn named_extra_metric_without_a_reading_shows_a_labeled_dash() { assert!(selected_tray_percents(&missing, &settings).0.is_nan()); } +#[test] +fn saved_grok_bot_shows_the_promoted_weekly_pool_in_the_tray() { + let mut settings = Settings { + show_as_used: true, + ..Settings::default() + }; + let preference = MetricPreference::NamedExtra("cursor-grok-bot".to_string()); + settings.set_provider_metric(ProviderId::Cursor, preference.clone()); + let mut weekly = grok_bot_cursor(None); + weekly.primary.used_percent = 10.0; + weekly.primary.is_informational = false; + weekly.secondary = None; + weekly.primary_label = Some("Weekly".to_string()); + + assert_eq!( + named_extra_status(&weekly, &preference, true, Language::English).as_deref(), + Some("Grok Bot 10% used") + ); + assert_eq!(selected_tray_percents(&weekly, &settings).0, 10.0); +} + #[test] fn named_extra_metric_without_a_reading_renders_an_unknown_icon_in_both_directions() { for show_as_used in [true, false] { diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric.rs b/apps/desktop-tauri/src-tauri/src/usage_metric.rs index 961337fcfe7..0327684730b 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_metric.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_metric.rs @@ -4,7 +4,7 @@ use std::borrow::Cow; use std::cmp::Ordering; use chrono::{DateTime, Utc}; -use codexbar::core::{IconLane, ProviderId, UsagePace}; +use codexbar::core::{IconLane, NamedExtraMetric, ProviderId, UsagePace}; use codexbar::settings::{MetricPreference, Settings}; use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot}; @@ -178,7 +178,7 @@ fn preferred_window( MetricPreference::NamedExtra(id) => { let declared = provider?.named_extra_metric(&id)?; Some( - plan_window(snapshot, declared.id) + named_extra_reading(snapshot, declared) .cloned() .unwrap_or_else(unknown_window), ) @@ -244,7 +244,7 @@ pub(crate) fn selected_named_extra_is_unknown( .get_provider_metric(provider) .named_extra_id() .and_then(|id| provider.named_extra_metric(id)) - .is_some_and(|declared| plan_window(snapshot, declared.id).is_none()) + .is_some_and(|declared| named_extra_reading(snapshot, declared).is_none()) } fn unknown_window() -> RateWindowSnapshot { @@ -258,6 +258,21 @@ fn monthly_plan_window_id(provider: ProviderId) -> Option<&'static str> { codexbar::core::instantiate_provider(provider).monthly_plan_window_id() } +/// The known reading for a declared named extra allowance: its own extra +/// window, or the primary window once the provider has promoted the +/// allowance into it (see [`NamedExtraMetric::promoted_primary_label`]). +pub(crate) fn named_extra_reading( + snapshot: &ProviderUsageSnapshot, + declared: NamedExtraMetric, +) -> Option<&RateWindowSnapshot> { + let promoted = declared.promoted_primary_label.is_some() + && snapshot.primary_label.as_deref() == declared.promoted_primary_label; + if promoted { + return non_informational(Some(&snapshot.primary)); + } + plan_window(snapshot, declared.id) +} + fn plan_window<'a>( snapshot: &'a ProviderUsageSnapshot, window_id: &str, diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs b/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs index f4aa8de0852..4bf413d9542 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs @@ -192,6 +192,22 @@ fn named_extra_without_a_reading_stays_unknown_instead_of_borrowing_a_lane() { assert_eq!(selected.used_percent, 0.0); } +#[test] +fn saved_grok_bot_reads_the_promoted_weekly_pool() { + let mut weekly = grok_bot_snapshot(None); + weekly.primary = window(10.0); + weekly.secondary = None; + weekly.primary_label = Some("Weekly".to_string()); + let selected = selected_usage_window(&weekly, &grok_bot_settings()); + assert!(!selected.is_informational); + assert_eq!(selected.used_percent, 10.0); + + // Without the weekly layout, a missing Grok Bot reading stays unknown. + let mut monthly = weekly.clone(); + monthly.primary_label = Some("Total".to_string()); + assert!(selected_usage_window(&monthly, &grok_bot_settings()).is_informational); +} + #[test] fn undeclared_named_extra_falls_back_to_automatic() { let mut settings = Settings::default(); diff --git a/rust/src/core/provider/id.rs b/rust/src/core/provider/id.rs index aa48a3144c7..90f9bbce106 100755 --- a/rust/src/core/provider/id.rs +++ b/rust/src/core/provider/id.rs @@ -12,6 +12,10 @@ pub struct NamedExtraMetric { pub id: &'static str, /// Label used when the snapshot carries no title for the window. pub label: &'static str, + /// Snapshot primary label under which the provider has folded this + /// allowance into the primary window. A saved choice then reads the + /// primary instead of an absent extra window. + pub promoted_primary_label: Option<&'static str>, } /// Unique identifier for a provider diff --git a/rust/src/providers/cursor/mod.rs b/rust/src/providers/cursor/mod.rs index e2e17e3945a..463c372f2e8 100755 --- a/rust/src/providers/cursor/mod.rs +++ b/rust/src/providers/cursor/mod.rs @@ -37,11 +37,14 @@ pub(crate) const PACE_CAPABILITY: crate::core::ProviderPaceCapability = ); /// Grok Bot is Cursor's named extra allowance (upstream 0.73.0 #4269), -/// published through [`ProviderId::named_extra_metrics`]. +/// published through [`ProviderId::named_extra_metrics`]. A unified weekly +/// pool replaces the Grok Bot extra, and a saved Grok Bot choice reads that +/// pool (upstream `CursorProviderDescriptor.menuBarExtras`). pub const NAMED_EXTRA_METRICS: &[crate::core::NamedExtraMetric] = &[crate::core::NamedExtraMetric { id: api::GROK_BOT_WINDOW_ID, label: api::GROK_BOT_TITLE, + promoted_primary_label: Some(api::SHARED_WEEKLY_POOL_LABEL), }]; /// Cursor provider for fetching AI usage limits @@ -326,6 +329,7 @@ mod tests { assert_eq!(metrics.len(), 1); assert_eq!(metrics[0].id, "cursor-grok-bot"); assert_eq!(metrics[0].label, "Grok Bot"); + assert_eq!(metrics[0].promoted_primary_label, Some("Weekly")); assert_eq!( ProviderId::Cursor.named_extra_metric("cursor-grok-bot"), Some(metrics[0])