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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop-tauri/scripts/proof-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ pub struct ProviderUsageSnapshot {
pub inventory: Vec<ProviderInventoryItemSnapshot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub display_details: Vec<ProviderDisplayDetailSnapshot>,
/// 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<String>,
#[serde(default)]
pub cost: Option<CostSnapshotBridge>,
#[serde(default)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop-tauri/src-tauri/src/tray_presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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<String> {
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
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop-tauri/src/floatbar/FloatBar.display.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
34 changes: 25 additions & 9 deletions apps/desktop-tauri/src/floatbar/FloatBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ function MetricSegment({
showResetInline,
resetIconSize,
problemLabel,
textLabel = false,
}: {
label: string;
rateWindow: RateWindowSnapshot;
Expand All @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -450,6 +465,7 @@ function ProviderPill({
showResetInline={showResetInline}
resetIconSize={resetIconSize}
problemLabel={state.isProblem ? stateLabel : null}
textLabel={index === 0 && showBalance !== null}
/>
</span>
))}
Expand Down
30 changes: 30 additions & 0 deletions apps/desktop-tauri/src/proof/fixtures/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
ProviderCatalogEntry,
ProviderUsageSnapshot,
QuotaWindowHistoryBridge,
RateWindowSnapshot,
SettingsSnapshot,
SpendModelRow,
TokenAccountBridge,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1364,6 +1393,7 @@ const BUILDERS: Record<ProofScenarioName, (now: Date) => ProofScenario> = {
vercel: vercelScenario,
"vercel-team-id-invalid": vercelTeamIdInvalidScenario,
"kimi-bonus-credits": kimiBonusCreditsScenario,
"deepseek-balance": deepseekBalanceScenario,
"agent-sessions-hosts": agentSessionsHostsScenario,
"copilot-daily-target": copilotDailyTargetScenario,
};
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src/test/floatBarHarness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function snapshot(
resetDescription?: string | null;
descriptionIsDetail?: boolean;
informational?: boolean;
balanceText?: string | null;
secondary?: {
used: number;
exhausted?: boolean;
Expand Down Expand Up @@ -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,
});
}

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop-tauri/src/types/bridge/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Binary file added docs/proof/deepseek-balance/floatbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions rust/src/core/provider/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
let name = name.to_lowercase();
Expand Down Expand Up @@ -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:?}");
}
}
}
}
Loading
Loading