diff --git a/src/main/rate-limits/service.ts b/src/main/rate-limits/service.ts index cbfd8289b..5c3243c87 100644 --- a/src/main/rate-limits/service.ts +++ b/src/main/rate-limits/service.ts @@ -79,6 +79,7 @@ type InternalRateLimitState = { gemini: ProviderRateLimits | null opencodeGo: ProviderRateLimits | null kimi: ProviderRateLimits | null + antigravity: ProviderRateLimits | null minimax: ProviderRateLimits | null grok: ProviderRateLimits | null } @@ -113,6 +114,7 @@ export class RateLimitService { gemini: null, opencodeGo: null, kimi: null, + antigravity: null, minimax: null, grok: null } @@ -1126,7 +1128,7 @@ export class RateLimitService { private withFetchingStatus( current: ProviderRateLimits | null, - provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok' + provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok' | 'antigravity' ): ProviderRateLimits { if (!current) { return { @@ -1200,6 +1202,7 @@ export class RateLimitService { ? this.withFetchingStatus(null, 'opencode-go') : this.withFetchingStatus(previousState.opencodeGo, 'opencode-go'), kimi: this.withFetchingStatus(previousState.kimi, 'kimi'), + antigravity: this.withFetchingStatus(previousState.antigravity, 'antigravity'), minimax: miniMaxConfigChanged ? this.withFetchingStatus(null, 'minimax') : this.withFetchingStatus(previousState.minimax, 'minimax'), @@ -1287,6 +1290,14 @@ export class RateLimitService { status: 'error' } satisfies ProviderRateLimits) + // Why: Antigravity shares Google/Gemini usage credentials today; mirror the + // Gemini snapshot under provider 'antigravity' so status-bar UI that checks + // antigravity state receives a real fetch lifecycle instead of staying null. + const antigravity: ProviderRateLimits = { + ...gemini, + provider: 'antigravity' + } + const opencodeGo = opencodeGoResult.status === 'fulfilled' ? opencodeGoResult.value @@ -1365,6 +1376,7 @@ export class RateLimitService { : this.applyStalePolicy(opencodeGo, previousState.opencodeGo) : this.state.opencodeGo, kimi: this.applyStalePolicy(kimi, previousState.kimi), + antigravity: this.applyStalePolicy(antigravity, previousState.antigravity), minimax: shouldApplyMiniMax ? miniMaxConfigChanged ? miniMax diff --git a/src/main/runtime/rpc/methods/client-ui.test.ts b/src/main/runtime/rpc/methods/client-ui.test.ts index 2af71e3ab..01a3f784e 100644 --- a/src/main/runtime/rpc/methods/client-ui.test.ts +++ b/src/main/runtime/rpc/methods/client-ui.test.ts @@ -182,11 +182,12 @@ describe('client UI RPC methods', () => { ...getDefaultUIState(), worktreeCardProperties: ['status', 'branch', 'automation', 'inline-agents'], _worktreeCardModeDefaulted: true, - statusBarItems: ['codex', 'kimi', 'minimax', 'grok', 'ports'], + statusBarItems: ['codex', 'kimi', 'minimax', 'grok', 'antigravity', 'ports'], _portsStatusBarDefaultAdded: true, _kimiStatusBarDefaultAdded: true, _minimaxStatusBarDefaultAdded: true, _grokStatusBarDefaultAdded: true, + _antigravityStatusBarDefaultAdded: true, taskResumeState: { githubMode: 'items', githubItemsQuery: 'is:open', @@ -222,11 +223,12 @@ describe('client UI RPC methods', () => { const payload = { worktreeCardProperties: ['status', 'branch', 'automation', 'inline-agents'], _worktreeCardModeDefaulted: true, - statusBarItems: ['codex', 'kimi', 'minimax', 'grok', 'ports'], + statusBarItems: ['codex', 'kimi', 'minimax', 'grok', 'antigravity', 'ports'], _portsStatusBarDefaultAdded: true, _kimiStatusBarDefaultAdded: true, _minimaxStatusBarDefaultAdded: true, _grokStatusBarDefaultAdded: true, + _antigravityStatusBarDefaultAdded: true, taskResumeState: { githubMode: 'items', githubItemsQuery: 'is:open', diff --git a/src/main/runtime/rpc/methods/client-ui.ts b/src/main/runtime/rpc/methods/client-ui.ts index 995959228..9b08e6dec 100644 --- a/src/main/runtime/rpc/methods/client-ui.ts +++ b/src/main/runtime/rpc/methods/client-ui.ts @@ -44,6 +44,7 @@ const StatusBarItem = z.enum([ 'claude', 'codex', 'gemini', + 'antigravity', 'opencode-go', 'kimi', 'minimax', @@ -204,6 +205,7 @@ const UiUpdate = z _portsStatusBarDefaultAdded: z.boolean().optional(), _kimiStatusBarDefaultAdded: z.boolean().optional(), _minimaxStatusBarDefaultAdded: z.boolean().optional(), + _antigravityStatusBarDefaultAdded: z.boolean().optional(), _grokStatusBarDefaultAdded: z.boolean().optional(), statusBarVisible: z.boolean().optional(), dismissedUpdateVersion: NullableString.optional(), diff --git a/src/renderer/src/components/settings/AppearancePane.test.tsx b/src/renderer/src/components/settings/AppearancePane.test.tsx index 5fe949cea..127df935f 100644 --- a/src/renderer/src/components/settings/AppearancePane.test.tsx +++ b/src/renderer/src/components/settings/AppearancePane.test.tsx @@ -412,6 +412,31 @@ describe('AppearancePane', () => { expect(mocks.state.toggleStatusBarItem).toHaveBeenCalledWith('minimax') }) + it('records Antigravity status bar toggles as usage tracking interactions', async () => { + mocks.state.availableStatusBarToggles = [ + { + id: 'antigravity', + title: 'Antigravity Usage', + description: 'Show Antigravity subscription usage in the status bar.', + toggleDescription: 'Show Antigravity subscription usage for the active workspace.', + keywords: ['status bar', 'antigravity', 'usage'] + } + ] + mocks.state.settingsSearchQuery = 'antigravity' + const container = await renderAppearancePane(getDefaultSettings('/tmp')) + const antigravitySwitch = container.querySelector( + 'button[role="switch"][aria-label="Antigravity Usage"]' + ) + + expect(antigravitySwitch).not.toBeNull() + await act(async () => { + antigravitySwitch?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(mocks.state.recordFeatureInteraction).toHaveBeenCalledWith('usage-tracking') + expect(mocks.state.toggleStatusBarItem).toHaveBeenCalledWith('antigravity') + }) + it('collapses sibling sections so only the Interface section is expanded by default', async () => { mocks.state.settingsSearchQuery = '' const container = await renderAppearancePane(getDefaultSettings('/tmp')) diff --git a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx index 97d53297e..1690832ee 100644 --- a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx +++ b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx @@ -43,6 +43,7 @@ function recordStatusBarToggleInteraction( id === 'gemini' || id === 'opencode-go' || id === 'kimi' || + id === 'antigravity' || id === 'minimax' || id === 'grok' ) { diff --git a/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts b/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts new file mode 100644 index 000000000..68c6513bb --- /dev/null +++ b/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts @@ -0,0 +1,43 @@ +import type { StatusBarItem } from '../../../../shared/types' +import { translate } from '@/i18n/i18n' +import { translateSearchKeyword } from './settings-search-keywords' + +export function getAntigravityStatusBarToggleSearchEntry(): { + id: StatusBarItem + title: string + description: string + keywords: string[] + toggleDescription: string +} { + return { + id: 'antigravity', + title: translate( + 'auto.components.settings.appearance.search.antigravityUsageTitle', + 'Antigravity Usage' + ), + description: translate( + 'auto.components.settings.appearance.search.antigravityUsageDescription', + 'Show Antigravity subscription usage in the status bar.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.896eb53fd4', + 'status bar' + ), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.antigravityKeyword', + 'antigravity' + ), + ...translateSearchKeyword('auto.components.settings.appearance.search.00a028f25f', 'usage'), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.de586def95', + 'subscription' + ), + ...translateSearchKeyword('auto.components.settings.appearance.search.51b0ccd6a2', 'google') + ], + toggleDescription: translate( + 'settings.appearance.statusBar.antigravityToggleDescription', + 'Show Antigravity subscription usage for the active workspace.' + ) + } +} diff --git a/src/renderer/src/components/settings/appearance-status-bar-search.test.ts b/src/renderer/src/components/settings/appearance-status-bar-search.test.ts index 18a218aca..3518bcba8 100644 --- a/src/renderer/src/components/settings/appearance-status-bar-search.test.ts +++ b/src/renderer/src/components/settings/appearance-status-bar-search.test.ts @@ -18,6 +18,19 @@ vi.mock('./settings-search-keywords', () => ({ import { getStatusBarToggles } from './appearance-status-bar-search' describe('getStatusBarToggles', () => { + it('includes Antigravity usage so Appearance can toggle the default-on status item', () => { + const antigravityToggle = getStatusBarToggles().find((entry) => entry.id === 'antigravity') + + expect(antigravityToggle).toMatchObject({ + title: 'Antigravity Usage', + description: 'Show Antigravity subscription usage in the status bar.', + toggleDescription: 'Show Antigravity subscription usage for the active workspace.' + }) + expect(antigravityToggle?.keywords).toEqual( + expect.arrayContaining(['status bar', 'antigravity', 'usage', 'subscription', 'google']) + ) + }) + it('includes MiniMax usage so Appearance can toggle the default-on status item', () => { const miniMaxToggle = getStatusBarToggles().find((entry) => entry.id === 'minimax') diff --git a/src/renderer/src/components/settings/appearance-status-bar-search.ts b/src/renderer/src/components/settings/appearance-status-bar-search.ts index 9044c084f..02403c24f 100644 --- a/src/renderer/src/components/settings/appearance-status-bar-search.ts +++ b/src/renderer/src/components/settings/appearance-status-bar-search.ts @@ -2,6 +2,7 @@ import type { StatusBarItem } from '../../../../shared/types' import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' +import { getAntigravityStatusBarToggleSearchEntry } from './appearance-status-bar-antigravity-toggle-search' import { getGrokStatusBarToggleSearchEntry } from './appearance-status-bar-grok-toggle-search' export const getStatusBarToggles = createLocalizedCatalog( @@ -99,6 +100,7 @@ export const getStatusBarToggles = createLocalizedCatalog( 'Show Gemini token and cost usage for the active workspace.' ) }, + getAntigravityStatusBarToggleSearchEntry(), { id: 'opencode-go', title: translate( diff --git a/src/renderer/src/components/stats/GrokUsagePane.test.tsx b/src/renderer/src/components/stats/GrokUsagePane.test.tsx index 6e9423631..d1ef3700f 100644 --- a/src/renderer/src/components/stats/GrokUsagePane.test.tsx +++ b/src/renderer/src/components/stats/GrokUsagePane.test.tsx @@ -22,6 +22,7 @@ const mockStoreState = { gemini: null, opencodeGo: null, kimi: null, + antigravity: null, minimax: null, grok: { provider: 'grok', diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index 3db1b663f..71cc71ffe 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -1760,11 +1760,13 @@ export function ProviderDetailsMenu({ ? 'O' : provider.provider === 'kimi' ? 'K' - : provider.provider === 'minimax' - ? 'M' - : provider.provider === 'grok' - ? 'R' - : 'X'} + : provider.provider === 'antigravity' + ? 'A' + : provider.provider === 'minimax' + ? 'M' + : provider.provider === 'grok' + ? 'R' + : 'X'} ) : ( @@ -1910,17 +1912,26 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele return null } - const { claude, codex, gemini, opencodeGo, kimi, minimax, grok } = rateLimits + const { claude, codex, gemini, opencodeGo, kimi, antigravity, minimax, grok } = rateLimits // Why: a provider earns a bar from either a usable live snapshot or durable // setup in Settings. The durable path keeps account switchers visible while // usage snapshots hydrate, fail, or temporarily report unavailable. // Detection-gating (see status-bar-agent-gating) additionally hides per-CLI // bars when the agent isn't installed on PATH. - // Why: thread the cookie durability flag from RateLimitState so the - // MiniMax bar stays visible after a reload and between snapshot refreshes. + // Why: Antigravity usage has no separate persisted credential. A checked + // status item plus detected CLI is the durable signal that the user wants + // its usage slot visible while the first snapshot is still pending. The + // visibility module additionally requires geminiCliOAuthEnabled (already in + // settings) because the snapshot mirrors the Gemini fetch. + const antigravityUsageConfigured = + statusBarItems.includes('antigravity') && + isStatusBarItemAvailable('antigravity', detectedAgentIds) + // Why: thread non-GlobalSettings durability flags from renderer/main state so + // bars stay visible after a reload and between snapshot refreshes. const usageSettings = { ...settings, + antigravityUsageConfigured, minimaxCookieConfigured: rateLimits.minimaxCookieConfigured, grokAuthConfigured: rateLimits.grokAuthConfigured } @@ -1928,6 +1939,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele const visibleCodex = getVisibleUsageProvider('codex', codex, usageSettings) const visibleGemini = getVisibleUsageProvider('gemini', gemini, usageSettings) const visibleKimi = getVisibleUsageProvider('kimi', kimi, usageSettings) + const visibleAntigravity = getVisibleUsageProvider('antigravity', antigravity, usageSettings) const visibleMiniMax = getVisibleUsageProvider('minimax', minimax, usageSettings) const visibleGrok = getVisibleUsageProvider('grok', grok, usageSettings) const showClaude = @@ -1946,6 +1958,10 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele visibleKimi !== null && statusBarItems.includes('kimi') && isStatusBarItemAvailable('kimi', detectedAgentIds) + const showAntigravity = + visibleAntigravity !== null && + statusBarItems.includes('antigravity') && + isStatusBarItemAvailable('antigravity', detectedAgentIds) // Why: MiniMax is a cookie-auth provider, not a CLI on PATH, so detection-gating // doesn't apply (same rationale as OpenCode Go below). const showMiniMax = visibleMiniMax !== null && statusBarItems.includes('minimax') @@ -1968,6 +1984,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele showGemini || showOpencodeGo || showKimi || + showAntigravity || showMiniMax || showGrok || showResourceUsage @@ -1976,7 +1993,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele // included because managed accounts are durable even when live usage // snapshots are still hydrating or unavailable after an update. const isEmptyUsageState = isUsageEmptyState( - { claude, codex, gemini, opencodeGo, kimi, minimax, grok }, + { claude, codex, gemini, opencodeGo, kimi, antigravity, minimax, grok }, usageSettings ) // Why: the teaching CTA is a one-time nudge — once the user hides it, keep it @@ -1988,6 +2005,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele gemini?.status === 'fetching' || opencodeGo?.status === 'fetching' || kimi?.status === 'fetching' || + antigravity?.status === 'fetching' || minimax?.status === 'fetching' || grok?.status === 'fetching' @@ -2045,6 +2063,17 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele )} /> )} + {showAntigravity && ( + + )} {showOpencodeGo && ( )} + {isStatusBarItemAvailable('antigravity', detectedAgentIds) && ( + { + recordFeatureInteraction('usage-tracking') + toggleStatusBarItem('antigravity') + }} + > + + {translate( + 'auto.components.status.bar.StatusBar.antigravityUsage', + 'Antigravity Usage' + )} + + )} { diff --git a/src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts b/src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts index b888a52fb..cd98315df 100644 --- a/src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts +++ b/src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts @@ -18,6 +18,7 @@ describe('isStatusBarItemAvailable', () => { expect(isStatusBarItemAvailable('claude', null)).toBe(true) expect(isStatusBarItemAvailable('codex', null)).toBe(true) expect(isStatusBarItemAvailable('gemini', null)).toBe(true) + expect(isStatusBarItemAvailable('antigravity', null)).toBe(true) expect(isStatusBarItemAvailable('grok', null)).toBe(true) }) @@ -25,6 +26,7 @@ describe('isStatusBarItemAvailable', () => { expect(isStatusBarItemAvailable('claude', [])).toBe(false) expect(isStatusBarItemAvailable('codex', ['claude'])).toBe(false) expect(isStatusBarItemAvailable('gemini', ['claude', 'codex'])).toBe(false) + expect(isStatusBarItemAvailable('antigravity', ['claude', 'codex'])).toBe(false) expect(isStatusBarItemAvailable('grok', ['claude', 'kimi'])).toBe(false) }) @@ -32,6 +34,7 @@ describe('isStatusBarItemAvailable', () => { expect(isStatusBarItemAvailable('claude', ['claude'])).toBe(true) expect(isStatusBarItemAvailable('codex', ['codex', 'claude'])).toBe(true) expect(isStatusBarItemAvailable('gemini', ['gemini'])).toBe(true) + expect(isStatusBarItemAvailable('antigravity', ['antigravity'])).toBe(true) expect(isStatusBarItemAvailable('grok', ['grok'])).toBe(true) }) }) diff --git a/src/renderer/src/components/status-bar/status-bar-agent-gating.ts b/src/renderer/src/components/status-bar/status-bar-agent-gating.ts index 805d7079c..4ded63147 100644 --- a/src/renderer/src/components/status-bar/status-bar-agent-gating.ts +++ b/src/renderer/src/components/status-bar/status-bar-agent-gating.ts @@ -1,6 +1,6 @@ import type { StatusBarItem, TuiAgent } from '../../../../shared/types' -// Why: Claude/Codex/Gemini usage bars are surface noise when the underlying +// Why: CLI-backed usage bars are surface noise when the underlying // CLI isn't installed (e.g. a fresh Ubuntu install showing "Gemini Usage" // when no Gemini CLI is on PATH). We hide both the bar and its toggle when // PATH detection reports the agent as missing. Pre-detection (null) keeps @@ -11,6 +11,7 @@ const CLI_GATED_ITEMS: ReadonlySet = new Set([ 'codex', 'gemini', 'kimi', + 'antigravity', 'grok' ]) 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 2dbba2315..7389a372f 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 @@ -70,6 +70,7 @@ function usageSettings(overrides: Partial = {}): UsagePro claudeManagedAccounts: [], opencodeSessionCookie: '', geminiCliOAuthEnabled: false, + antigravityUsageConfigured: false, minimaxCookieConfigured: false, grokAuthConfigured: false, ...overrides @@ -119,6 +120,11 @@ describe('hasUsageProviderSettings', () => { expect( hasUsageProviderSettings(usageSettings({ opencodeSessionCookie: ' session=abc ' })) ).toBe(true) + // Why: antigravity durability requires the Gemini OAuth opt-in; the + // checked item alone must not suppress the usage setup CTA. + expect(hasUsageProviderSettings(usageSettings({ antigravityUsageConfigured: true }))).toBe( + false + ) expect(hasUsageProviderSettings(usageSettings({ minimaxCookieConfigured: true }))).toBe(true) expect(hasUsageProviderSettings(usageSettings({ grokAuthConfigured: true }))).toBe(true) }) @@ -150,6 +156,32 @@ describe('hasUsageProviderSettingsForProvider', () => { ).toBe(true) expect(hasUsageProviderSettingsForProvider('claude', usageSettings())).toBe(false) expect(hasUsageProviderSettingsForProvider('kimi', usageSettings())).toBe(false) + expect(hasUsageProviderSettingsForProvider('grok', usageSettings())).toBe(false) + }) + + it('requires both a checked Antigravity item and Gemini OAuth as the durable Antigravity signal', () => { + expect( + hasUsageProviderSettingsForProvider( + 'antigravity', + usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: 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. + expect( + hasUsageProviderSettingsForProvider( + 'antigravity', + usageSettings({ antigravityUsageConfigured: true }) + ) + ).toBe(false) + expect( + hasUsageProviderSettingsForProvider( + 'antigravity', + usageSettings({ geminiCliOAuthEnabled: true }) + ) + ).toBe(false) + expect(hasUsageProviderSettingsForProvider('antigravity', usageSettings())).toBe(false) + expect(hasUsageProviderSettingsForProvider('antigravity', null)).toBe(false) }) it('treats minimaxCookieConfigured as the durable signal for MiniMax', () => { @@ -283,6 +315,42 @@ describe('getVisibleUsageProvider', () => { ) ).toBe(null) }) + + it('keeps Antigravity visible while the snapshot is pending when checked and Gemini OAuth is on', () => { + const visible = getVisibleUsageProvider( + 'antigravity', + null, + usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: true }) + ) + expect(visible).toMatchObject({ + provider: 'antigravity', + status: 'fetching', + session: null, + weekly: null + }) + }) + + 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) + expect( + getVisibleUsageProvider( + 'antigravity', + provider('unavailable', { + provider: 'antigravity', + error: 'Gemini CLI OAuth is disabled in settings' + }), + usageSettings({ antigravityUsageConfigured: true }) + ) + ).toBe(null) + }) }) describe('isUsageEmptyState', () => { @@ -295,6 +363,7 @@ describe('isUsageEmptyState', () => { gemini: null, opencodeGo: null, kimi: null, + antigravity: null, minimax: null, grok: null }, @@ -312,6 +381,7 @@ describe('isUsageEmptyState', () => { gemini: provider('unavailable'), opencodeGo: provider('unavailable', { provider: 'opencode-go' }), kimi: provider('unavailable', { provider: 'kimi' }), + antigravity: provider('unavailable', { provider: 'antigravity' }), minimax: provider('unavailable', { provider: 'minimax' }), grok: provider('unavailable', { provider: 'grok' }) }, @@ -329,6 +399,7 @@ describe('isUsageEmptyState', () => { gemini: provider('unavailable'), opencodeGo: provider('unavailable', { provider: 'opencode-go' }), kimi: provider('unavailable', { provider: 'kimi' }), + antigravity: provider('unavailable', { provider: 'antigravity' }), minimax: provider('unavailable', { provider: 'minimax' }), grok: provider('unavailable', { provider: 'grok' }) }, @@ -357,6 +428,7 @@ describe('isUsageEmptyState', () => { gemini: null, opencodeGo: null, kimi: null, + antigravity: null, minimax: null, grok: null }, @@ -374,6 +446,7 @@ describe('isUsageEmptyState', () => { gemini: provider('unavailable'), opencodeGo: provider('unavailable', { provider: 'opencode-go' }), kimi: provider('unavailable', { provider: 'kimi' }), + antigravity: null, minimax: provider('unavailable', { provider: 'minimax' }), grok: provider('unavailable', { provider: 'grok' }) }, @@ -381,4 +454,42 @@ describe('isUsageEmptyState', () => { ) ).toBe(true) }) + + it('does not show the setup CTA while checked Antigravity usage is awaiting a snapshot', () => { + expect( + isUsageEmptyState( + { + claude: provider('unavailable', { provider: 'claude' }), + codex: provider('unavailable', { provider: 'codex' }), + gemini: provider('unavailable'), + opencodeGo: provider('unavailable', { provider: 'opencode-go' }), + kimi: provider('unavailable', { provider: 'kimi' }), + antigravity: null, + grok: provider('unavailable', { provider: 'grok' }), + minimax: provider('unavailable', { provider: 'minimax' }) + }, + usageSettings({ antigravityUsageConfigured: true, geminiCliOAuthEnabled: 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 must not hide the teaching CTA from users who set nothing up. + expect( + isUsageEmptyState( + { + claude: provider('unavailable', { provider: 'claude' }), + codex: provider('unavailable', { provider: 'codex' }), + gemini: provider('unavailable'), + opencodeGo: provider('unavailable', { provider: 'opencode-go' }), + kimi: provider('unavailable', { provider: 'kimi' }), + antigravity: null, + grok: provider('unavailable', { provider: 'grok' }), + minimax: provider('unavailable', { provider: 'minimax' }) + }, + usageSettings({ antigravityUsageConfigured: true }) + ) + ).toBe(true) + }) }) 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 54e7ad1a3..1da3dc0f7 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 @@ -8,6 +8,12 @@ export type UsageProviderSettings = Pick< | 'opencodeSessionCookie' | 'geminiCliOAuthEnabled' > & { + // 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. + antigravityUsageConfigured: boolean // Why: MiniMax/Grok sign-in live on disk, not in settings; main sets these each poll. minimaxCookieConfigured: boolean grokAuthConfigured: boolean @@ -19,6 +25,7 @@ type UsageProviderSnapshots = { gemini: ProviderRateLimits | null opencodeGo: ProviderRateLimits | null kimi: ProviderRateLimits | null + antigravity: ProviderRateLimits | null minimax: ProviderRateLimits | null grok: ProviderRateLimits | null } @@ -65,6 +72,8 @@ 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. settings?.minimaxCookieConfigured === true || settings?.grokAuthConfigured === true ) @@ -89,6 +98,12 @@ export function hasUsageProviderSettingsForProvider( if (providerId === 'opencode-go') { 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 + } if (providerId === 'minimax') { return settings.minimaxCookieConfigured === true } @@ -137,12 +152,16 @@ export function isUsageEmptyState( // Why: system-default Claude/Codex accounts have no persisted account row; // their first durable signal is the usage snapshot, so wait for snapshots to // settle before teaching the user to connect an account. + const antigravitySnapshotPending = + hasUsageProviderSettingsForProvider('antigravity', settings) && + isProviderSnapshotPending(providers.antigravity) if ( isProviderSnapshotPending(providers.claude) || isProviderSnapshotPending(providers.codex) || isProviderSnapshotPending(providers.gemini) || isProviderSnapshotPending(providers.opencodeGo) || isProviderSnapshotPending(providers.kimi) || + antigravitySnapshotPending || isProviderSnapshotPending(providers.minimax) || isProviderSnapshotPending(providers.grok) ) { @@ -155,6 +174,7 @@ export function isUsageEmptyState( !isProviderConfigured(providers.gemini) && !isProviderConfigured(providers.opencodeGo) && !isProviderConfigured(providers.kimi) && + !isProviderConfigured(providers.antigravity) && !isProviderConfigured(providers.minimax) && !isProviderConfigured(providers.grok) ) diff --git a/src/renderer/src/components/status-bar/tooltip.test.ts b/src/renderer/src/components/status-bar/tooltip.test.ts index 510e3eea6..dea9af3ab 100644 --- a/src/renderer/src/components/status-bar/tooltip.test.ts +++ b/src/renderer/src/components/status-bar/tooltip.test.ts @@ -1,10 +1,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { renderToStaticMarkup } from 'react-dom/server' +import type * as ReactModule from 'react' import type { ProviderRateLimits } from '../../../../shared/rate-limit-types' -vi.mock('@/lib/agent-catalog', () => ({ - AgentIcon: () => null -})) +vi.mock('@/lib/agent-catalog', async () => { + const ReactActual = await vi.importActual('react') + return { + AgentIcon: ({ agent }: { agent: string }) => + ReactActual.createElement('span', { 'data-agent-icon': agent }) + } +}) vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string, values?: Record) => { @@ -448,6 +453,11 @@ describe('ProviderPanel reset rendering', () => { }) describe('ProviderIcon', () => { + it('renders the Antigravity agent icon for the antigravity provider', () => { + const markup = renderToStaticMarkup(ProviderIcon({ provider: 'antigravity' })) + expect(markup).toContain('data-agent-icon="antigravity"') + }) + it('renders the official MiniMax icon asset for the minimax provider', () => { // Why: the icon must travel to the status bar / tooltip unchanged so the // user recognises the brand. We pin it to an with a non-empty diff --git a/src/renderer/src/components/status-bar/tooltip.tsx b/src/renderer/src/components/status-bar/tooltip.tsx index c013c1104..be174965b 100644 --- a/src/renderer/src/components/status-bar/tooltip.tsx +++ b/src/renderer/src/components/status-bar/tooltip.tsx @@ -93,6 +93,9 @@ export function ProviderIcon({ provider }: { provider: string }): React.JSX.Elem if (provider === 'kimi') { return } + if (provider === 'antigravity') { + return + } if (provider === 'minimax') { return } diff --git a/src/renderer/src/components/status-bar/usage-error-copy.test.ts b/src/renderer/src/components/status-bar/usage-error-copy.test.ts index 3fc29780a..c91d0704a 100644 --- a/src/renderer/src/components/status-bar/usage-error-copy.test.ts +++ b/src/renderer/src/components/status-bar/usage-error-copy.test.ts @@ -7,6 +7,10 @@ vi.mock('@/i18n/i18n', () => ({ import { getProviderDisplayName } from './usage-error-copy' describe('getProviderDisplayName', () => { + it('returns the Antigravity brand name', () => { + expect(getProviderDisplayName('antigravity')).toBe('Antigravity') + }) + it('returns the MiniMax brand name', () => { expect(getProviderDisplayName('minimax')).toBe('MiniMax') }) @@ -17,6 +21,7 @@ describe('getProviderDisplayName', () => { expect(getProviderDisplayName('gemini')).toBe('Gemini') expect(getProviderDisplayName('opencode-go')).toBe('OpenCode Go') expect(getProviderDisplayName('kimi')).toBe('Kimi') + expect(getProviderDisplayName('grok')).toBe('Grok') }) it('falls back to the raw provider id when no mapping exists', () => { diff --git a/src/renderer/src/components/status-bar/usage-error-copy.ts b/src/renderer/src/components/status-bar/usage-error-copy.ts index f6f9b8d69..83fc1888b 100644 --- a/src/renderer/src/components/status-bar/usage-error-copy.ts +++ b/src/renderer/src/components/status-bar/usage-error-copy.ts @@ -17,6 +17,9 @@ export function getProviderDisplayName(provider: ProviderRateLimits['provider']) if (provider === 'kimi') { return 'Kimi' } + if (provider === 'antigravity') { + return 'Antigravity' + } if (provider === 'minimax') { return 'MiniMax' } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 7c8045033..004a9d9e3 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -29,6 +29,7 @@ "sshToggleDescription": "Show configured SSH and remote Orca hosts when any are available.", "resourceUsageToggleDescription": "Show the Resource Manager. Click it for CPU, memory, sessions, daemon controls, and workspace disk scans.", "portsToggleDescription": "Show live workspace ports. Click it for workspace-scoped ports and external listeners.", + "antigravityToggleDescription": "Show Antigravity subscription usage for the active workspace.", "grokToggleDescription": "Show Grok subscription credit usage when signed in via Grok CLI." } } @@ -3034,6 +3035,8 @@ "06741a2f3d": "Open MiniMax usage details", "3bbf140864": "MiniMax Usage", "remoteServerLabel": "Remote server", + "antigravityUsage": "Antigravity Usage", + "antigravityUsageDetails": "Open Antigravity usage details", "grokUsageAria": "Open Grok usage details", "grokUsageMenu": "Grok Usage" }, @@ -7223,6 +7226,9 @@ "workspaceOptions": "workspace options", "detailed": "detailed" }, + "antigravityUsageTitle": "Antigravity Usage", + "antigravityUsageDescription": "Show Antigravity subscription usage in the status bar.", + "antigravityKeyword": "antigravity", "f8e2a1c4b6": "Grok Usage", "e7d1b0f3a5": "Show Grok weekly credit usage from Grok CLI OAuth.", "d6c0a9e2f4": "grok", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index ffd31bdd3..b8d4ca45d 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -29,6 +29,7 @@ "sshToggleDescription": "Muestra hosts SSH configurados y hosts remotos de Orca cuando haya alguno disponible.", "resourceUsageToggleDescription": "Muestra el Administrador de recursos. Haz clic para ver CPU, memoria, sesiones, controles del servicio en segundo plano y análisis de disco de los espacios de trabajo.", "portsToggleDescription": "Muestra los puertos activos de los espacios de trabajo. Haz clic para ver los puertos de cada espacio de trabajo y los puertos externos.", + "antigravityToggleDescription": "Muestra el uso de suscripción de Antigravity para el espacio de trabajo activo.", "grokToggleDescription": "Muestra el uso de créditos de suscripción de Grok cuando has iniciado sesión con Grok CLI." } } @@ -3034,6 +3035,8 @@ "06741a2f3d": "Abrir detalles de uso de MiniMax", "3bbf140864": "Uso de MiniMax", "remoteServerLabel": "Servidor remoto", + "antigravityUsage": "Uso de Antigravity", + "antigravityUsageDetails": "Abrir detalles de uso de Antigravity", "grokUsageAria": "Abrir detalles de uso de Grok", "grokUsageMenu": "Uso de Grok" }, @@ -7186,6 +7189,9 @@ }, "9a115966d3": "Minimizar a la bandeja al cerrar", "4d5b9427b5": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir.", + "antigravityUsageTitle": "Uso de Antigravity", + "antigravityUsageDescription": "Muestra el uso de suscripción de Antigravity en la barra de estado.", + "antigravityKeyword": "antigravity", "f8e2a1c4b6": "Uso de Grok", "e7d1b0f3a5": "Muestra el uso semanal de créditos de Grok desde OAuth de Grok CLI.", "d6c0a9e2f4": "grok", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 360123c88..d35dc5d81 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -29,6 +29,7 @@ "sshToggleDescription": "設定済み SSH ホストとリモート Orca ホストがある場合に表示します。", "resourceUsageToggleDescription": "リソースマネージャーを表示します。これをクリックすると、CPU、メモリ、セッション、デーモン コントロール、およびワークスペース ディスク スキャンが行われます。", "portsToggleDescription": "ライブワークスペースポートを表示します。ワークスペーススコープのポートと外部リスナーの場合はこれをクリックします。", + "antigravityToggleDescription": "アクティブなワークスペースの Antigravity サブスクリプション使用量を表示します。", "grokToggleDescription": "Grok CLI でサインインしている場合に Grok サブスクリプションのクレジット使用量を表示します。" } } @@ -3034,6 +3035,8 @@ "06741a2f3d": "MiniMax の使用量詳細を開く", "3bbf140864": "MiniMax 使用量", "remoteServerLabel": "リモートサーバー", + "antigravityUsage": "Antigravity の使用状況", + "antigravityUsageDetails": "Antigravity の使用状況詳細を開く", "grokUsageAria": "Grok の使用状況詳細を開く", "grokUsageMenu": "Grok の使用状況" }, @@ -7208,6 +7211,9 @@ }, "9a115966d3": "閉じるときにトレイへ最小化", "4d5b9427b5": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。", + "antigravityUsageTitle": "Antigravity の使用状況", + "antigravityUsageDescription": "ステータスバーに Antigravity サブスクリプションの使用量を表示します。", + "antigravityKeyword": "antigravity", "f8e2a1c4b6": "Grok の使用状況", "e7d1b0f3a5": "Grok CLI OAuth から Grok の週次クレジット使用量を表示します。", "d6c0a9e2f4": "grok", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index c74f362a7..390d5836d 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -29,6 +29,7 @@ "sshToggleDescription": "사용 가능한 SSH 및 원격 Orca 호스트가 있으면 표시합니다.", "resourceUsageToggleDescription": "리소스 관리자를 표시합니다. 클릭하면 CPU, 메모리, 세션, 데몬 제어, 워크스페이스 디스크 스캔을 볼 수 있습니다.", "portsToggleDescription": "라이브 워크스페이스 포트를 표시합니다. 워크스페이스 범위 포트 및 외부 수신기를 보려면 클릭하세요.", + "antigravityToggleDescription": "활성 워크스페이스에 대한 Antigravity 구독 사용량을 표시합니다.", "grokToggleDescription": "Grok CLI로 로그인한 경우 Grok 구독 크레딧 사용량을 표시합니다." } } @@ -3034,6 +3035,8 @@ "06741a2f3d": "MiniMax 사용량 세부 정보 열기", "3bbf140864": "MiniMax 사용량", "remoteServerLabel": "원격 서버", + "antigravityUsage": "Antigravity 사용량", + "antigravityUsageDetails": "Antigravity 사용량 세부 정보 열기", "grokUsageAria": "Grok 사용량 세부 정보 열기", "grokUsageMenu": "Grok 사용량" }, @@ -7171,6 +7174,9 @@ "workspaceOptions": "워크스페이스 옵션", "detailed": "상세" }, + "antigravityUsageTitle": "Antigravity 사용량", + "antigravityUsageDescription": "상태 표시줄에 Antigravity 구독 사용량을 표시합니다.", + "antigravityKeyword": "antigravity", "f8e2a1c4b6": "Grok 사용량", "e7d1b0f3a5": "Grok CLI OAuth에서 Grok 주간 크레딧 사용량을 표시합니다.", "d6c0a9e2f4": "grok", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 3796138b5..dee19362a 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -29,6 +29,7 @@ "sshToggleDescription": "当有可用的 SSH 和远程 Orca 主机时显示它们。", "resourceUsageToggleDescription": "显示资源管理器。点击可查看 CPU、内存、会话、守护进程控制和工作区磁盘扫描。", "portsToggleDescription": "显示实时工作区端口。单击它可获取工作区范围的端口和外部侦听器。", + "antigravityToggleDescription": "显示当前工作区的 Antigravity 订阅使用量。", "grokToggleDescription": "通过 Grok CLI 登录后显示 Grok 订阅额度使用量。" } } @@ -3034,6 +3035,8 @@ "06741a2f3d": "打开 MiniMax 使用量详情", "3bbf140864": "MiniMax 使用量", "remoteServerLabel": "远程服务器", + "antigravityUsage": "Antigravity 使用量", + "antigravityUsageDetails": "打开 Antigravity 使用详情", "grokUsageAria": "打开 Grok 使用详情", "grokUsageMenu": "Grok 使用量" }, @@ -7171,6 +7174,9 @@ }, "9a115966d3": "关闭时最小化到托盘", "4d5b9427b5": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。", + "antigravityUsageTitle": "Antigravity 使用量", + "antigravityUsageDescription": "在状态栏中显示 Antigravity 订阅使用量。", + "antigravityKeyword": "antigravity", "f8e2a1c4b6": "Grok 使用量", "e7d1b0f3a5": "显示来自 Grok CLI OAuth 的 Grok 每周额度使用量。", "d6c0a9e2f4": "grok", diff --git a/src/renderer/src/store/slices/rate-limits.test.ts b/src/renderer/src/store/slices/rate-limits.test.ts new file mode 100644 index 000000000..9772483f9 --- /dev/null +++ b/src/renderer/src/store/slices/rate-limits.test.ts @@ -0,0 +1,19 @@ +import { createStore, type StoreApi } from 'zustand/vanilla' +import { describe, expect, it } from 'vitest' +import { createRateLimitSlice } from './rate-limits' +import type { AppState } from '../types' + +function createRateLimitStore(): StoreApi { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return createStore()((...args: any[]) => + createRateLimitSlice(...(args as Parameters)) + ) as unknown as StoreApi +} + +describe('createRateLimitSlice', () => { + it('initializes Antigravity usage with a stable pending key', () => { + const store = createRateLimitStore() + + expect(store.getState().rateLimits.antigravity).toBeNull() + }) +}) diff --git a/src/renderer/src/store/slices/rate-limits.ts b/src/renderer/src/store/slices/rate-limits.ts index 37bd64383..3df495cf3 100644 --- a/src/renderer/src/store/slices/rate-limits.ts +++ b/src/renderer/src/store/slices/rate-limits.ts @@ -22,6 +22,7 @@ export const createRateLimitSlice: StateCreator { 'ports', 'kimi', 'minimax', + 'antigravity', 'grok' ]) expect(setUI).toHaveBeenCalledWith({ - statusBarItems: ['claude', 'resource-usage', 'ports', 'kimi', 'minimax', 'grok'], + statusBarItems: [ + 'claude', + 'resource-usage', + 'ports', + 'kimi', + 'minimax', + 'antigravity', + 'grok' + ], _portsStatusBarDefaultAdded: true, _kimiStatusBarDefaultAdded: true, _minimaxStatusBarDefaultAdded: true, + _antigravityStatusBarDefaultAdded: true, _grokStatusBarDefaultAdded: true }) }) @@ -1241,6 +1251,7 @@ describe('createUISlice hydratePersistedUI', () => { _portsStatusBarDefaultAdded: true, _kimiStatusBarDefaultAdded: true, _minimaxStatusBarDefaultAdded: true, + _antigravityStatusBarDefaultAdded: true, _grokStatusBarDefaultAdded: true }) ) diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts index 321f9ce09..f5d2432a9 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -265,6 +265,7 @@ function migrateStatusBarItems(items: readonly string[] | undefined): StatusBarI const DEFAULT_ON_PORTS_STATUS_BAR_ITEM: StatusBarItem = 'ports' const DEFAULT_ON_KIMI_STATUS_BAR_ITEM: StatusBarItem = 'kimi' const DEFAULT_ON_MINIMAX_STATUS_BAR_ITEM: StatusBarItem = 'minimax' +const DEFAULT_ON_ANTIGRAVITY_STATUS_BAR_ITEM: StatusBarItem = 'antigravity' const DEFAULT_ON_GROK_STATUS_BAR_ITEM: StatusBarItem = 'grok' function normalizeHydratedVisibleWorkspaceHostIds(ui: PersistedUIState): VisibleWorkspaceHostIds { @@ -2264,14 +2265,19 @@ export const createUISlice: StateCreator = (set, get) ui._minimaxStatusBarDefaultAdded || statusBarItems.includes('minimax') ? statusBarItems : [...statusBarItems, DEFAULT_ON_MINIMAX_STATUS_BAR_ITEM] - const statusBarItemsWithGrok = - ui._grokStatusBarDefaultAdded || statusBarItemsWithMiniMax.includes('grok') + const statusBarItemsWithAntigravity = + ui._antigravityStatusBarDefaultAdded || statusBarItemsWithMiniMax.includes('antigravity') ? statusBarItemsWithMiniMax - : [...statusBarItemsWithMiniMax, DEFAULT_ON_GROK_STATUS_BAR_ITEM] + : [...statusBarItemsWithMiniMax, DEFAULT_ON_ANTIGRAVITY_STATUS_BAR_ITEM] + const statusBarItemsWithGrok = + ui._grokStatusBarDefaultAdded || statusBarItemsWithAntigravity.includes('grok') + ? statusBarItemsWithAntigravity + : [...statusBarItemsWithAntigravity, DEFAULT_ON_GROK_STATUS_BAR_ITEM] if ( (!ui._portsStatusBarDefaultAdded || !ui._kimiStatusBarDefaultAdded || !ui._minimaxStatusBarDefaultAdded || + !ui._antigravityStatusBarDefaultAdded || !ui._grokStatusBarDefaultAdded) && typeof window !== 'undefined' ) { @@ -2281,6 +2287,7 @@ export const createUISlice: StateCreator = (set, get) _portsStatusBarDefaultAdded: true, _kimiStatusBarDefaultAdded: true, _minimaxStatusBarDefaultAdded: true, + _antigravityStatusBarDefaultAdded: true, _grokStatusBarDefaultAdded: true }) .catch(console.error) diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index 62df42b60..4bd602c76 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -2587,6 +2587,7 @@ function createRateLimitsApi(): NonNullable['rateLimits']> { gemini: null, opencodeGo: null, kimi: null, + antigravity: null, minimax: null, grok: null, minimaxCookieConfigured: false, diff --git a/src/shared/rate-limit-types.test.ts b/src/shared/rate-limit-types.test.ts index 255965ec0..6d35c2d22 100644 --- a/src/shared/rate-limit-types.test.ts +++ b/src/shared/rate-limit-types.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import type { RateLimitState } from './rate-limit-types' describe('RateLimitState', () => { - it('documents the MiniMax surface used by the AccountsPane settings UI', () => { + it('documents provider surfaces used by settings and status-bar UI', () => { // Why: the AccountsPane and the status bar both read these fields // from RateLimitState. The shape must stay stable so that the // visibility check (status-bar-provider-visibility) keeps working @@ -13,6 +13,7 @@ describe('RateLimitState', () => { gemini: null, opencodeGo: null, kimi: null, + antigravity: null, minimax: null, grok: null, minimaxCookieConfigured: false, @@ -23,6 +24,7 @@ describe('RateLimitState', () => { inactiveCodexAccounts: [] } + expect(state.antigravity).toBeNull() expect(state.minimax).toBeNull() expect(state.minimaxCookieConfigured).toBe(false) }) diff --git a/src/shared/rate-limit-types.ts b/src/shared/rate-limit-types.ts index d19699dee..5470e8515 100644 --- a/src/shared/rate-limit-types.ts +++ b/src/shared/rate-limit-types.ts @@ -44,7 +44,7 @@ export type UsageRateLimitMetadata = { } export type ProviderRateLimits = { - provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok' + provider: 'claude' | 'codex' | 'gemini' | 'opencode-go' | 'kimi' | 'minimax' | 'grok' | 'antigravity' /** 5-hour session window, null if not available. */ session: RateLimitWindow | null /** 7-day weekly window, null if not available. */ @@ -109,6 +109,7 @@ export type RateLimitState = { gemini: ProviderRateLimits | null opencodeGo: ProviderRateLimits | null kimi: ProviderRateLimits | null + antigravity: ProviderRateLimits | null minimax: ProviderRateLimits | null grok: ProviderRateLimits | null /** diff --git a/src/shared/status-bar-defaults.ts b/src/shared/status-bar-defaults.ts index 0d48a6364..b1884262d 100644 --- a/src/shared/status-bar-defaults.ts +++ b/src/shared/status-bar-defaults.ts @@ -4,6 +4,7 @@ export const DEFAULT_STATUS_BAR_ITEMS: StatusBarItem[] = [ 'claude', 'codex', 'gemini', + 'antigravity', 'opencode-go', 'kimi', 'minimax', diff --git a/src/shared/types.ts b/src/shared/types.ts index d6e04ef85..8cc2d2eef 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -3152,6 +3152,7 @@ export type StatusBarItem = | 'claude' | 'codex' | 'gemini' + | 'antigravity' | 'opencode-go' | 'kimi' | 'minimax' @@ -3271,6 +3272,8 @@ export type PersistedUIState = { _kimiStatusBarDefaultAdded?: boolean /** One-shot migration flag for adding the default-on MiniMax status item. */ _minimaxStatusBarDefaultAdded?: boolean + /** One-shot migration flag for adding the default-on Antigravity status item. */ + _antigravityStatusBarDefaultAdded?: boolean /** One-shot migration flag for adding the default-on Grok status item. */ _grokStatusBarDefaultAdded?: boolean statusBarItems: StatusBarItem[]