diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2cec1cef4..f853a3f44f6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## [Windows] Unreleased +- DeepSeek: the tray status and the float bar show the account balance (for example `¥231.01`) instead of a meaningless 0%/100% percentage, matching upstream's balance-only menu-bar display. The tray icon keeps its availability meter, and Paid/Granted stay on the card as display details. - Tray panel: **Settings > Menu > Panel look** switches between Modern and Classic. Classic is the layout from before the redesign (icon-only provider switcher, percent after the title, the full cost dashboard, shortcut hints and icons on every menu row). Drag either vertical edge of the panel to change its width (280 to 560 px at 100 % Panel scale); the width is saved, arrow keys on the focused edge step it, and a double-click or Home restores 310 px. At a high Panel scale the panel never grows wider than the screen's work area. - Tray panel: a new look. The panel sits on a soft tinted surface with larger type. Provider tabs show an icon and a name over a quota underline, wrap to extra rows when many providers are on, and keep the dense icon grid beyond 18. Each quota row is a title, a thicker bar, then the percent used on the left and the reset on the right. Sections are set off by hairlines, and the daily-cost dashboard becomes a collapsible **Cost** section that opens from a two-line summary. The menu is text-only: shortcut hints and the Settings, About and Quit icons are gone, and About no longer carries the version. - Claude: when Claude Code signs out but keeps its login entry with blank tokens, Settings and the tray still list the saved Claude Code accounts, and Switch and Add account work again. Before, every account action failed with "Claude login is missing accessToken" and "No Claude Code accounts found", even though the accounts were saved. diff --git a/apps/desktop-tauri/scripts/proof-scenarios.json b/apps/desktop-tauri/scripts/proof-scenarios.json index c21467375cf..35ae98d9a1b 100644 --- a/apps/desktop-tauri/scripts/proof-scenarios.json +++ b/apps/desktop-tauri/scripts/proof-scenarios.json @@ -882,6 +882,17 @@ { "screenshot": "floatbar" } ] }, + { + "name": "floatbar-deepseek-balance", + "query": "window=floatbar&fixture=deepseek-balance", + "size": [520, 120], + "steps": [ + { "expectSelector": ".floatbar__pill" }, + { "expectText": "¥231.01" }, + { "expectNoText": "0%" }, + { "screenshot": "floatbar-deepseek-balance" } + ] + }, { "name": "tray-agent-sessions-hosts", "query": "window=main&fixture=agent-sessions-hosts", diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 48cdac7f9c4..d7865242895 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -370,6 +370,11 @@ pub struct ProviderUsageSnapshot { pub inventory: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub display_details: Vec, + /// Short balance text for a balance-only provider (upstream + /// `balanceOnly`); the tray status and float bar show it in place of a + /// percentage. Absent for metered providers and when no balance is known. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub balance_text: Option, #[serde(default)] pub cost: Option, #[serde(default)] @@ -461,6 +466,7 @@ impl Default for ProviderUsageSnapshot { quota_estimated: false, account_organization: None, tray_status_label: None, + balance_text: None, fetch_duration_ms: None, wayfinder_usage: None, open_ai_api_usage: None, @@ -659,7 +665,7 @@ impl ProviderUsageSnapshot { usage.secondary.as_ref(), ); - Self { + let mut snapshot = Self { provider_id: id.cli_name().to_string(), display_name: id.display_name().to_string(), primary: primary_snap, @@ -801,13 +807,16 @@ impl ProviderUsageSnapshot { quota_estimated: result.quota_estimated, account_organization: usage.account_organization.clone(), tray_status_label: None, + balance_text: None, fetch_duration_ms: None, wayfinder_usage: result.wayfinder_usage.clone(), open_ai_api_usage: result.open_ai_api_usage.as_ref().map(Into::into), throughput: result.throughput.as_ref().map(Into::into), session_equivalent_forecast, quota_burndown, - } + }; + snapshot.balance_text = crate::tray_presentation::balance_only_status_text(&snapshot); + snapshot } pub(super) fn from_error( diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs index 7487e0275cb..e2972eeb53c 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs @@ -504,6 +504,7 @@ pub(crate) fn metric_status_label( .unwrap_or_default(); named_extra_status(snapshot, &preference, settings.show_as_used, language) .or_else(|| tokens_per_minute_status(snapshot, settings, language)) + .or_else(|| balance_only_status_text(snapshot)) .or_else(|| balance_status_label(snapshot)) .unwrap_or_else(|| headline_status_label(snapshot, settings.show_as_used, language)) } @@ -589,6 +590,29 @@ pub(crate) fn tokens_per_minute_status( ) } +/// Balance text for a balance-only provider (upstream +/// `ProviderMetadata.balanceOnly`), such as DeepSeek's account balance. The +/// headline window carries no quota length or reset, so its description is the +/// balance itself. `None` for metered providers and when no text was reported. +pub(crate) fn balance_only_status_text(snapshot: &ProviderUsageSnapshot) -> Option { + let provider = ProviderId::from_cli_name(&snapshot.provider_id)?; + if !provider.is_balance_only() { + return None; + } + let headline = headline_window(snapshot); + if !headline.is_informational + && (headline.window_minutes.is_some() || headline.resets_at.is_some()) + { + return None; + } + headline + .reset_description + .as_deref() + .map(str::trim) + .filter(|text| !text.is_empty()) + .map(str::to_string) +} + /// Balance text for a balance-carrying provider whose headline window is /// informational (no monthly meter), such as `$6.76` for Nous Portal. It is the /// first reported `display_details` row in the provider's declared order 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 80d041e31b1..c7cb10e7838 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -1455,6 +1455,37 @@ fn balance_only_nous_status_row_reads_its_balance() { ); } +#[test] +fn balance_only_deepseek_status_row_reads_its_balance() { + let settings = Settings { + tray_icon_mode: TrayIconMode::PerProvider, + show_as_used: true, + ..Settings::default() + }; + // A balance-only provider reports no quota length or reset; the headline + // description is the balance itself. + let mut deepseek = fake_snapshot("deepseek", "DeepSeek", 0.0); + deepseek.primary.reset_description = Some("¥231.01".to_string()); + // A window with a real length keeps its percentage. + let mut metered = fake_snapshot("deepseek", "DeepSeek", 25.0); + metered.primary.window_minutes = Some(300); + metered.primary.reset_description = Some("¥231.01".to_string()); + + let balance_only = vec![deepseek]; + let plan = TrayPresentationPlan::resolve(&settings, &balance_only, None); + assert_eq!( + plan.status_labels(Language::English), + vec![("deepseek".to_string(), "DeepSeek ¥231.01".to_string())] + ); + + let metered = vec![metered]; + let plan = TrayPresentationPlan::resolve(&settings, &metered, None); + assert_eq!( + plan.status_labels(Language::English), + vec![("deepseek".to_string(), "DeepSeek 25% used".to_string())] + ); +} + #[test] fn tokens_per_minute_metric_without_reported_minutes_is_unavailable() { let mut settings = Settings::default(); diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.display.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.display.test.tsx index e7df51b318d..7c68dc76720 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.display.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.display.test.tsx @@ -104,6 +104,23 @@ describe("FloatBar", () => { }); }); + it("shows a declared balance text in place of the percentage", async () => { + tauriMocks.getCachedProviders.mockResolvedValue([ + snapshot("deepseek", "DeepSeek", 0, { + balanceText: "¥231.01", + resetDescription: "¥231.01", + }), + ]); + tauriMocks.getSettingsSnapshot.mockResolvedValue(settings({ enabledProviders: ["deepseek"] })); + const { container } = renderFloatBar(bootstrap({ enabledProviders: ["deepseek"] })); + await waitFor(() => { + expect(container.querySelector(".floatbar__pct")?.textContent).toBe("¥231.01"); + expect(container.querySelector(".floatbar__pill")?.getAttribute("title")).toBe( + "DeepSeek: ¥231.01", + ); + }); + }); + it("applies warning tone when remaining drops below the high threshold", async () => { // highUsageThreshold = 70 → high-remaining cutoff = 30%. // claude at 80% used → 20% remaining → critical (also below crit cutoff 10). diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.tsx index 8b0188a63c8..a27531250ed 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.tsx @@ -311,6 +311,7 @@ function MetricSegment({ showResetInline, resetIconSize, problemLabel, + textLabel = false, }: { label: string; rateWindow: RateWindowSnapshot; @@ -320,11 +321,15 @@ function MetricSegment({ resetIconSize: number; /** Set while the provider is in an error state; replaces the percentage. */ problemLabel: string | null; + /** The label is provider text (a balance), so it sizes like informational text. */ + textLabel?: boolean; }) { - // Only percentages size the warning placeholder; informational text would - // widen the pill after an error. + // Only percentages size the warning placeholder; text labels would widen the + // pill after an error. const lastPercentageLabel = useRef("0%"); - if (!problemLabel && rateWindow.isInformational !== true) lastPercentageLabel.current = label; + if (!problemLabel && rateWindow.isInformational !== true && textLabel !== true) { + lastPercentageLabel.current = label; + } const inlineReset = resetText ? inlineResetTime(resetText, rateWindow.resetsAt, resetRelative) : null; @@ -395,30 +400,40 @@ function ProviderPill({ const state = describeProviderState(provider.errorState); // An error collapses the pill to its first metric with a warning mark. const metrics = pillMetrics(provider, showWeekly && !state.isProblem); + // A balance-only provider (upstream `balanceOnly`) reports no quota + // percentage, so its single metric shows the balance text instead. + const balanceText = localizeProviderText(provider.balanceText?.trim(), t) || null; + const showBalance = metrics.length === 1 ? balanceText : null; const resetTexts = [ - useWindowResetText(metrics[0].window, resetRelative), + useWindowResetText(showBalance ? null : metrics[0].window, resetRelative), useWindowResetText(metrics[1]?.window ?? null, resetRelative), ]; let tone: PillTone = "ok"; if (state.isProblem) tone = "crit"; else { - for (const metric of metrics) { - const next = metricTone(metric, provider.providerId, thresholds); + metrics.forEach((metric, index) => { + const next = + index === 0 && showBalance + ? "ok" + : metricTone(metric, provider.providerId, thresholds); if (TONE_RANK[next] > TONE_RANK[tone]) tone = next; - } + }); } const displaySuffix = showAsUsed ? usedSuffix : remainingSuffix; - const labels = metrics.map((metric) => metricLabel(metric.window, showAsUsed, t)); + const labels = metrics.map((metric, index) => + index === 0 && showBalance ? showBalance : metricLabel(metric.window, showAsUsed, t), + ); const title = state.isProblem ? `${provider.displayName}: ${stateLabel}` : metrics .map((metric, index) => { const resetText = resetTexts[index]; const detailText = localizeProviderText(windowDetailText(metric.window), t) || null; + const textOnly = index === 0 && showBalance !== null; const head = - metric.window.isInformational === true + textOnly || metric.window.isInformational === true ? `${provider.displayName}: ${labels[index]}` : `${provider.displayName}: ${labels[index]} ${displaySuffix}`; return [head, resetText, detailText].filter(Boolean).join("\n"); @@ -450,6 +465,7 @@ function ProviderPill({ showResetInline={showResetInline} resetIconSize={resetIconSize} problemLabel={state.isProblem ? stateLabel : null} + textLabel={index === 0 && showBalance !== null} /> ))} diff --git a/apps/desktop-tauri/src/proof/fixtures/scenarios.ts b/apps/desktop-tauri/src/proof/fixtures/scenarios.ts index 1f465ee8e05..0b3716d1e2e 100644 --- a/apps/desktop-tauri/src/proof/fixtures/scenarios.ts +++ b/apps/desktop-tauri/src/proof/fixtures/scenarios.ts @@ -18,6 +18,7 @@ import type { ProviderCatalogEntry, ProviderUsageSnapshot, QuotaWindowHistoryBridge, + RateWindowSnapshot, SettingsSnapshot, SpendModelRow, TokenAccountBridge, @@ -63,6 +64,7 @@ export const PROOF_SCENARIO_NAMES = [ "vercel", "vercel-team-id-invalid", "kimi-bonus-credits", + "deepseek-balance", "agent-sessions-hosts", "copilot-daily-target", ] as const; @@ -1268,6 +1270,33 @@ function kimiBonusCreditsScenario(now: Date): ProofScenario { }; } +/** A balance-only DeepSeek account: surfaces show the balance text, not a percent. */ +function deepseekBalanceScenario(now: Date): ProofScenario { + const primary: RateWindowSnapshot = { + ...rateWindow(now, 0, null, null), + resetDescription: "¥231.01", + }; + const snapshot = providerSnapshot( + { + providerId: "deepseek", + displayName: "DeepSeek", + primary, + primaryLabel: "Balance", + updatedAt: isoBefore(now, 1), + }, + { balanceText: "¥231.01" }, + ); + return { + name: "deepseek-balance", + settings: baseSettings({ enabledProviders: ["deepseek"], floatBarProviderIds: ["deepseek"] }), + catalog: PROOF_PROVIDER_CATALOG, + providers: [snapshot], + codexAccounts: emptyCodexAccounts(), + claudeAccounts: [], + localSpend: [], + }; +} + /** A monthly allowance with a daily target and no pace: 52% left over 18 days. */ function copilotDailyTargetScenario(now: Date): ProofScenario { const snapshot = copilotSnapshot(now); @@ -1364,6 +1393,7 @@ const BUILDERS: Record ProofScenario> = { vercel: vercelScenario, "vercel-team-id-invalid": vercelTeamIdInvalidScenario, "kimi-bonus-credits": kimiBonusCreditsScenario, + "deepseek-balance": deepseekBalanceScenario, "agent-sessions-hosts": agentSessionsHostsScenario, "copilot-daily-target": copilotDailyTargetScenario, }; diff --git a/apps/desktop-tauri/src/test/floatBarHarness.tsx b/apps/desktop-tauri/src/test/floatBarHarness.tsx index 907960ea927..ee618698990 100644 --- a/apps/desktop-tauri/src/test/floatBarHarness.tsx +++ b/apps/desktop-tauri/src/test/floatBarHarness.tsx @@ -42,6 +42,7 @@ export function snapshot( resetDescription?: string | null; descriptionIsDetail?: boolean; informational?: boolean; + balanceText?: string | null; secondary?: { used: number; exhausted?: boolean; @@ -76,6 +77,7 @@ export function snapshot( updatedAt: "2026-05-15T00:00:00Z", error: opts.error ?? null, errorState: opts.errorState ?? "ready", + balanceText: opts.balanceText ?? null, }); } diff --git a/apps/desktop-tauri/src/types/bridge/usage.ts b/apps/desktop-tauri/src/types/bridge/usage.ts index 207f84fbbda..e60cf2fef3a 100644 --- a/apps/desktop-tauri/src/types/bridge/usage.ts +++ b/apps/desktop-tauri/src/types/bridge/usage.ts @@ -216,6 +216,11 @@ export interface ProviderUsageSnapshot { inventory?: ProviderInventoryItem[]; /** Provider-specific display rows; never used as quota math or persistence. */ displayDetails?: ProviderDisplayDetail[]; + /** + * Short balance text for a balance-only provider (upstream `balanceOnly`); + * tray status and the float bar show it in place of a percentage. + */ + balanceText?: string | null; /** Presentation-only hidden metric/extra row IDs. */ hiddenUsageItemIds?: string[]; cost: CostSnapshotBridge | null; diff --git a/docs/proof/deepseek-balance/floatbar.png b/docs/proof/deepseek-balance/floatbar.png new file mode 100644 index 00000000000..e00310714df Binary files /dev/null and b/docs/proof/deepseek-balance/floatbar.png differ diff --git a/rust/src/core/provider/id.rs b/rust/src/core/provider/id.rs index fe65b579189..cdb0077c8ec 100755 --- a/rust/src/core/provider/id.rs +++ b/rust/src/core/provider/id.rs @@ -223,6 +223,14 @@ impl ProviderId { self.spec().balance_detail_ids } + /// Whether the provider reports a balance instead of a quota meter + /// (upstream `ProviderMetadata.balanceOnly`). The tray status and the + /// float bar show the headline description as balance text where a + /// percentage would otherwise appear. + pub fn is_balance_only(self) -> bool { + self.spec().balance_only + } + /// Parse from CLI name string pub fn from_cli_name(name: &str) -> Option { let name = name.to_lowercase(); @@ -966,4 +974,14 @@ mod tests { } } } + + #[test] + fn only_deepseek_is_balance_only() { + assert!(ProviderId::DeepSeek.is_balance_only()); + for &id in ProviderId::all() { + if id != ProviderId::DeepSeek { + assert!(!id.is_balance_only(), "{id:?}"); + } + } + } } diff --git a/rust/src/core/provider/spec.rs b/rust/src/core/provider/spec.rs index f2f84adecfe..5b458d9365a 100644 --- a/rust/src/core/provider/spec.rs +++ b/rust/src/core/provider/spec.rs @@ -41,6 +41,10 @@ pub(crate) struct ProviderSpec { /// when the headline window is informational, so a balance-only account /// never reads as a generic label. Empty for providers without a balance. pub(crate) balance_detail_ids: &'static [&'static str], + /// The provider reports an account balance, not a quota meter (upstream + /// `ProviderMetadata.balanceOnly`). Surfaces show the headline description + /// as the balance text where a percentage would otherwise appear. + pub(crate) balance_only: bool, } impl ProviderSpec { @@ -115,6 +119,11 @@ impl ProviderSpec { self.balance_detail_ids = ids; self } + + const fn balance_only(mut self) -> Self { + self.balance_only = true; + self + } } const fn spec( @@ -145,6 +154,7 @@ const fn spec( aliases: &[], parse_aliases: &[], balance_detail_ids: &[], + balance_only: false, } } @@ -383,6 +393,7 @@ pub(crate) static SPECS: [ProviderSpec; COUNT] = [ .parse_aliases(&["code-rabbit", "code rabbit"]), spec(P::DeepSeek, "deepseek", "DeepSeek", "#4D6BFE") .labels("Balance", "Balance") + .balance_only() .credits() .dashboard("https://platform.deepseek.com/usage") .status("https://status.deepseek.com") diff --git a/rust/src/providers/deepseek/mod.rs b/rust/src/providers/deepseek/mod.rs index 537058c951b..92fb423f322 100644 --- a/rust/src/providers/deepseek/mod.rs +++ b/rust/src/providers/deepseek/mod.rs @@ -17,8 +17,8 @@ pub mod pricing; mod session_resolver; use crate::core::{ - CostSnapshot, FetchContext, LastGoodFailurePolicy, Provider, ProviderError, - ProviderFetchResult, ProviderId, RateWindow, SourceMode, UsageSnapshot, + CostSnapshot, FetchContext, LastGoodFailurePolicy, Provider, ProviderDisplayDetail, + ProviderError, ProviderFetchResult, ProviderId, RateWindow, SourceMode, UsageSnapshot, }; const DEEPSEEK_API_BASE: &str = "https://api.deepseek.com"; @@ -233,10 +233,14 @@ impl DeepSeekProvider { match resolution { // A deadline has no attributable session, so it never retains a balance. Err(_) => Err(ProviderError::Timeout.with_failure_owner(None)), - Ok(session_resolver::Resolution::Balance { balance, owner }) => Ok( - ProviderFetchResult::new(Self::snapshot_from_balance(*balance), "web") - .with_last_good_owner(owner), - ), + Ok(session_resolver::Resolution::Balance { balance, owner }) => { + let details = Self::balance_display_details(&balance); + Ok( + ProviderFetchResult::new(Self::snapshot_from_balance(*balance), "web") + .with_last_good_owner(owner) + .with_display_details(details), + ) + } Ok(session_resolver::Resolution::SessionRequired) => Err(match missing_key { ProviderError::NotInstalled(message) => ProviderError::NotInstalled(format!( "{message} Or sign in to platform.deepseek.com in Chrome." @@ -279,6 +283,7 @@ impl DeepSeekProvider { let balance: BalanceResponse = resp.json().await.map_err(|e| { ProviderError::Parse(format!("Failed to parse DeepSeek balance response: {e}")) })?; + let details = Self::balance_display_details(&balance); let mut usage = Self::snapshot_from_balance(balance); let mut result = ProviderFetchResult::new(usage.clone(), "api"); @@ -298,7 +303,7 @@ impl DeepSeekProvider { } } - Ok(result) + Ok(result.with_display_details(details)) } async fn fetch_usage_summary( @@ -351,8 +356,6 @@ impl DeepSeekProvider { }; let total = parse_money(&info.total_balance); - let granted = parse_money(&info.granted_balance); - let topped_up = parse_money(&info.topped_up_balance); let symbol = currency_symbol(&info.currency); let mut window = RateWindow::new(if !balance.is_available || total <= 0.0 { @@ -361,16 +364,14 @@ impl DeepSeekProvider { 0.0 }); + // The short balance text is what the tray status and float bar show; + // the paid/granted breakdown travels beside it as display details. window.reset_description = if !balance.is_available { Some("Balance unavailable for API calls".to_string()) } else if total <= 0.0 { - Some(format!( - "{symbol}0.00 — add credits at platform.deepseek.com" - )) + Some(format!("{symbol}0.00 - add credits")) } else { - Some(format!( - "{symbol}{total:.2} (Paid: {symbol}{topped_up:.2} / Granted: {symbol}{granted:.2})" - )) + Some(format!("{symbol}{total:.2}")) }; UsageSnapshot::new(window).with_login_method(format!( @@ -379,6 +380,34 @@ impl DeepSeekProvider { )) } + /// Paid/granted breakdown rows for the card. The window description keeps + /// only the short balance text used by the tray status and float bar. + fn balance_display_details(balance: &BalanceResponse) -> Vec { + if !balance.is_available { + return Vec::new(); + } + let Some(info) = select_balance_info(&balance.balance_infos) else { + return Vec::new(); + }; + let symbol = currency_symbol(&info.currency); + let paid = parse_money(&info.topped_up_balance); + let granted = parse_money(&info.granted_balance); + let mut rows = Vec::new(); + if let Some(row) = + ProviderDisplayDetail::new("deepseek-paid", "Paid", format!("{symbol}{paid:.2}")) + { + rows.push(row); + } + if let Some(row) = ProviderDisplayDetail::new( + "deepseek-granted", + "Granted", + format!("{symbol}{granted:.2}"), + ) { + rows.push(row); + } + rows + } + fn apply_usage_summary( mut usage: UsageSnapshot, summary: &DeepSeekUsageSummary, diff --git a/rust/src/providers/deepseek/platform_balance_tests.rs b/rust/src/providers/deepseek/platform_balance_tests.rs index eee4c68c4f9..0c0504aec58 100644 --- a/rust/src/providers/deepseek/platform_balance_tests.rs +++ b/rust/src/providers/deepseek/platform_balance_tests.rs @@ -20,10 +20,7 @@ fn sums_wallets_per_currency_and_prefers_funded_usd() { assert!(response.is_available); let snapshot = crate::providers::deepseek::DeepSeekProvider::snapshot_from_balance(response); - assert_eq!( - snapshot.primary.reset_description.as_deref(), - Some("$3.75 (Paid: $2.50 / Granted: $1.25)") - ); + assert_eq!(snapshot.primary.reset_description.as_deref(), Some("$3.75")); } #[test] diff --git a/rust/src/providers/deepseek/tests.rs b/rust/src/providers/deepseek/tests.rs index 4acac37f8db..2820d3f7622 100644 --- a/rust/src/providers/deepseek/tests.rs +++ b/rust/src/providers/deepseek/tests.rs @@ -1,7 +1,7 @@ use super::*; #[test] -fn prefers_usd_balance_and_formats_paid_and_granted() { +fn prefers_usd_balance_and_shows_the_short_amount() { let snapshot = DeepSeekProvider::snapshot_from_balance(BalanceResponse { is_available: true, balance_infos: vec![ @@ -21,10 +21,7 @@ fn prefers_usd_balance_and_formats_paid_and_granted() { }); assert_eq!(snapshot.primary.used_percent, 0.0); - assert_eq!( - snapshot.primary.reset_description.as_deref(), - Some("$3.50 (Paid: $3.00 / Granted: $0.50)") - ); + assert_eq!(snapshot.primary.reset_description.as_deref(), Some("$3.50")); } #[test] @@ -48,13 +45,47 @@ fn uses_cny_balance_when_usd_is_empty() { }); assert_eq!(snapshot.primary.used_percent, 0.0); + let symbol = currency_symbol("CNY"); + let expected = format!("{symbol}42.25"); assert_eq!( snapshot.primary.reset_description.as_deref(), - Some("¥42.25 (Paid: ¥40.00 / Granted: ¥2.25)") + Some(expected.as_str()) ); + let expected_login = format!("CNY balance: {symbol}42.25"); assert_eq!( snapshot.login_method.as_deref(), - Some("CNY balance: ¥42.25") + Some(expected_login.as_str()) + ); +} + +#[test] +fn exposes_paid_and_granted_as_display_details() { + let balance = BalanceResponse { + is_available: true, + balance_infos: vec![BalanceInfo { + currency: "CNY".into(), + total_balance: "42.25".into(), + granted_balance: "2.25".into(), + topped_up_balance: "40".into(), + }], + }; + let details = DeepSeekProvider::balance_display_details(&balance); + let symbol = currency_symbol("CNY"); + let paid = format!("{symbol}40.00"); + let granted = format!("{symbol}2.25"); + assert_eq!(details.len(), 2); + assert_eq!(details[0].id(), "deepseek-paid"); + assert_eq!(details[0].title(), "Paid"); + assert_eq!(details[0].value(), paid.as_str()); + assert_eq!(details[1].id(), "deepseek-granted"); + assert_eq!(details[1].title(), "Granted"); + assert_eq!(details[1].value(), granted.as_str()); + + let snapshot = DeepSeekProvider::snapshot_from_balance(balance); + let expected_description = format!("{symbol}42.25"); + assert_eq!( + snapshot.primary.reset_description.as_deref(), + Some(expected_description.as_str()) ); } @@ -79,9 +110,10 @@ fn keeps_zero_usd_when_no_currency_has_balance() { }); assert_eq!(snapshot.primary.used_percent, 100.0); + let expected = format!("{}0.00 - add credits", currency_symbol("USD")); assert_eq!( snapshot.primary.reset_description.as_deref(), - Some("$0.00 — add credits at platform.deepseek.com") + Some(expected.as_str()) ); assert_eq!(snapshot.login_method.as_deref(), Some("USD balance: $0.00")); }