From 78daf712680e28d27657bcd54229e80916a5cc4d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:03:24 -0700 Subject: [PATCH] fix(status-bar): show Antigravity's model-group pools instead of an empty segment (#24074) The verbose bucket allowlist was written for Gemini's experimental models, and its fallback window was session-or-monthly. Antigravity reports one pool per model group and some tiers meter weekly only, so a signed-in account with an exhausted pool rendered an icon and no number. Antigravity bypasses the allowlist by provider, because its group names come from the account's tier and cannot be enumerated. Cursor stays name-matched so an unrecognised pool still falls back to the plan total. Refs #22511 Refs #16704 --- .../status-bar/StatusBarProviderSegment.tsx | 22 +++- .../status-bar-antigravity-segment.test.tsx | 123 ++++++++++++++++++ .../status-bar-provider-visibility.ts | 6 +- 3 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx 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'