diff --git a/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx b/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx index f3aaa6cbede..0e6aaae68d9 100644 --- a/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx +++ b/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx @@ -165,9 +165,16 @@ function getProviderLetter(provider: ProviderRateLimits['provider']): string { // Why: Gemini exposes extra experimental buckets that made the pre-existing verbose footer noisy. const STATUS_BAR_BUCKET_NAMES = new Set(['Flash', 'Pro', '1.5 Pro']) -// Why: the allowlist above is Gemini's. Cursor's pools are its whole meter — filtering -// them out leaves a signed-in account with an icon and no number at all. -function isVisibleStatusBarBucket(name: string): boolean { +/** + * Why Antigravity is matched by provider and not by name: its pools are one per model group, and the + * group names come from the account's own tier ("Gemini Models", "Claude and GPT models" today), so + * there is no list to allow. Cursor stays name-matched on purpose — a pool Orca does not recognise + * is filtered so the segment can fall back to the plan total instead of showing an unlabelled row. + */ +function isVisibleStatusBarBucket(name: string, provider: ProviderRateLimits['provider']): boolean { + if (provider === 'antigravity') { + return true + } return STATUS_BAR_BUCKET_NAMES.has(name) || isCursorUsageBucket(name) } @@ -179,10 +186,15 @@ function VerboseProviderUsage({ display: UsagePercentageDisplay }): React.JSX.Element { if (p.buckets && p.buckets.length > 0) { - const visibleBuckets = p.buckets.filter((bucket) => isVisibleStatusBarBucket(bucket.name)) + const visibleBuckets = p.buckets.filter((bucket) => + isVisibleStatusBarBucket(bucket.name, p.provider) + ) // Why: a provider whose buckets are all filtered out still has a headline // window worth showing rather than rendering an empty segment. - const fallbackWindow = p.session ?? p.monthly ?? null + // Why weekly is in the chain: a tier metered weekly only (Antigravity reports no 5h pool on + // some tiers) has no session window, and omitting weekly rendered an empty segment for an + // account that does have a limit worth showing. + const fallbackWindow = p.session ?? p.monthly ?? p.weekly ?? null return ( <> {visibleBuckets.map((bucket, index) => ( diff --git a/src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx b/src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx new file mode 100644 index 00000000000..1c203f769ca --- /dev/null +++ b/src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx @@ -0,0 +1,123 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' +import type { ProviderRateLimits, RateLimitWindow } from '../../../../shared/rate-limit-types' + +vi.mock('@/i18n/i18n', () => ({ + i18n: { language: 'en' }, + translate: (_key: string, fallback: string, values?: Record) => { + let result = fallback + for (const [key, value] of Object.entries(values ?? {})) { + result = result.replace(`{{${key}}}`, value) + } + return result + } +})) + +vi.mock('@/lib/agent-catalog', () => ({ + AgentIcon: () => null +})) + +vi.mock('../../store', () => ({ + useAppStore: (selector: (state: { usagePercentageDisplay: 'used' | 'remaining' }) => unknown) => + selector({ usagePercentageDisplay: 'used' }) +})) + +const WEEKLY_MINUTES = 10_080 + +function weeklyWindow(usedPercent: number): RateLimitWindow { + return { usedPercent, windowMinutes: WEEKLY_MINUTES, resetsAt: null, resetDescription: null } +} + +/** + * The record `agy -p "/usage"` produces for a tier metered weekly only, with the Gemini pool + * genuinely exhausted — captured from agy 1.2.11. + */ +function antigravityLimits(overrides: Partial = {}): ProviderRateLimits { + return { + provider: 'antigravity', + session: null, + weekly: weeklyWindow(100), + buckets: [ + { name: 'Gemini Models', ...weeklyWindow(100) }, + { name: 'Claude and GPT models', ...weeklyWindow(0) } + ], + updatedAt: Date.now(), + error: null, + status: 'ok', + ...overrides + } +} + +describe('Antigravity status-bar segment', () => { + it('renders both model-group pools by name', async () => { + // Why: the verbose bucket allowlist was written for Gemini's experimental models, so + // Antigravity's pools — whose names come from the account's tier and cannot be enumerated + // ahead of time — were filtered out and the segment showed no number at all. + const { ProviderSegment } = await import('./StatusBar') + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('Gemini Models') + expect(markup).toContain('Claude and GPT models') + expect(markup).toContain('100%') + }) + + it('shows the weekly window when a tier reports no session pool', async () => { + // Why: the verbose fallback chain was `session ?? monthly`, so a weekly-only provider with a + // real limit rendered an empty segment. + const { ProviderSegment } = await import('./StatusBar') + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('100%') + }) + + it('still renders a reading when only the weekly window is known', async () => { + const { ProviderSegment } = await import('./StatusBar') + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('100%') + }) + + it('does not widen the allowlist for providers that rely on it', async () => { + // Why: Gemini's experimental buckets are still filtered; only providers whose buckets are the + // whole meter bypass the allowlist. + const { ProviderSegment } = await import('./StatusBar') + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('Pro') + expect(markup).not.toContain('Some Experimental Model') + }) +}) 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 24392aa48ab..a49a9fe3a59 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 @@ -137,7 +137,11 @@ function createPendingProviderSnapshot(providerId: UsageProviderId): ProviderRat session: null, weekly: null, ...(providerId === 'opencode-go' ? { monthly: null } : {}), - ...(providerId === 'gemini' || providerId === 'cursor' ? { buckets: [] } : {}), + // Why antigravity joins these: it reports one pool per model group, so its pending skeleton + // has to be bucket-shaped too or the segment changes shape once the first reading lands. + ...(providerId === 'gemini' || providerId === 'cursor' || providerId === 'antigravity' + ? { buckets: [] } + : {}), updatedAt: 0, error: null, status: 'fetching'