feat(status-bar): Antigravity usage status (#7996)
* fix(status-bar): keep antigravity usage visible * feat(rate-limits): add Grok usage status * feat(grok): add managed usage accounts * test(rate-limits): isolate Codex PTY fallback fetches * fix(grok): address CodeRabbit review on usage status PR - Guard auth.json read/parse so missing files return null - Thread AbortSignal through Grok rate-limit fetch and ACP auth - Pass isRemote:false for floating-terminal agent launch env * fix(grok): wire ACP abort after child listeners to avoid TDZ * fix(status-bar): address CodeRabbit on combined Grok/Antigravity usage - Pin WSL shell distro when resolving managed GROK_HOME (parity with Codex) - Refresh only Grok on account change via fetchGrokOnly - Consolidate usage status-bar toggle catalogs; add Grok locale keys - Fix Grok aria-label, feature-interaction tracking, and test fixture * fix(settings): drop duplicate usage status-bar toggle catalog Remove the leftover re-export path so Appearance search has a single source of truth for usage provider toggles (including Antigravity + Grok). * fix(settings): restore AccountsPane and locale UTF-8 after merge Re-do AccountsPane three-way merge with binary-safe git objects and keep both Grok accounts + remote provider client imports. Rebuild locale JSON merges so em dash/ellipsis/middle-dot strings are no longer mojibake. * fix(i18n): add missing comma after merged status-bar locale keys * test: add antigravity to GrokUsagePane rate-limit fixture RateLimitState gained a required antigravity field; the pane fixture must carry it to typecheck. * fix(status-bar): gate Antigravity durable visibility on Gemini OAuth opt-in The Antigravity snapshot mirrors the Gemini fetch, which is permanently 'unavailable' until the user opts into Gemini CLI OAuth. Without this gate the default-on checked item plus a detected agy CLI pinned a dead 'A --' bar (Gemini itself hides in that state) and suppressed the usage setup CTA for users who configured nothing. --------- Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
This commit is contained in:
parent
143c36e566
commit
4209889f60
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement>(
|
||||
'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'))
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ function recordStatusBarToggleInteraction(
|
|||
id === 'gemini' ||
|
||||
id === 'opencode-go' ||
|
||||
id === 'kimi' ||
|
||||
id === 'antigravity' ||
|
||||
id === 'minimax' ||
|
||||
id === 'grok'
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const mockStoreState = {
|
|||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
antigravity: null,
|
||||
minimax: null,
|
||||
grok: {
|
||||
provider: 'grok',
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -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 && (
|
||||
<ProviderDetailsMenu
|
||||
provider={visibleAntigravity}
|
||||
compact={compact}
|
||||
iconOnly={iconOnly}
|
||||
ariaLabel={translate(
|
||||
'auto.components.status.bar.StatusBar.antigravityUsageDetails',
|
||||
'Open Antigravity usage details'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showOpencodeGo && (
|
||||
<ProviderDetailsMenu
|
||||
provider={visibleOpencodeGo}
|
||||
|
|
@ -2210,6 +2239,21 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
{translate('auto.components.status.bar.StatusBar.c1df0d67ec', 'Gemini Usage')}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
{isStatusBarItemAvailable('antigravity', detectedAgentIds) && (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={statusBarItems.includes('antigravity')}
|
||||
onCheckedChange={() => {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
toggleStatusBarItem('antigravity')
|
||||
}}
|
||||
>
|
||||
<AgentIcon agent="antigravity" size={14} />
|
||||
{translate(
|
||||
'auto.components.status.bar.StatusBar.antigravityUsage',
|
||||
'Antigravity Usage'
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={statusBarItems.includes('opencode-go')}
|
||||
onCheckedChange={() => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<StatusBarItem> = new Set([
|
|||
'codex',
|
||||
'gemini',
|
||||
'kimi',
|
||||
'antigravity',
|
||||
'grok'
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ function usageSettings(overrides: Partial<UsageProviderSettings> = {}): 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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<typeof ReactModule>('react')
|
||||
return {
|
||||
AgentIcon: ({ agent }: { agent: string }) =>
|
||||
ReactActual.createElement('span', { 'data-agent-icon': agent })
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
translate: (_key: string, fallback: string, values?: Record<string, string>) => {
|
||||
|
|
@ -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 <img> with a non-empty
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ export function ProviderIcon({ provider }: { provider: string }): React.JSX.Elem
|
|||
if (provider === 'kimi') {
|
||||
return <AgentIcon agent="kimi" size={13} />
|
||||
}
|
||||
if (provider === 'antigravity') {
|
||||
return <AgentIcon agent="antigravity" size={13} />
|
||||
}
|
||||
if (provider === 'minimax') {
|
||||
return <MiniMaxIcon size={13} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<AppState> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return createStore<any>()((...args: any[]) =>
|
||||
createRateLimitSlice(...(args as Parameters<typeof createRateLimitSlice>))
|
||||
) as unknown as StoreApi<AppState>
|
||||
}
|
||||
|
||||
describe('createRateLimitSlice', () => {
|
||||
it('initializes Antigravity usage with a stable pending key', () => {
|
||||
const store = createRateLimitStore()
|
||||
|
||||
expect(store.getState().rateLimits.antigravity).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -22,6 +22,7 @@ export const createRateLimitSlice: StateCreator<AppState, [], [], RateLimitSlice
|
|||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
antigravity: null,
|
||||
minimax: null,
|
||||
grok: null,
|
||||
minimaxCookieConfigured: false,
|
||||
|
|
|
|||
|
|
@ -1219,13 +1219,23 @@ describe('createUISlice hydratePersistedUI', () => {
|
|||
'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
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<AppState, [], [], UISlice> = (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<AppState, [], [], UISlice> = (set, get)
|
|||
_portsStatusBarDefaultAdded: true,
|
||||
_kimiStatusBarDefaultAdded: true,
|
||||
_minimaxStatusBarDefaultAdded: true,
|
||||
_antigravityStatusBarDefaultAdded: true,
|
||||
_grokStatusBarDefaultAdded: true
|
||||
})
|
||||
.catch(console.error)
|
||||
|
|
|
|||
|
|
@ -2587,6 +2587,7 @@ function createRateLimitsApi(): NonNullable<Partial<PreloadApi>['rateLimits']> {
|
|||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
antigravity: null,
|
||||
minimax: null,
|
||||
grok: null,
|
||||
minimaxCookieConfigured: false,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export const DEFAULT_STATUS_BAR_ITEMS: StatusBarItem[] = [
|
|||
'claude',
|
||||
'codex',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'opencode-go',
|
||||
'kimi',
|
||||
'minimax',
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
Loading…
Reference in New Issue