From 4f794771b0f8146751030c4da502e659185c976d Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Tue, 4 Aug 2026 21:11:34 -0400 Subject: [PATCH 1/2] Show Cursor on-demand spend in dollars (#191) On-demand is the only Cursor lane that bills real money, but it could go missing entirely and, when it did appear, had nowhere to put the dollar figure the user actually cares about. - Add an optional `WindowAmount` to `NamedRateWindow`, plumbed through the Tauri bridge to `MetricRow`. A metered lane can now carry currency alongside its bar; `CostSnapshot` is one slot per provider and cannot describe individual windows. - Read `individual.on_demand` in the `overall` branch too. It was only consulted when a `plan` object was present, so accounts reporting `overall` lost the overdraft meter completely. - Surface uncapped on-demand spend. `usage_percent` needs a denominator, so on-demand enabled with real spend but no cap produced no meter and the spend was dropped. It now reports as an explicit non-metering window rather than inventing a percentage. - Label on-demand cost as "On-demand" instead of folding it into a generic "Monthly". Verified against a live Cursor account: the on-demand lane now reads "$0.00 of $1.00" against a $1 cap, which is the only real-money figure on that account. Plan usage is metered at internal rates and billed nothing, so it is deliberately left out of the cost slot. --- .../src-tauri/src/auto_refresh.rs | 1 + .../src-tauri/src/capacity_events.rs | 58 +++++ .../src-tauri/src/commands/bridge.rs | 25 ++ .../src-tauri/src/commands/tests.rs | 2 + .../src-tauri/src/enforcement.rs | 2 + .../src-tauri/src/taskbar_widget.rs | 5 + .../src-tauri/src/tray_bridge.rs | 1 + .../src/components/MenuCard.test.tsx | 47 ++++ .../desktop-tauri/src/components/MenuCard.tsx | 14 ++ apps/desktop-tauri/src/i18n/keys.ts | 1 + apps/desktop-tauri/src/styles.css | 11 + apps/desktop-tauri/src/types/bridge.ts | 14 ++ rust/src/core/usage_snapshot.rs | 51 +++++ rust/src/locale.rs | 1 + rust/src/locale/en-US.ftl | 1 + rust/src/locale/zh-CN.ftl | 1 + rust/src/providers/cursor/api.rs | 215 ++++++++++++++++-- 17 files changed, 425 insertions(+), 25 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/auto_refresh.rs b/apps/desktop-tauri/src-tauri/src/auto_refresh.rs index 942122c0..c00c171f 100644 --- a/apps/desktop-tauri/src-tauri/src/auto_refresh.rs +++ b/apps/desktop-tauri/src-tauri/src/auto_refresh.rs @@ -279,6 +279,7 @@ mod tests { id: "claude-routines".to_string(), title: "Daily Routines".to_string(), window: window_resetting_at(Some("2026-07-21T03:00:00Z")), + amount: None, }); let mut times = snapshot_reset_times(&snapshot); diff --git a/apps/desktop-tauri/src-tauri/src/capacity_events.rs b/apps/desktop-tauri/src-tauri/src/capacity_events.rs index dcfc8775..1b16b404 100644 --- a/apps/desktop-tauri/src-tauri/src/capacity_events.rs +++ b/apps/desktop-tauri/src-tauri/src/capacity_events.rs @@ -1000,10 +1000,68 @@ mod tests { id: id.into(), title: title.into(), window: window(used, reset), + amount: None, }); snapshot } + #[test] + fn third_and_model_windows_are_observed_without_displacing_core_slots() { + // Neither slot reached this map before, so their resets and exhaustions + // were invisible to events. Both share a cadence with a core window in + // the wild (Claude's model pool is 7 days like its weekly), and the map + // is keyed by id — a shared key silently drops one of the two. + let now = Utc::now(); + let reset = now + Duration::hours(3); + let mut snapshot = snapshot(now, 10.0, reset); + snapshot.secondary = Some(RateWindowSnapshot { + window_minutes: Some(10_080), + ..window(20.0, reset) + }); + snapshot.secondary_label = Some("Weekly".into()); + snapshot.model_specific = Some(RateWindowSnapshot { + window_minutes: Some(10_080), + ..window(96.0, reset) + }); + snapshot.tertiary = Some(RateWindowSnapshot { + window_minutes: Some(43_200), + ..window(57.0, reset) + }); + snapshot.tertiary_label = Some("Monthly".into()); + + let (windows, extra_ids) = observed_windows(&snapshot); + + assert_eq!(windows["weekly"].used_percent, 20.0); + assert_eq!(windows["model"].used_percent, 96.0); + assert_eq!(windows["monthly"].used_percent, 57.0); + assert_eq!(windows["monthly"].label, "Monthly"); + // Not "extras": an extra id appearing for the first time announces a + // granted allowance, and these windows have been there all along. + assert!(!extra_ids.contains("model")); + assert!(!extra_ids.contains("monthly")); + } + + #[test] + fn a_third_window_sharing_a_cadence_falls_back_to_its_own_id() { + let now = Utc::now(); + let reset = now + Duration::hours(3); + let mut snapshot = snapshot(now, 10.0, reset); + snapshot.secondary = Some(RateWindowSnapshot { + window_minutes: Some(10_080), + ..window(20.0, reset) + }); + snapshot.secondary_label = Some("Weekly".into()); + snapshot.tertiary = Some(RateWindowSnapshot { + window_minutes: Some(10_080), + ..window(80.0, reset) + }); + + let (windows, _) = observed_windows(&snapshot); + + assert_eq!(windows["weekly"].used_percent, 20.0); + assert_eq!(windows["tertiary"].used_percent, 80.0); + } + fn with_inactive( mut snapshot: ProviderUsageSnapshot, id: &str, diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 089215f1..b0fc6719 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -67,6 +67,30 @@ pub struct NamedRateWindowSnapshot { pub id: String, pub title: String, pub window: RateWindowSnapshot, + /// Money behind this lane, for providers that denominate it that way. + pub amount: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowAmountBridge { + pub used: f64, + pub limit: Option, + pub currency_code: String, + pub formatted_used: String, + pub formatted_limit: Option, +} + +impl WindowAmountBridge { + fn from_amount(amount: &codexbar::core::WindowAmount) -> Self { + Self { + used: amount.used, + limit: amount.limit, + currency_code: amount.currency_code.clone(), + formatted_used: amount.format_used(), + formatted_limit: amount.format_limit(), + } + } } #[derive(Debug, Clone, Serialize)] @@ -283,6 +307,7 @@ impl ProviderUsageSnapshot { id: extra.id.clone(), title: extra.title.clone(), window: RateWindowSnapshot::from_rate_window(&extra.window), + amount: extra.amount.as_ref().map(WindowAmountBridge::from_amount), }) .collect(), inactive_rate_windows: usage diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index 19dac898..143b51c6 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -993,11 +993,13 @@ fn hiding_codex_spark_rows_preserves_other_extra_usage() { id: "codex-spark".to_string(), title: "Codex Spark 5-hour".to_string(), window: snapshot.primary.clone(), + amount: None, }, NamedRateWindowSnapshot { id: "credits".to_string(), title: "Credits".to_string(), window: snapshot.primary.clone(), + amount: None, }, ]; diff --git a/apps/desktop-tauri/src-tauri/src/enforcement.rs b/apps/desktop-tauri/src-tauri/src/enforcement.rs index 6acd1b17..34b62b79 100644 --- a/apps/desktop-tauri/src-tauri/src/enforcement.rs +++ b/apps/desktop-tauri/src-tauri/src/enforcement.rs @@ -380,6 +380,7 @@ mod tests { id: "codex-spark-weekly".into(), title: "Codex Spark Weekly".into(), window: window(Some(10_080)), + amount: None, }); tracker.annotate(&mut with_spark); @@ -399,6 +400,7 @@ mod tests { id: "codex-spark-weekly".into(), title: "Codex Spark Weekly".into(), window: window(Some(10_080)), + amount: None, }); tracker.annotate(&mut with_spark); diff --git a/apps/desktop-tauri/src-tauri/src/taskbar_widget.rs b/apps/desktop-tauri/src-tauri/src/taskbar_widget.rs index c34aab55..6fd43381 100644 --- a/apps/desktop-tauri/src-tauri/src/taskbar_widget.rs +++ b/apps/desktop-tauri/src-tauri/src/taskbar_widget.rs @@ -2228,6 +2228,7 @@ mod tests { id: "cursor-api".into(), title: "API".into(), window: rate_window(100.0, Some(10_080)), + amount: None, }); let readout = constraining_readout(&snapshot); @@ -2247,6 +2248,7 @@ mod tests { id: "cursor-api".into(), title: "API".into(), window: rate_window(40.0, Some(10_080)), + amount: None, }); let readout = constraining_readout(&snapshot); @@ -2265,6 +2267,7 @@ mod tests { id: "cursor-api".into(), title: " ".into(), window: rate_window(70.0, Some(10_080)), + amount: None, }); let readout = constraining_readout(&snapshot); @@ -2291,6 +2294,7 @@ mod tests { id: "cursor-api".into(), title: "API".into(), window: rate_window(30.0, Some(10_080)), + amount: None, }); let readout = constraining_readout(&snapshot); @@ -2315,6 +2319,7 @@ mod tests { id: "cursor-api".into(), title: "API".into(), window: soon, + amount: None, }); let readout = constraining_readout(&snapshot); diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index ac14e44f..dbda41ea 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -1367,6 +1367,7 @@ mod tests { reserve_will_last_to_reset: false, reserve_eta_seconds: None, }, + amount: None, } } diff --git a/apps/desktop-tauri/src/components/MenuCard.test.tsx b/apps/desktop-tauri/src/components/MenuCard.test.tsx index 9cd1534f..cc819837 100644 --- a/apps/desktop-tauri/src/components/MenuCard.test.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.test.tsx @@ -112,6 +112,7 @@ describe("MenuCard", () => { PanelOneHour: "1h", PanelFiveHours: "5h", PanelOnPaceBudget: "On-pace budget", + PanelAmountOf: "of", PanelReserveSuffix: "in reserve", PanelThirtyDayCost: "30d cost", PanelThirtyDayTokens: "30d tokens", @@ -234,6 +235,52 @@ describe("MenuCard", () => { expect(screen.getByText("58% left")).toBeInTheDocument(); }); + it("shows the money behind a lane that is denominated in currency", async () => { + const snapshot = provider(null, 20); + snapshot.providerId = "cursor"; + snapshot.extraRateWindows = [ + { + id: "cursor-on-demand", + title: "On-demand", + window: rateWindow(35), + amount: { + used: 3.5, + limit: 10, + currencyCode: "USD", + formattedUsed: "$3.50", + formattedLimit: "$10.00", + }, + }, + ]; + + renderCard(snapshot); + + expect(await screen.findByText("On-demand")).toBeInTheDocument(); + expect(screen.getByText("$3.50 of $10.00")).toBeInTheDocument(); + }); + + it("shows a lane's spend alone when it has no limit", async () => { + const snapshot = provider(null, 20); + snapshot.extraRateWindows = [ + { + id: "cursor-on-demand", + title: "On-demand", + window: rateWindow(0), + amount: { + used: 12.34, + limit: null, + currencyCode: "USD", + formattedUsed: "$12.34", + formattedLimit: null, + }, + }, + ]; + + renderCard(snapshot); + + expect(await screen.findByText("$12.34")).toBeInTheDocument(); + }); + it("renders inactive windows as text without inventing a percentage", async () => { const snapshot = provider(null, 40); snapshot.secondary = rateWindow(55); diff --git a/apps/desktop-tauri/src/components/MenuCard.tsx b/apps/desktop-tauri/src/components/MenuCard.tsx index 6b58cd26..d0e2a7d0 100644 --- a/apps/desktop-tauri/src/components/MenuCard.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.tsx @@ -5,6 +5,7 @@ import type { ProviderLocalUsageSummary, ProviderUsageSnapshot, RateWindowSnapshot, + WindowAmountBridge, } from "../types/bridge"; import { getProviderChartData } from "../lib/tauri"; import { useLocale } from "../hooks/useLocale"; @@ -246,6 +247,8 @@ interface MetricEntry { id: string; label: string; snap: RateWindowSnapshot; + /** Money behind this lane, for providers that meter in currency. */ + amount?: WindowAmountBridge | null; } interface InactiveMetricEntry { @@ -284,6 +287,7 @@ function getMetricPaceView(snap: RateWindowSnapshot): MetricPaceView { function MetricRow({ title, snap, + amount, exhaustedLabel, resetTimeRelative, showResetWhenExhausted, @@ -293,6 +297,7 @@ function MetricRow({ }: { title: string; snap: RateWindowSnapshot; + amount?: WindowAmountBridge | null; exhaustedLabel: string; resetTimeRelative: boolean; showResetWhenExhausted: boolean; @@ -338,6 +343,13 @@ function MetricRow({ {resetText} )} + {amount && ( +
+ {amount.formattedLimit + ? `${amount.formattedUsed} ${t("PanelAmountOf")} ${amount.formattedLimit}` + : amount.formattedUsed} +
+ )} {snap.isExhausted && (
{exhaustedLabel}
)} @@ -496,6 +508,7 @@ export default function MenuCard({ id: `extra-${extra.id}`, label: extra.title, snap: extra.window, + amount: extra.amount, }); } for (const inactive of provider.inactiveRateWindows ?? []) { @@ -635,6 +648,7 @@ export default function MenuCard({ key={m.id} title={m.label} snap={m.snap} + amount={m.amount} exhaustedLabel={t("DetailWindowExhausted")} resetTimeRelative={resetTimeRelative} showResetWhenExhausted={showResetWhenExhausted} diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index f0f80280..1cfe001e 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -496,6 +496,7 @@ export const ALL_LOCALE_KEYS = [ "PanelUsedSuffix", "PanelLeftSuffix", "PanelOnPaceBudget", + "PanelAmountOf", "PanelNow", "PanelOneHour", "PanelFiveHours", diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index 342a22d8..7d09eca5 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -4854,6 +4854,17 @@ html:has(.menu-surface--tray) { color: var(--provider-status-error); } +/* Money behind a lane that is natively denominated in currency. */ +.menu-metric__amount { + font-size: 11px; + font-variant-numeric: tabular-nums; + color: var(--text-secondary); +} + +.menu-surface--tray .menu-metric__amount { + font-size: 10px; +} + .menu-metric--inactive { gap: 2px; padding: 6px 8px; diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index d2ba337d..d737cb21 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -399,6 +399,18 @@ export interface RateWindowSnapshot { reserveEtaSeconds?: number | null; } +/** + * Money behind a single metered lane. Distinct from CostSnapshotBridge, which + * is one figure for the whole provider and cannot describe individual windows. + */ +export interface WindowAmountBridge { + used: number; + limit: number | null; + currencyCode: string; + formattedUsed: string; + formattedLimit: string | null; +} + export interface CostSnapshotBridge { used: number; limit: number | null; @@ -434,6 +446,7 @@ export interface ProviderUsageSnapshot { id: string; title: string; window: RateWindowSnapshot; + amount?: WindowAmountBridge | null; }>; inactiveRateWindows?: Array<{ id: string; @@ -1006,6 +1019,7 @@ export interface ProviderDetail { id: string; title: string; window: RateWindowSnapshot; + amount?: WindowAmountBridge | null; }>; cost: CostSnapshotBridge | null; diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index 9d266637..4c2fd8f3 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -38,12 +38,56 @@ pub struct WayfinderRouteSummary { pub saved: f64, } +/// A monetary reading attached to a metered window. +/// +/// Some providers meter lanes that are natively denominated in money rather +/// than percent (Cursor's on-demand overdraft, bonus credit pools). The +/// percentage still drives the bar, but the amount is the number the user +/// actually cares about, and it has nowhere else to live: `CostSnapshot` is a +/// single slot per provider and cannot describe individual lanes. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowAmount { + /// Amount consumed in this window. + pub used: f64, + /// Amount the window is metered against, when the provider states one. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Currency code (e.g. "USD"). + pub currency_code: String, +} + +impl WindowAmount { + pub fn new(used: f64, currency_code: impl Into) -> Self { + Self { + used: finite_amount(used).unwrap_or(0.0), + limit: None, + currency_code: currency_code.into(), + } + } + + pub fn with_limit(mut self, limit: f64) -> Self { + self.limit = finite_amount(limit); + self + } + + pub fn format_used(&self) -> String { + format_currency(self.used, &self.currency_code) + } + + pub fn format_limit(&self) -> Option { + self.limit.map(|l| format_currency(l, &self.currency_code)) + } +} + /// A labeled extra usage window surfaced by provider APIs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NamedRateWindow { pub id: String, pub title: String, pub window: RateWindow, + /// Money behind this lane, when the provider denominates it that way. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub amount: Option, } /// Explicit enforcement state for a known limit window, so surfaces never have @@ -129,8 +173,15 @@ impl NamedRateWindow { id: id.into(), title: title.into(), window, + amount: None, } } + + /// Attach the money this lane is denominated in. + pub fn with_amount(mut self, amount: WindowAmount) -> Self { + self.amount = Some(amount); + self + } } /// Kind of temporary promotional signal surfaced beside normal meters. diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 8a94ff8e..d87cc6ce 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -736,6 +736,7 @@ locale_keys! { PanelUsedSuffix, PanelLeftSuffix, PanelOnPaceBudget, + PanelAmountOf, PanelNow, PanelOneHour, PanelFiveHours, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index 83692c47..fb91b4ef 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -491,6 +491,7 @@ PanelActual = Actual PanelUsedSuffix = used PanelLeftSuffix = left PanelOnPaceBudget = On-pace budget +PanelAmountOf = of PanelNow = now PanelOneHour = 1h PanelFiveHours = 5h diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 10ff75e4..0684e152 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -491,6 +491,7 @@ PanelActual = 实际 PanelUsedSuffix = 已用 PanelLeftSuffix = 剩余 PanelOnPaceBudget = 按节奏预算 +PanelAmountOf = 共 PanelNow = 现在 PanelOneHour = 1 小时 PanelFiveHours = 5 小时 diff --git a/rust/src/providers/cursor/api.rs b/rust/src/providers/cursor/api.rs index 3f582d4c..5a39512b 100755 --- a/rust/src/providers/cursor/api.rs +++ b/rust/src/providers/cursor/api.rs @@ -4,7 +4,7 @@ use crate::core::{ CostSnapshot, InactiveRateWindow, NamedRateWindow, PromoSignal, ProviderError, RateWindow, - UsageSnapshot, + UsageSnapshot, WindowAmount, }; use crate::providers::browser_cookie_header; use chrono::{DateTime, Utc}; @@ -13,6 +13,10 @@ use serde::Deserialize; const BASE_URL: &str = "https://cursor.com"; const COOKIE_DOMAINS: [&str; 2] = ["cursor.com", "cursor.sh"]; const NOT_ENFORCED: &str = "Not currently enforced by Cursor"; +const ON_DEMAND_ID: &str = "cursor-on-demand"; +const ON_DEMAND_TITLE: &str = "On-demand"; +const ON_DEMAND_PERIOD: &str = "On-demand"; +const MONTHLY_PERIOD: &str = "Monthly"; pub(super) type CursorUsageResult = (UsageSnapshot, Option); @@ -184,29 +188,44 @@ impl CursorApi { .and_then(|t| t.on_demand.as_ref()) }); - if let Some(window) = on_demand_window(on_demand, window_minutes, billing_end) { - extras.push(window); - } + let (metered, notice) = on_demand_windows(on_demand, window_minutes, billing_end); + extras.extend(metered); + inactives.extend(notice); - cost_snapshot = Self::on_demand_cost(on_demand, billing_end) + // On-demand owns the cost slot: it is the only Cursor lane that + // bills real money. Plan usage is metered at internal rates and + // charged nothing, so it must never outrank actual spend here. + cost_snapshot = Self::on_demand_cost(on_demand, billing_end, ON_DEMAND_PERIOD) .or_else(|| plan_cost_snapshot(plan, billing_end)); } else if let Some(overall) = &individual.overall { // Overall is a single reported pool — keep it as monthly + cost only. monthly_percent = Self::usage_percent(overall); - cost_snapshot = Self::on_demand_cost(Some(overall), billing_end); + cost_snapshot = Self::on_demand_cost(Some(overall), billing_end, MONTHLY_PERIOD); + + // `overall` does not replace on-demand: the overdraft meter is + // reported separately and would otherwise never be read here. + let on_demand = individual.on_demand.as_ref().or_else(|| { + summary + .team_usage + .as_ref() + .and_then(|t| t.on_demand.as_ref()) + }); + let (metered, notice) = on_demand_windows(on_demand, window_minutes, billing_end); + extras.extend(metered); + inactives.extend(notice); } } else if let Some(team) = &summary.team_usage { if let Some(pooled) = &team.pooled { monthly_percent = Self::usage_percent(pooled); - cost_snapshot = Self::on_demand_cost(Some(pooled), billing_end); - } - if let Some(window) = - on_demand_window(team.on_demand.as_ref(), window_minutes, billing_end) - { - extras.push(window); + cost_snapshot = Self::on_demand_cost(Some(pooled), billing_end, MONTHLY_PERIOD); } + let (metered, notice) = + on_demand_windows(team.on_demand.as_ref(), window_minutes, billing_end); + extras.extend(metered); + inactives.extend(notice); if cost_snapshot.is_none() { - cost_snapshot = Self::on_demand_cost(team.on_demand.as_ref(), billing_end); + cost_snapshot = + Self::on_demand_cost(team.on_demand.as_ref(), billing_end, ON_DEMAND_PERIOD); } } @@ -261,6 +280,7 @@ impl CursorApi { fn on_demand_cost( on_demand: Option<&OnDemandUsage>, billing_end: Option>, + period: &str, ) -> Option { let usage = on_demand?; if usage.enabled == Some(false) { @@ -281,7 +301,7 @@ impl CursorApi { return None; } - let mut cost = CostSnapshot::new(used_cents / 100.0, "USD", "Monthly"); + let mut cost = CostSnapshot::new(used_cents / 100.0, "USD", period); if limit_cents > 0.0 { cost = cost.with_limit(limit_cents / 100.0); } @@ -431,23 +451,58 @@ fn promotional_window( )) } -fn on_demand_window( +/// Cursor reports on-demand two ways: as a capped pool that can be metered, or +/// as uncapped spend with no limit at all. Uncapped overdraft is still real +/// money, so surface it as an explicit non-metering window instead of dropping +/// it on the floor for want of a denominator. +fn on_demand_windows( on_demand: Option<&OnDemandUsage>, window_minutes: Option, billing_end: Option>, -) -> Option { - let usage = on_demand?; +) -> (Option, Option) { + let Some(usage) = on_demand else { + return (None, None); + }; if usage.enabled == Some(false) { - return None; + return (None, None); } - let percent = CursorApi::usage_percent(usage)?; - Some(NamedRateWindow::new( - "cursor-on-demand", - "On-demand", - RateWindow::with_details(percent, window_minutes, billing_end, None), - )) + + if let Some(percent) = CursorApi::usage_percent(usage) { + let mut window = NamedRateWindow::new( + ON_DEMAND_ID, + ON_DEMAND_TITLE, + RateWindow::with_details(percent, window_minutes, billing_end, None), + ); + // The percentage drives the bar, but overdraft is money — show it. + if let Some(amount) = on_demand_amount(usage) { + window = window.with_amount(amount); + } + return (Some(window), None); + } + + // No limit and no remaining: nothing to meter against, but the spend itself + // is worth showing once the plan is exhausted. + let used_cents = usage.used.unwrap_or(0); + if used_cents <= 0 { + return (None, None); + } + let notice = InactiveRateWindow::new( + ON_DEMAND_ID, + ON_DEMAND_TITLE, + format!( + "${:.2} spent, no spend limit set", + used_cents as f64 / 100.0 + ), + ); + (None, Some(notice)) } +/// The plan lane expressed in currency. +/// +/// Note this is *not* money owed. Cursor prices included usage at internal +/// rates, and an exported usage report shows every such event as "Included" or +/// "Free" with no charge. Only the on-demand lane is real billing, which is why +/// it takes the cost slot ahead of this. fn plan_cost_snapshot( plan: &PlanUsage, billing_end: Option>, @@ -472,6 +527,20 @@ fn plan_cost_snapshot( Some(cost) } +/// Dollars behind an on-demand lane, for display beside its meter. +fn on_demand_amount(usage: &OnDemandUsage) -> Option { + let used = usage.used? as f64; + let mut amount = WindowAmount::new(used / 100.0, "USD"); + if let Some(limit) = usage + .limit + .or_else(|| usage.remaining.map(|r| r + usage.used.unwrap_or(0))) + .filter(|&l| l > 0) + { + amount = amount.with_limit(limit as f64 / 100.0); + } + Some(amount) +} + fn membership_label(membership: Option<&str>) -> Option { membership.map(|t| match t.to_lowercase().as_str() { "enterprise" => "Cursor Enterprise".to_string(), @@ -740,15 +809,111 @@ mod tests { assert!((on_demand.window.used_percent - 35.0).abs() < 0.01); assert!(on_demand.window.resets_at.is_some()); + // The overdraft meter carries its own dollars, so it stays visible even + // though the single cost slot belongs to plan spend. + let amount = on_demand.amount.as_ref().expect("on-demand carries money"); + assert!((amount.used - 3.50).abs() < 0.01); + assert_eq!(amount.limit, Some(10.0)); + assert_eq!(amount.format_used(), "$3.50"); + // Lane format with only total → Auto/API inactive assert_eq!(usage.inactive_rate_windows.len(), 2); - let cost = cost.expect("cost should exist from on-demand usage"); + // On-demand is the only real-money lane, so it owns the cost slot. + let cost = cost.expect("on-demand cost"); assert!((cost.used - 3.5).abs() < 0.01); assert_eq!(cost.limit, Some(10.0)); + assert_eq!(cost.period, "On-demand"); + } + + #[test] + fn test_cursor_uncapped_on_demand_reports_spend() { + // On-demand enabled with real spend but no cap: there is no denominator + // to meter, so the spend surfaces as an explicit non-metering window. + let json = r#"{ + "billingCycleEnd": "2026-09-01T00:00:00Z", + "membershipType": "pro", + "individualUsage": { + "plan": { + "used": 2000, + "limit": 2000, + "totalPercentUsed": 100.0 + }, + "onDemand": { + "enabled": true, + "used": 1234 + } + } + }"#; + + let summary = parse_summary(json); + let (usage, cost) = api().build_result(summary, None).unwrap(); + + assert!( + !usage + .extra_rate_windows + .iter() + .any(|w| w.id == "cursor-on-demand"), + "no cap means no meter" + ); + let notice = inactive(&usage, "cursor-on-demand"); + assert_eq!(notice.title, "On-demand"); + assert_eq!(notice.description, "$12.34 spent, no spend limit set"); + + let cost = cost.expect("uncapped on-demand still reports spend"); + assert!((cost.used - 12.34).abs() < 0.01); + assert_eq!(cost.limit, None); + assert_eq!(cost.period, "On-demand"); + } + + #[test] + fn test_cursor_on_demand_surfaces_alongside_overall() { + let json = r#"{ + "individualUsage": { + "overall": {"used": 2500, "limit": 2500}, + "onDemand": {"enabled": true, "used": 900, "limit": 5000} + } + }"#; + + let summary = parse_summary(json); + let (usage, cost) = api().build_result(summary, None).unwrap(); + + assert!((usage.primary.used_percent - 100.0).abs() < 0.01); + let on_demand = extra(&usage, "cursor-on-demand"); + assert!((on_demand.window.used_percent - 18.0).abs() < 0.01); + + // `overall` remains the reported total pool cost. + let cost = cost.expect("overall cost"); + assert!((cost.used - 25.0).abs() < 0.01); assert_eq!(cost.period, "Monthly"); } + #[test] + fn test_cursor_zero_spend_uncapped_on_demand_is_silent() { + let json = r#"{ + "individualUsage": { + "plan": {"used": 100, "limit": 1000, "totalPercentUsed": 10.0}, + "onDemand": {"enabled": true, "used": 0} + } + }"#; + + let summary = parse_summary(json); + let (usage, _) = api().build_result(summary, None).unwrap(); + + assert!( + !usage + .inactive_rate_windows + .iter() + .any(|w| w.id == "cursor-on-demand") + ); + assert!( + !usage + .extra_rate_windows + .iter() + .any(|w| w.id == "cursor-on-demand") + ); + } + #[test] fn test_cursor_disabled_on_demand_is_ignored() { let json = r#"{ From cbcb8a2344d24206805332601923f439dae11ada Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Tue, 4 Aug 2026 21:40:22 -0400 Subject: [PATCH 2/2] Address review: on-demand amount, currency formatting, team cost order - Treat an omitted `used` as zero in `on_demand_amount`, matching `usage_percent`. A cap reported without a `used` field dropped the whole amount instead of showing zero spend against a real limit. - Format the uncapped notice through `WindowAmount` rather than a hard-coded `$`, so it follows the same currency path as every other amount. - Give on-demand the cost slot ahead of `team.pooled`, matching the individual branch. On-demand is the billed lane; the pooled allowance is not, and letting it win contradicted the stated rule. --- rust/src/providers/cursor/api.rs | 62 +++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/rust/src/providers/cursor/api.rs b/rust/src/providers/cursor/api.rs index 5a39512b..da0c2475 100755 --- a/rust/src/providers/cursor/api.rs +++ b/rust/src/providers/cursor/api.rs @@ -217,15 +217,19 @@ impl CursorApi { } else if let Some(team) = &summary.team_usage { if let Some(pooled) = &team.pooled { monthly_percent = Self::usage_percent(pooled); - cost_snapshot = Self::on_demand_cost(Some(pooled), billing_end, MONTHLY_PERIOD); } let (metered, notice) = on_demand_windows(team.on_demand.as_ref(), window_minutes, billing_end); extras.extend(metered); inactives.extend(notice); + // Same rule as the individual branch: on-demand is the billed lane, + // so it takes the cost slot ahead of the pooled plan allowance. + cost_snapshot = + Self::on_demand_cost(team.on_demand.as_ref(), billing_end, ON_DEMAND_PERIOD); if cost_snapshot.is_none() { - cost_snapshot = - Self::on_demand_cost(team.on_demand.as_ref(), billing_end, ON_DEMAND_PERIOD); + cost_snapshot = team.pooled.as_ref().and_then(|pooled| { + Self::on_demand_cost(Some(pooled), billing_end, MONTHLY_PERIOD) + }); } } @@ -486,13 +490,11 @@ fn on_demand_windows( if used_cents <= 0 { return (None, None); } + let spent = WindowAmount::new(used_cents as f64 / 100.0, "USD").format_used(); let notice = InactiveRateWindow::new( ON_DEMAND_ID, ON_DEMAND_TITLE, - format!( - "${:.2} spent, no spend limit set", - used_cents as f64 / 100.0 - ), + format!("{spent} spent, no spend limit set"), ); (None, Some(notice)) } @@ -529,7 +531,9 @@ fn plan_cost_snapshot( /// Dollars behind an on-demand lane, for display beside its meter. fn on_demand_amount(usage: &OnDemandUsage) -> Option { - let used = usage.used? as f64; + // Treat an omitted `used` as zero, the way `usage_percent` does, so a + // reported cap still renders instead of the amount vanishing entirely. + let used = usage.used.unwrap_or(0) as f64; let mut amount = WindowAmount::new(used / 100.0, "USD"); if let Some(limit) = usage .limit @@ -957,6 +961,48 @@ mod tests { assert!(usage.extra_rate_windows.is_empty()); } + #[test] + fn test_cursor_on_demand_cap_renders_without_a_used_field() { + // Cursor can report a cap with no `used`. That is zero spend against a + // real limit, not an absent amount. + let json = r#"{ + "individualUsage": { + "plan": {"used": 100, "limit": 1000, "totalPercentUsed": 10.0}, + "onDemand": {"enabled": true, "limit": 2000} + } + }"#; + + let summary = parse_summary(json); + let (usage, _) = api().build_result(summary, None).unwrap(); + + let amount = extra(&usage, "cursor-on-demand") + .amount + .as_ref() + .expect("a cap is still an amount"); + assert_eq!(amount.used, 0.0); + assert_eq!(amount.limit, Some(20.0)); + } + + #[test] + fn test_cursor_team_on_demand_outranks_pooled_for_cost() { + // On-demand is the billed lane for teams too, so it must not be + // shadowed by the pooled plan allowance. + let json = r#"{ + "teamUsage": { + "pooled": {"used": 5000, "limit": 10000}, + "onDemand": {"enabled": true, "used": 750, "limit": 2000} + } + }"#; + + let summary = parse_summary(json); + let (usage, cost) = api().build_result(summary, None).unwrap(); + + assert!((usage.primary.used_percent - 50.0).abs() < 0.01); + let cost = cost.expect("on-demand cost"); + assert!((cost.used - 7.5).abs() < 0.01); + assert_eq!(cost.period, "On-demand"); + } + #[test] fn test_cursor_team_pooled_fallback() { let summary = parse_summary(r#"{"teamUsage":{"pooled":{"used":5000,"limit":10000}}}"#);