diff --git a/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts b/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts index 5e94ad52ece..2197b25f3f0 100644 --- a/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts +++ b/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts @@ -32,7 +32,7 @@ type UsageProviderSnapshots = { type UsageProviderId = ProviderRateLimits['provider'] -function hasUsageData(provider: ProviderRateLimits): boolean { +export function hasUsageData(provider: ProviderRateLimits): boolean { return Boolean( provider.session || provider.weekly || diff --git a/src/renderer/src/components/status-bar/usage-rate-limits-source.test.ts b/src/renderer/src/components/status-bar/usage-rate-limits-source.test.ts index 6acd3754540..9c1db3b578d 100644 --- a/src/renderer/src/components/status-bar/usage-rate-limits-source.test.ts +++ b/src/renderer/src/components/status-bar/usage-rate-limits-source.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it, vi } from 'vitest' import type { ProviderRateLimits, RateLimitState } from '../../../../shared/rate-limit-types' +import { getVisibleUsageProvider, isUsageEmptyState } from './status-bar-provider-visibility' import { latestUsageUpdatedAt, resolveStatusBarUsageRateLimits } from './usage-rate-limits-source' vi.mock('@/i18n/i18n', () => ({ @@ -135,6 +136,85 @@ describe('resolveStatusBarUsageRateLimits', () => { expect(provider?.weekly).toBeNull() } }) + + it('does not invent bars for providers the viewer never set up (#15804)', () => { + const local = localState() + local.minimax = null + local.opencodeGo = { + provider: 'opencode-go', + session: null, + weekly: null, + monthly: null, + updatedAt: 2_000, + error: 'Session cookie not configured', + status: 'unavailable' + } + local.minimaxCookieConfigured = false + + const resolved = resolveStatusBarUsageRateLimits(local, { + kind: 'remote-unreachable', + ownerLabel: 'Mac Mini' + }) + const settings = { + antigravityUsageConfigured: false, + minimaxCookieConfigured: resolved.minimaxCookieConfigured, + grokAuthConfigured: resolved.grokAuthConfigured + } + + // Why: an 'error' snapshot reads as "configured" to the visibility gate, so + // stamping unconfigured providers pins bars the user never enabled. + expect(resolved.minimax).toBeNull() + expect(resolved.opencodeGo?.status).toBe('unavailable') + expect(getVisibleUsageProvider('minimax', resolved.minimax, settings)).toBeNull() + expect(getVisibleUsageProvider('opencode-go', resolved.opencodeGo, settings)).toBeNull() + // Grok is configured locally, so its bar survives carrying the honest verdict. + expect(getVisibleUsageProvider('grok', resolved.grok, settings)?.error).toBe( + 'Usage unavailable — cannot reach Mac Mini' + ) + }) + + it('leaves the usage setup CTA reachable when nothing is configured (#15804)', () => { + const unconfigured = localState() + for (const key of PROVIDER_KEYS) { + unconfigured[key] = { + provider: key === 'opencodeGo' ? 'opencode-go' : key, + session: null, + weekly: null, + updatedAt: 2_000, + error: 'Not signed in', + status: 'unavailable' + } + } + unconfigured.minimaxCookieConfigured = false + unconfigured.grokAuthConfigured = false + + const resolved = resolveStatusBarUsageRateLimits(unconfigured, { + kind: 'remote-unreachable', + ownerLabel: 'Mac Mini' + }) + + // Why: synthesized 'error' snapshots count as configured providers, which + // would silently swallow the "connect an account" empty state. + expect( + isUsageEmptyState( + { + claude: resolved.claude, + codex: resolved.codex, + gemini: resolved.gemini, + opencodeGo: resolved.opencodeGo, + kimi: resolved.kimi, + antigravity: resolved.antigravity, + minimax: resolved.minimax, + grok: resolved.grok + }, + { + antigravityUsageConfigured: false, + minimaxCookieConfigured: false, + grokAuthConfigured: false + } + ) + ).toBe(true) + }) }) describe('latestUsageUpdatedAt', () => { diff --git a/src/renderer/src/components/status-bar/usage-rate-limits-source.ts b/src/renderer/src/components/status-bar/usage-rate-limits-source.ts index 77e1036fc12..be934513f66 100644 --- a/src/renderer/src/components/status-bar/usage-rate-limits-source.ts +++ b/src/renderer/src/components/status-bar/usage-rate-limits-source.ts @@ -1,6 +1,10 @@ import type { ProviderRateLimits, RateLimitState } from '../../../../shared/rate-limit-types' import { translate } from '@/i18n/i18n' -import { createPendingProviderSnapshot } from './status-bar-provider-visibility' +import { + createPendingProviderSnapshot, + hasUsageData, + isProviderConfigured +} from './status-bar-provider-visibility' /** * Which machine's usage the status-bar badges describe (#15798). @@ -71,6 +75,32 @@ function createUnreachableProviderSnapshot( } } +/** + * Blank every bar the unreachable owner can no longer vouch for, without + * inventing bars it never had (#15804). + * + * Why the gate: a locally blank + unconfigured provider has no numbers to leak + * and is no evidence the *server* has it set up. Stamping it 'error' reads as + * "configured" to `isProviderConfigured`, which would pin MiniMax/OpenCode Go + * bars on users who never enabled them and suppress the usage setup CTA. + */ +function markProvidersUnreachable( + local: RateLimitState, + ownerLabel: string +): Pick { + const replaced = {} as Pick + for (const key of USAGE_PROVIDER_KEYS) { + const localProvider = local[key] + const blankAndUnconfigured = + localProvider == null || + (!isProviderConfigured(localProvider) && !hasUsageData(localProvider)) + replaced[key] = blankAndUnconfigured + ? localProvider + : createUnreachableProviderSnapshot(USAGE_PROVIDER_IDS[key], ownerLabel) + } + return replaced +} + function normalizeFlag(remoteValue: boolean | undefined, localValue: boolean): boolean { return typeof remoteValue === 'boolean' ? remoteValue : localValue } @@ -105,14 +135,12 @@ export function resolveStatusBarUsageRateLimits( if (remoteUsage.kind === 'remote') { return adoptRemoteRateLimits(localRateLimits, remoteUsage.rateLimits) } - // Why: never spread the local provider here. Keeping any local window would - // render the viewer's percentages under the server's name — the exact - // "looks correct, is wrong" failure #15798 reports. + // Why: never keep a local window here. Rendering the viewer's percentages + // under the server's name is the exact "looks correct, is wrong" failure + // #15798 reports. const providers = remoteUsage.kind === 'remote-unreachable' - ? replaceProviders((providerId) => - createUnreachableProviderSnapshot(providerId, remoteUsage.ownerLabel) - ) + ? markProvidersUnreachable(localRateLimits, remoteUsage.ownerLabel) : replaceProviders(createPendingProviderSnapshot) return { ...localRateLimits, ...providers } }