diff --git a/src/main/rate-limits/antigravity-native-usage-source.test.ts b/src/main/rate-limits/antigravity-native-usage-source.test.ts new file mode 100644 index 000000000000..abae32224dd5 --- /dev/null +++ b/src/main/rate-limits/antigravity-native-usage-source.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { + ANTIGRAVITY_NO_NATIVE_SOURCE_REASON, + buildAntigravityNativeUnavailable, + fetchAntigravityRateLimits +} from './antigravity-native-usage-source' + +describe('antigravity native usage source', () => { + it('reports Antigravity-native unavailability rather than any other provider quota', async () => { + const antigravity = await fetchAntigravityRateLimits() + + expect(antigravity.provider).toBe('antigravity') + expect(antigravity.status).toBe('unavailable') + expect(antigravity.error).toBe(ANTIGRAVITY_NO_NATIVE_SOURCE_REASON) + expect(antigravity.session).toBeNull() + expect(antigravity.weekly).toBeNull() + expect(antigravity.buckets).toBeUndefined() + }) + + it('never blames Gemini for an Antigravity unavailability', async () => { + const antigravity = await fetchAntigravityRateLimits() + + expect(antigravity.error).not.toMatch(/gemini/i) + expect(antigravity.error).not.toMatch(/code assist/i) + }) + + it('takes no account or host input, so two accounts cannot share state', async () => { + // Why: the source reads nothing account-scoped; identical output is the isolation proof + // until a native per-account probe exists. + const first = buildAntigravityNativeUnavailable(1_700_000_000_000) + const second = buildAntigravityNativeUnavailable(1_700_000_000_000) + + expect(first).toEqual(second) + expect(fetchAntigravityRateLimits.length).toBe(0) + }) + + it('stamps the snapshot so activation freshness checks can age it out', () => { + expect(buildAntigravityNativeUnavailable(1_700_000_000_000).updatedAt).toBe(1_700_000_000_000) + }) +}) diff --git a/src/main/rate-limits/antigravity-native-usage-source.ts b/src/main/rate-limits/antigravity-native-usage-source.ts new file mode 100644 index 000000000000..b115702ee4f3 --- /dev/null +++ b/src/main/rate-limits/antigravity-native-usage-source.ts @@ -0,0 +1,27 @@ +import type { ProviderRateLimits } from '../../shared/rate-limit-types' + +// Why: Antigravity has no quota source Orca can read. `agy` keeps its token in the OS keyring +// and its subcommands (agent, changelog, install, mcp, mic-serve, models, plugin, +// remote-control, update) expose no usage/quota output. Deriving this provider from Gemini's +// Code Assist quota published a different provider's pools under the Antigravity label +// (#14515) and made Antigravity usage depend on a Gemini CLI install (#9122), so this source +// reports native unavailability instead of substituting another provider. +export const ANTIGRAVITY_NO_NATIVE_SOURCE_REASON = + 'Antigravity usage is not available. Orca cannot read the Antigravity account quota pools yet.' + +export function buildAntigravityNativeUnavailable(now: number = Date.now()): ProviderRateLimits { + return { + provider: 'antigravity', + session: null, + weekly: null, + updatedAt: now, + error: ANTIGRAVITY_NO_NATIVE_SOURCE_REASON, + status: 'unavailable' + } +} + +// Why: async and argument-free like every other provider fetcher, so a real native probe can +// replace the body without moving the call site back into a derivation of another provider. +export async function fetchAntigravityRateLimits(): Promise { + return buildAntigravityNativeUnavailable() +} diff --git a/src/main/rate-limits/antigravity-usage-mirror.test.ts b/src/main/rate-limits/antigravity-usage-mirror.test.ts deleted file mode 100644 index 84c92e1ad22f..000000000000 --- a/src/main/rate-limits/antigravity-usage-mirror.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { ProviderRateLimits, ProviderRateLimitStatus } from '../../shared/rate-limit-types' -import { deriveAntigravityRateLimits } from './antigravity-usage-mirror' - -function geminiSnapshot( - status: ProviderRateLimitStatus, - error: string | null, - usedPercent: number | null = null -): ProviderRateLimits { - return { - provider: 'gemini', - session: - usedPercent === null - ? null - : { usedPercent, windowMinutes: 300, resetsAt: null, resetDescription: null }, - weekly: null, - updatedAt: 1_700_000_000_000, - error, - status - } -} - -describe('deriveAntigravityRateLimits', () => { - it('mirrors a successful Gemini read as shared Code Assist quota', () => { - const antigravity = deriveAntigravityRateLimits(geminiSnapshot('ok', null, 42)) - - expect(antigravity.provider).toBe('antigravity') - expect(antigravity.status).toBe('ok') - expect(antigravity.session?.usedPercent).toBe(42) - expect(antigravity.error).toBeNull() - }) - - it('reports unavailable without quoting the Gemini failure', () => { - const antigravity = deriveAntigravityRateLimits( - geminiSnapshot('error', 'Gemini project ID not found') - ) - - expect(antigravity.provider).toBe('antigravity') - expect(antigravity.status).toBe('unavailable') - expect(antigravity.error).not.toContain('Gemini project ID not found') - expect(antigravity.error).toContain('Antigravity usage is not available') - expect(antigravity.session).toBeNull() - expect(antigravity.weekly).toBeNull() - }) - - it('does not blame a missing sign-in when the quota read itself failed', () => { - const antigravity = deriveAntigravityRateLimits(geminiSnapshot('error', 'Token refresh failed')) - - // Why: the reported symptom is a connected sign-in whose Code Assist read failed. - expect(antigravity.error).toContain('could not be read right now') - expect(antigravity.error).not.toContain('sign-in is connected') - }) - - it('keeps the Gemini timestamp so activation freshness checks are not forced to refetch', () => { - const antigravity = deriveAntigravityRateLimits(geminiSnapshot('error', 'Token refresh failed')) - - expect(antigravity.updatedAt).toBe(1_700_000_000_000) - }) - - it('points at the missing sign-in when the Gemini opt-in is off', () => { - const antigravity = deriveAntigravityRateLimits( - geminiSnapshot('unavailable', 'Gemini CLI OAuth is disabled in settings') - ) - - expect(antigravity.status).toBe('unavailable') - expect(antigravity.error).not.toContain('Gemini CLI OAuth is disabled in settings') - expect(antigravity.error).toContain('Antigravity usage is not available') - expect(antigravity.error).toContain('Gemini CLI sign-in is connected') - }) -}) diff --git a/src/main/rate-limits/antigravity-usage-mirror.ts b/src/main/rate-limits/antigravity-usage-mirror.ts deleted file mode 100644 index 3cea6decc111..000000000000 --- a/src/main/rate-limits/antigravity-usage-mirror.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ProviderRateLimits } from '../../shared/rate-limit-types' - -// Why: the Antigravity CLI keeps its token in the OS keyring, not in the files the Gemini -// fetcher reads, so Orca never actually queries Antigravity. Only a *successful* Gemini read -// describes shared Google Code Assist quota; republishing a Gemini failure under the -// Antigravity provider id surfaced "Refresh failed" for a request that was never attempted. -const ANTIGRAVITY_NO_SIGN_IN_REASON = - 'Antigravity usage is not available. Orca can only show shared Google Code Assist quota while a Gemini CLI sign-in is connected.' -// Why: a Gemini `error` means the sign-in exists and the quota read failed, so blaming a missing sign-in would misdirect the user. -const ANTIGRAVITY_QUOTA_UNREADABLE_REASON = - 'Antigravity usage is not available. Orca reads it from the shared Google Code Assist quota, which could not be read right now.' - -export function deriveAntigravityRateLimits(gemini: ProviderRateLimits): ProviderRateLimits { - if (gemini.status === 'ok') { - return { ...gemini, provider: 'antigravity' } - } - return { - provider: 'antigravity', - session: null, - weekly: null, - // Why: reuse the Gemini timestamp so activation freshness checks don't force a refetch every cycle. - updatedAt: gemini.updatedAt, - error: - gemini.status === 'unavailable' - ? ANTIGRAVITY_NO_SIGN_IN_REASON - : ANTIGRAVITY_QUOTA_UNREADABLE_REASON, - status: 'unavailable' - } -} diff --git a/src/main/rate-limits/service-antigravity-usage.test.ts b/src/main/rate-limits/service-antigravity-usage.test.ts index e0975af0db07..07083d0c604b 100644 --- a/src/main/rate-limits/service-antigravity-usage.test.ts +++ b/src/main/rate-limits/service-antigravity-usage.test.ts @@ -1,12 +1,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { RateLimitService } from './service' +import { ANTIGRAVITY_NO_NATIVE_SOURCE_REASON } from './antigravity-native-usage-source' import { fetchClaudeRateLimits } from './claude-fetcher' import { fetchCodexRateLimits } from './codex-fetcher' import { fetchGeminiRateLimits } from './gemini-usage-fetcher' import { errorProvider, okProvider, - resetRateLimitProviderMocks + resetRateLimitProviderMocks, + unavailableProvider } from './rate-limit-service-test-harness' vi.mock('./claude-fetcher', () => ({ @@ -54,7 +56,7 @@ describe('RateLimitService Antigravity usage', () => { vi.mocked(fetchCodexRateLimits).mockResolvedValue(okProvider('codex', 20)) }) - it('does not republish a Gemini failure as an Antigravity refresh failure', async () => { + it('does not republish a Gemini failure as an Antigravity failure', async () => { vi.mocked(fetchGeminiRateLimits).mockResolvedValue( errorProvider('gemini', 'Gemini project ID not found') ) @@ -71,16 +73,47 @@ describe('RateLimitService Antigravity usage', () => { expect(state.gemini?.error).toBe('Gemini project ID not found') }) - it('keeps mirroring a successful Gemini read under the Antigravity provider', async () => { - vi.mocked(fetchGeminiRateLimits).mockResolvedValue(okProvider('gemini', 42, Date.now())) + it('never surfaces successful Gemini quota under the Antigravity label (#14515)', async () => { + vi.mocked(fetchGeminiRateLimits).mockResolvedValue({ + ...okProvider('gemini', 42, Date.now()), + buckets: [ + { + name: 'Gemini 3 Pro', + usedPercent: 42, + windowMinutes: 300, + resetsAt: null, + resetDescription: null + } + ] + }) const service = new RateLimitService() await service.refresh() const state = service.getState() - expect(state.antigravity?.status).toBe('ok') - expect(state.antigravity?.provider).toBe('antigravity') - expect(state.antigravity?.session?.usedPercent).toBe(42) + expect(state.antigravity?.status).toBe('unavailable') + expect(state.antigravity?.session).toBeNull() + expect(state.antigravity?.buckets).toBeUndefined() + expect(state.antigravity?.error).toBe(ANTIGRAVITY_NO_NATIVE_SOURCE_REASON) + // Why: Gemini's own snapshot is untouched; only the impersonation is gone. + expect(state.gemini?.session?.usedPercent).toBe(42) + }) + + it('reports native unavailability when no Gemini CLI sign-in exists at all (#9122)', async () => { + // Why: "no Gemini binary" is the #9122 repro; Antigravity must not inherit that failure + // or name Gemini in its own reason. + vi.mocked(fetchGeminiRateLimits).mockResolvedValue( + unavailableProvider('gemini', 'Gemini CLI credentials not found') + ) + const service = new RateLimitService() + + await service.refresh() + + const antigravity = service.getState().antigravity + expect(antigravity?.provider).toBe('antigravity') + expect(antigravity?.status).toBe('unavailable') + expect(antigravity?.error).not.toMatch(/gemini/i) + expect(antigravity?.error).toBe(ANTIGRAVITY_NO_NATIVE_SOURCE_REASON) }) it('never leaves a cached Antigravity snapshot in the error retry lane', async () => { @@ -93,7 +126,6 @@ describe('RateLimitService Antigravity usage', () => { ) await service.refresh() - // Why: stale-retention would otherwise show Gemini numbers as "Refresh failed" Antigravity usage. expect(service.getState().antigravity?.status).toBe('unavailable') expect(service.getState().antigravity?.session).toBeNull() }) diff --git a/src/main/rate-limits/service/service-full-cycle-application.ts b/src/main/rate-limits/service/service-full-cycle-application.ts index 7104face38fa..43de5cf54846 100644 --- a/src/main/rate-limits/service/service-full-cycle-application.ts +++ b/src/main/rate-limits/service/service-full-cycle-application.ts @@ -1,5 +1,5 @@ import { RateLimitServiceFullCyclePreparation } from './service-full-cycle-preparation' -import { deriveAntigravityRateLimits } from '../antigravity-usage-mirror' +import { buildAntigravityNativeUnavailable } from '../antigravity-native-usage-source' import type { ProviderRateLimits } from './service-types' export abstract class RateLimitServiceFullCycleApplication extends RateLimitServiceFullCyclePreparation { @@ -32,7 +32,8 @@ export abstract class RateLimitServiceFullCycleApplication extends RateLimitServ geminiResult, opencodeGoResult, kimiResult, - miniMaxResult + miniMaxResult, + antigravityResult ], grokResultPromise } = prepared @@ -79,8 +80,12 @@ export abstract class RateLimitServiceFullCycleApplication extends RateLimitServ status: 'error' } satisfies ProviderRateLimits) - // Why: Antigravity can only borrow a *successful* Gemini read; a Gemini failure is not an Antigravity failure. - const antigravity = deriveAntigravityRateLimits(gemini) + // Why: Antigravity owns its own source; a rejected probe must still read as Antigravity-native + // unavailability, never another provider's quota or another provider's failure (#14515, #9122). + const antigravity = + antigravityResult.status === 'fulfilled' + ? antigravityResult.value + : buildAntigravityNativeUnavailable() const opencodeGo = opencodeGoResult.status === 'fulfilled' diff --git a/src/main/rate-limits/service/service-full-cycle-preparation.ts b/src/main/rate-limits/service/service-full-cycle-preparation.ts index 7551652bca1d..d234a617e32b 100644 --- a/src/main/rate-limits/service/service-full-cycle-preparation.ts +++ b/src/main/rate-limits/service/service-full-cycle-preparation.ts @@ -1,3 +1,4 @@ +import { fetchAntigravityRateLimits } from '../antigravity-native-usage-source' import { fetchClaudeRateLimits } from '../claude-fetcher' import { fetchCodexRateLimits } from '../codex-fetcher' import { fetchGeminiRateLimits } from '../gemini-usage-fetcher' @@ -36,6 +37,7 @@ export type FetchAllCyclePrepared = { PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, + PromiseSettledResult, PromiseSettledResult ] grokResultPromise: Promise< @@ -138,42 +140,42 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ const claudeFetchGated = !options?.force && this.shouldSkipAutomatedClaudeFetch(previousState.claude) - const [claudeResult, codexResult, geminiResult, opencodeGoResult, kimiResult, miniMaxResult] = - await Promise.allSettled([ - claudeFetchGated - ? Promise.resolve(previousState.claude as ProviderRateLimits) - : fetchClaudeRateLimits({ - authPreparation: claudeAuthPreparation, - allowPtyFallback: this.shouldAllowClaudePtyFallback(claudeAuthPreparation), - allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), - networkProxySettings: this.networkProxySettingsResolver?.(), - signal - }), - codexFetchGated - ? Promise.resolve(previousState.codex as ProviderRateLimits) - : (missingWslCodexHome ?? - fetchCodexRateLimits({ - codexHomePath, - allowPtyFallback: this.shouldAllowCodexPtyFallback(), - signal - })), - fetchGeminiRateLimits(geminiCliOAuthEnabled), - fetchOpenCodeGoRateLimits( - cookie, - workspaceIdOverride || undefined, - this.networkProxySettingsResolver?.() - ), - this.fetchKimiWithResolvedHome(), - miniMaxConfigResult.error - ? Promise.resolve(this.getMiniMaxCredentialError(miniMaxConfigResult.error)) - : fetchMiniMaxRateLimits({ - cookie: miniMaxCookie, - groupId: miniMaxGroupId, - models: miniMaxModels, - endpointMode: miniMaxEndpoint, - apiKey: miniMaxApiKey - }) - ]) + const results = await Promise.allSettled([ + claudeFetchGated + ? Promise.resolve(previousState.claude as ProviderRateLimits) + : fetchClaudeRateLimits({ + authPreparation: claudeAuthPreparation, + allowPtyFallback: this.shouldAllowClaudePtyFallback(claudeAuthPreparation), + allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), + networkProxySettings: this.networkProxySettingsResolver?.(), + signal + }), + codexFetchGated + ? Promise.resolve(previousState.codex as ProviderRateLimits) + : (missingWslCodexHome ?? + fetchCodexRateLimits({ + codexHomePath, + allowPtyFallback: this.shouldAllowCodexPtyFallback(), + signal + })), + fetchGeminiRateLimits(geminiCliOAuthEnabled), + fetchOpenCodeGoRateLimits( + cookie, + workspaceIdOverride || undefined, + this.networkProxySettingsResolver?.() + ), + this.fetchKimiWithResolvedHome(), + miniMaxConfigResult.error + ? Promise.resolve(this.getMiniMaxCredentialError(miniMaxConfigResult.error)) + : fetchMiniMaxRateLimits({ + cookie: miniMaxCookie, + groupId: miniMaxGroupId, + models: miniMaxModels, + endpointMode: miniMaxEndpoint, + apiKey: miniMaxApiKey + }), + fetchAntigravityRateLimits() + ]) if (signal.aborted) { return null @@ -194,14 +196,7 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ miniMaxConfigChanged, miniMaxGeneration, claudeFetchGated, - results: [ - claudeResult, - codexResult, - geminiResult, - opencodeGoResult, - kimiResult, - miniMaxResult - ], + results, grokResultPromise } } diff --git a/src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts b/src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts index ab2a0eca23f1..4f2460a91b89 100644 --- a/src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts +++ b/src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts @@ -163,21 +163,20 @@ describe('hasUsageProviderSettingsForProvider', () => { expect(hasUsageProviderSettingsForProvider('grok', usageSettings())).toBe(false) }) - it('requires both a checked Antigravity item and Gemini OAuth as the durable Antigravity signal', () => { + it('treats a checked Antigravity item as durable on its own, with no Gemini opt-in', () => { expect( hasUsageProviderSettingsForProvider( 'antigravity', - usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: true }) + usageSettings({ antigravityUsageConfigured: true }) ) ).toBe(true) - // Why: the snapshot mirrors the Gemini fetch — without the OAuth opt-in it - // is permanently unavailable, so the checked item alone is not durable. + // Why: an unrelated provider's opt-in must never decide whether Antigravity's slot exists (#14515). expect( hasUsageProviderSettingsForProvider( 'antigravity', - usageSettings({ antigravityUsageConfigured: true }) + usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: true }) ) - ).toBe(false) + ).toBe(true) expect( hasUsageProviderSettingsForProvider( 'antigravity', @@ -345,11 +344,11 @@ describe('getVisibleUsageProvider', () => { ).toBe(null) }) - it('keeps Antigravity visible while the snapshot is pending when checked and Gemini OAuth is on', () => { + it('keeps Antigravity visible while the snapshot is pending when its item is checked', () => { const visible = getVisibleUsageProvider( 'antigravity', null, - usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: true }) + usageSettings({ antigravityUsageConfigured: true }) ) expect(visible).toMatchObject({ provider: 'antigravity', @@ -359,26 +358,21 @@ describe('getVisibleUsageProvider', () => { }) }) - it('hides Antigravity while Gemini OAuth is off even when its status item is checked', () => { - // Why: without the OAuth opt-in the mirrored snapshot is permanently - // 'unavailable'; the default-on item must not pin a dead bar. - expect( - getVisibleUsageProvider( - 'antigravity', - null, - usageSettings({ antigravityUsageConfigured: true }) - ) - ).toBe(null) + it('surfaces the unavailable Antigravity slot instead of hiding it or borrowing Gemini', () => { + // Why: the slot is how Orca says "no readable Antigravity quota yet"; hiding it + // leaves #9122's reporter with no explanation at all. expect( getVisibleUsageProvider( 'antigravity', provider('unavailable', { provider: 'antigravity', - error: 'Gemini CLI OAuth is disabled in settings' + error: 'Antigravity usage is not available.' }), usageSettings({ antigravityUsageConfigured: true }) ) - ).toBe(null) + ).toMatchObject({ provider: 'antigravity', status: 'unavailable' }) + // Why: an unchecked or undetected CLI still hides it entirely. + expect(getVisibleUsageProvider('antigravity', null, usageSettings())).toBe(null) }) }) @@ -487,13 +481,13 @@ describe('isUsageEmptyState', () => { grok: provider('unavailable', { provider: 'grok' }), minimax: provider('unavailable', { provider: 'minimax' }) }, - usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: true }) + usageSettings({ antigravityUsageConfigured: true }) ) ).toBe(false) }) - it('still shows the setup CTA when Antigravity is checked but Gemini OAuth is off', () => { - // Why: the default-on Antigravity item is not configured usage on its own; + it('still shows the setup CTA once the Antigravity slot settles as unavailable', () => { + // Why: an Antigravity slot with no readable quota is not configured usage; // it must not hide the teaching CTA from users who set nothing up. expect( isUsageEmptyState( @@ -503,7 +497,7 @@ describe('isUsageEmptyState', () => { gemini: provider('unavailable'), opencodeGo: provider('unavailable', { provider: 'opencode-go' }), kimi: provider('unavailable', { provider: 'kimi' }), - antigravity: null, + antigravity: provider('unavailable', { provider: 'antigravity' }), grok: provider('unavailable', { provider: 'grok' }), minimax: provider('unavailable', { provider: 'minimax' }) }, 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 19258592f1fd..8e07b4ad8b61 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 @@ -10,9 +10,7 @@ export type UsageProviderSettings = Pick< > & { // Why: Antigravity has no separate persisted usage credential in Orca. The // checked status-bar item is the durable user signal; StatusBar only sets - // this after PATH detection says the agent is available. Durability further - // requires geminiCliOAuthEnabled — the snapshot mirrors the Gemini fetch, - // which never yields data while that opt-in is off. + // this after PATH detection says the agent is available. antigravityUsageConfigured: boolean // Why: MiniMax/Grok sign-in live on disk, not in settings; main sets these each poll. minimaxCookieConfigured: boolean @@ -75,8 +73,9 @@ export function hasUsageProviderSettings( (settings?.claudeManagedAccounts?.length ?? 0) > 0 || settings?.geminiCliOAuthEnabled === true || Boolean(settings?.opencodeSessionCookie?.trim()) || - // Antigravity's durable signal requires geminiCliOAuthEnabled, so it is - // already covered by the gemini term above. + // Why: Antigravity is deliberately absent — Orca has no readable Antigravity + // quota source yet (#14515), so it can never stand in for a configured provider + // and must not suppress the setup CTA for a user who set nothing up. settings?.minimaxCookieConfigured === true || settings?.minimaxApiKeyConfigured === true || settings?.grokAuthConfigured === true @@ -103,10 +102,11 @@ export function hasUsageProviderSettingsForProvider( return Boolean(settings.opencodeSessionCookie?.trim()) } if (providerId === 'antigravity') { - // Why: the Antigravity snapshot mirrors the Gemini fetch, which stays - // 'unavailable' until the user opts into Gemini CLI OAuth. Without that - // gate the default-on checked item would pin a permanently dead bar. - return settings.antigravityUsageConfigured === true && settings.geminiCliOAuthEnabled === true + // Why: Antigravity's slot is earned by Antigravity signals alone — a checked + // item plus the CLI on PATH. It has no readable quota source yet (#14515), so + // the slot renders an explicit unavailable state rather than another + // provider's numbers. + return settings.antigravityUsageConfigured === true } if (providerId === 'minimax') { return settings.minimaxCookieConfigured === true || settings.minimaxApiKeyConfigured === true diff --git a/src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts b/src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts index cbcfcda04929..d669cf1008ef 100644 --- a/src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts +++ b/src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts @@ -6,7 +6,6 @@ describe('getUsageProviderAccountsSectionId', () => { expect(getUsageProviderAccountsSectionId('claude')).toBe('accounts-claude') expect(getUsageProviderAccountsSectionId('codex')).toBe('accounts-codex') expect(getUsageProviderAccountsSectionId('gemini')).toBe('accounts-gemini') - expect(getUsageProviderAccountsSectionId('antigravity')).toBe('accounts-gemini') expect(getUsageProviderAccountsSectionId('opencode-go')).toBe('accounts-opencode-go') expect(getUsageProviderAccountsSectionId('minimax')).toBe('accounts-minimax') expect(getUsageProviderAccountsSectionId('grok')).toBe('accounts-grok') @@ -15,4 +14,10 @@ describe('getUsageProviderAccountsSectionId', () => { it('does not invent an Accounts section for CLI-owned Kimi credentials', () => { expect(getUsageProviderAccountsSectionId('kimi')).toBeNull() }) + + it("does not route Antigravity into another provider's Accounts section", () => { + // Why: `accounts-gemini` configures Gemini, not Antigravity — a sign-in CTA there + // sends the user to the wrong provider (#14515). + expect(getUsageProviderAccountsSectionId('antigravity')).toBeNull() + }) }) diff --git a/src/renderer/src/components/status-bar/usage-provider-settings-target.ts b/src/renderer/src/components/status-bar/usage-provider-settings-target.ts index 59ea71042490..4adc99fd3d51 100644 --- a/src/renderer/src/components/status-bar/usage-provider-settings-target.ts +++ b/src/renderer/src/components/status-bar/usage-provider-settings-target.ts @@ -9,9 +9,11 @@ export function getUsageProviderAccountsSectionId( case 'codex': return 'accounts-codex' case 'gemini': - case 'antigravity': - // Why: Antigravity usage currently shares Gemini's OAuth configuration. return 'accounts-gemini' + case 'antigravity': + // Why: Antigravity sign-in lives in the OS keyring under the `agy` CLI; Orca has no + // Accounts section that configures it, and Gemini's would configure the wrong provider. + return null case 'opencode-go': return 'accounts-opencode-go' case 'minimax':