feat: real Antigravity usage, zh-TW UI, and renderer diagnostics shim#9360
feat: real Antigravity usage, zh-TW UI, and renderer diagnostics shim#9360RX5950XT wants to merge 1 commit into
Conversation
Stop mirroring Gemini for Antigravity. Read Windows Credential Manager target gemini:antigravity and fetch Cloud Code quota with independent status-bar visibility. Add Traditional Chinese (zh-TW) as a first-class UI language. Shim node:diagnostics_channel so @xterm/addon-ligatures does not whitescreen the renderer or fail production builds.
📝 WalkthroughWalkthroughThe changes add Antigravity credential handling, API access, quota aggregation, rate-limit refresh orchestration, state propagation, and status-bar integration. Traditional Chinese becomes a supported UI locale with normalization, lazy loading, translations, document language synchronization, and font selection. A renderer diagnostics-channel shim is added and wired into Vite resolution, transformation, and dependency pre-bundling. Tests cover the new Antigravity flows, locale behavior, state shape, and UI visibility rules. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 70a679a0-d0ae-4de3-8db5-c881d393697d
📒 Files selected for processing (36)
electron.vite.config.tssrc/main/i18n/main-i18n.tssrc/main/rate-limits/antigravity-api-client.tssrc/main/rate-limits/antigravity-credentials.test.tssrc/main/rate-limits/antigravity-credentials.tssrc/main/rate-limits/antigravity-quota-aggregation.test.tssrc/main/rate-limits/antigravity-quota-aggregation.tssrc/main/rate-limits/antigravity-usage-fetcher.test.tssrc/main/rate-limits/antigravity-usage-fetcher.tssrc/main/rate-limits/service.test.tssrc/main/rate-limits/service.tssrc/renderer/src/assets/main.csssrc/renderer/src/components/settings/appearance-search.tssrc/renderer/src/components/stats/GrokUsagePane.test.tsxsrc/renderer/src/components/status-bar/StatusBar.tsxsrc/renderer/src/components/status-bar/status-bar-provider-visibility.test.tssrc/renderer/src/components/status-bar/status-bar-provider-visibility.tssrc/renderer/src/components/status-bar/tooltip.tsxsrc/renderer/src/i18n/I18nProvider.tsxsrc/renderer/src/i18n/i18n.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh-TW.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/i18n/supported-languages.tssrc/renderer/src/shims/diagnostics-channel.tssrc/renderer/src/store/slices/rate-limits.tssrc/renderer/src/web/web-preload-api.tssrc/shared/rate-limit-types.test.tssrc/shared/rate-limit-types.tssrc/shared/ui-language.test.tssrc/shared/ui-language.tssrc/shared/ui-locale.test.tssrc/shared/ui-locale.ts
| export async function fetchAntigravityQuotaSummary( | ||
| accessToken: string, | ||
| projectId: string | null | ||
| ): Promise<AntigravityQuotaSummaryResponse | null> { | ||
| const result = await fetchWithProjectBodies( | ||
| '/v1internal:retrieveUserQuotaSummary', | ||
| accessToken, | ||
| projectId | ||
| ) | ||
| if (!result.ok) { | ||
| return null | ||
| } | ||
| return (result.data ?? { groups: [] }) as AntigravityQuotaSummaryResponse |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Preserve quota-summary HTTP status so 401 triggers token refresh.
fetchAntigravityQuotaSummary converts every failure to null. A summary 401 combined with a successful models response therefore publishes partial/backfilled usage without entering the refresh path.
src/main/rate-limits/antigravity-api-client.ts#L178-L190: return a discriminated{ errorStatus }result, as the models endpoint does.src/main/rate-limits/antigravity-usage-fetcher.ts#L69-L89: treat summary 401 asunauthorized; retain summary-only/model-only fallback for other failures.
📍 Affects 2 files
src/main/rate-limits/antigravity-api-client.ts#L178-L190(this comment)src/main/rate-limits/antigravity-usage-fetcher.ts#L69-L89
| const qi = info.quotaInfo | ||
| const remaining = | ||
| typeof qi?.remainingFraction === 'number' ? Math.min(1, Math.max(0, qi.remainingFraction)) : 0 | ||
| const reset = qi?.resetTime ?? '' | ||
| const target = family === 'claude' ? claude : gemini | ||
| if (!target || remaining < target.remaining) { | ||
| const next = { remaining, reset: reset.length > 0 ? reset : (target?.reset ?? '') } | ||
| if (family === 'claude') { | ||
| claude = next | ||
| } else { | ||
| gemini = next | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Skip models without quota data instead of synthesizing 100% usage.
The response type permits missing quotaInfo and remainingFraction, but this path converts both to zero. A metadata-only model therefore creates a fake exhausted 5-hour bucket and may make the aggregate session appear 100% used.
Proposed fix
const qi = info.quotaInfo
- const remaining =
- typeof qi?.remainingFraction === 'number' ? Math.min(1, Math.max(0, qi.remainingFraction)) : 0
+ if (typeof qi?.remainingFraction !== 'number') {
+ continue
+ }
+ const remaining = Math.min(1, Math.max(0, qi.remainingFraction))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const qi = info.quotaInfo | |
| const remaining = | |
| typeof qi?.remainingFraction === 'number' ? Math.min(1, Math.max(0, qi.remainingFraction)) : 0 | |
| const reset = qi?.resetTime ?? '' | |
| const target = family === 'claude' ? claude : gemini | |
| if (!target || remaining < target.remaining) { | |
| const next = { remaining, reset: reset.length > 0 ? reset : (target?.reset ?? '') } | |
| if (family === 'claude') { | |
| claude = next | |
| } else { | |
| gemini = next | |
| } | |
| } | |
| const qi = info.quotaInfo | |
| if (typeof qi?.remainingFraction !== 'number') { | |
| continue | |
| } | |
| const remaining = Math.min(1, Math.max(0, qi.remainingFraction)) | |
| const reset = qi?.resetTime ?? '' | |
| const target = family === 'claude' ? claude : gemini | |
| if (!target || remaining < target.remaining) { | |
| const next = { remaining, reset: reset.length > 0 ? reset : (target?.reset ?? '') } | |
| if (family === 'claude') { | |
| claude = next | |
| } else { | |
| gemini = next | |
| } | |
| } |
| const byName = new Map(nextBuckets.map((b) => [b.name, b])) | ||
| let restored = false | ||
| for (const entry of ANTIGRAVITY_BUCKET_ORDER) { | ||
| if (byName.has(entry.name)) { | ||
| continue | ||
| } | ||
| const prevBucket = previous?.buckets?.find((b) => b.name === entry.name) | ||
| if (prevBucket) { | ||
| byName.set(entry.name, prevBucket) | ||
| restored = true | ||
| } | ||
| } | ||
|
|
||
| if (!restored) { | ||
| return next | ||
| } | ||
|
|
||
| const buckets = ANTIGRAVITY_BUCKET_ORDER.map((entry) => byName.get(entry.name)).filter( | ||
| (b): b is RateLimitBucket => b != null | ||
| ) | ||
| const { session, weekly } = deriveAntigravitySessionWeekly(buckets) | ||
| return { | ||
| ...next, | ||
| session, | ||
| weekly, | ||
| buckets, | ||
| error: next.error, | ||
| status: buckets.length > 0 ? 'ok' : next.status, | ||
| usageMetadata: { | ||
| ...next.usageMetadata, | ||
| // Why: restored bars are last-known values, not a fresh official sample. | ||
| lastSuccessfulSource: previous?.usageMetadata?.source ?? next.usageMetadata?.source | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not refresh restored buckets’ age indefinitely.
Each partial response restores old buckets but retains next.updatedAt and changes the result to ok. On the next refresh, those restored values are treated as newly successful again, so an omitted window can survive indefinitely—even beyond its reset or across credential changes.
Track freshness per restored bucket, or otherwise preserve and enforce the original successful timestamp instead of promoting the merged snapshot as fully fresh.
| const antigravityAuthReadResult = await readAntigravityCredentials() | ||
| this.antigravityAuthConfigured = probeAntigravityAuthConfigured(antigravityAuthReadResult) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Propagate cancellation through Antigravity credential and quota reads.
Unlike the other fetchers, this path neither rechecks signal.aborted after the credential await nor passes the signal into fetchAntigravityRateLimits. Consequently, stop() can leave the full cycle waiting while Antigravity continues credential and network work.
Service-side fix
const antigravityAuthReadResult = await readAntigravityCredentials()
+ if (signal.aborted) {
+ return
+ }
this.antigravityAuthConfigured = probeAntigravityAuthConfigured(antigravityAuthReadResult)
...
- fetchAntigravityRateLimits({ credentialsReadResult: antigravityAuthReadResult }),
+ fetchAntigravityRateLimits({
+ credentialsReadResult: antigravityAuthReadResult,
+ signal
+ }),Thread signal through the fetcher and API-client requests as well.
Also applies to: 1456-1456
| runStores: <T>(fn: () => T, ..._args: unknown[]) => T | ||
| } | ||
|
|
||
| type TracingChannel = { | ||
| hasSubscribers: boolean | ||
| subscribe: (..._args: unknown[]) => void | ||
| unsubscribe: (..._args: unknown[]) => void | ||
| traceSync: <T>(fn: () => T, ..._args: unknown[]) => T | ||
| tracePromise: <T>(fn: () => Promise<T>, ..._args: unknown[]) => Promise<T> | ||
| traceCallback: <T>(fn: () => T, ..._args: unknown[]) => T | ||
| } | ||
|
|
||
| function createChannel(): Channel { | ||
| return { | ||
| hasSubscribers: false, | ||
| subscribe() {}, | ||
| unsubscribe() {}, | ||
| publish() {}, | ||
| bindStore() {}, | ||
| unbindStore() {}, | ||
| runStores(fn) { | ||
| return fn() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function createTracingChannel(): TracingChannel { | ||
| return { | ||
| hasSubscribers: false, | ||
| subscribe() {}, | ||
| unsubscribe() {}, | ||
| traceSync(fn) { | ||
| return fn() | ||
| }, | ||
| tracePromise(fn) { | ||
| return fn() | ||
| }, | ||
| traceCallback(fn) { | ||
| return fn() | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Align API signatures and forward arguments to match Node.js.
The shim's execution methods have incorrect signatures that could lead to runtime crashes:
runStoresexpects(context, fn[, thisArg[, ...args]])in Node.js, but the shim expectsfnas the first argument. Calling it with a context object will throwTypeError: _context is not a function.traceSync,tracePromise, andtraceCallbackdropthisArgand...args. If a library passes these arguments,fnwill not receive them, potentially causing downstream logic to fail.
Update the types and implementations to align with Node.js and forward arguments safely.
🐛 Proposed fixes
- runStores: <T>(fn: () => T, ..._args: unknown[]) => T
+ runStores: <T>(context: unknown, fn: (...args: any[]) => T, thisArg?: any, ...args: any[]) => T
}
type TracingChannel = {
hasSubscribers: boolean
subscribe: (..._args: unknown[]) => void
unsubscribe: (..._args: unknown[]) => void
- traceSync: <T>(fn: () => T, ..._args: unknown[]) => T
- tracePromise: <T>(fn: () => Promise<T>, ..._args: unknown[]) => Promise<T>
- traceCallback: <T>(fn: () => T, ..._args: unknown[]) => T
+ traceSync: <T>(fn: (...args: any[]) => T, context?: unknown, thisArg?: any, ...args: any[]) => T
+ tracePromise: <T>(fn: (...args: any[]) => Promise<T>, context?: unknown, thisArg?: any, ...args: any[]) => Promise<T>
+ traceCallback: <T>(fn: (...args: any[]) => T, position?: number, context?: unknown, thisArg?: any, ...args: any[]) => T
}
function createChannel(): Channel {
return {
hasSubscribers: false,
subscribe() {},
unsubscribe() {},
publish() {},
bindStore() {},
unbindStore() {},
- runStores(fn) {
- return fn()
+ runStores(_context, fn, thisArg, ...args) {
+ return fn.apply(thisArg, args)
}
}
}
function createTracingChannel(): TracingChannel {
return {
hasSubscribers: false,
subscribe() {},
unsubscribe() {},
- traceSync(fn) {
- return fn()
+ traceSync(fn, _context, thisArg, ...args) {
+ return fn.apply(thisArg, args)
},
- tracePromise(fn) {
- return fn()
+ tracePromise(fn, _context, thisArg, ...args) {
+ return fn.apply(thisArg, args)
},
- traceCallback(fn) {
- return fn()
+ traceCallback(fn, _position, _context, thisArg, ...args) {
+ return fn.apply(thisArg, args)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| runStores: <T>(fn: () => T, ..._args: unknown[]) => T | |
| } | |
| type TracingChannel = { | |
| hasSubscribers: boolean | |
| subscribe: (..._args: unknown[]) => void | |
| unsubscribe: (..._args: unknown[]) => void | |
| traceSync: <T>(fn: () => T, ..._args: unknown[]) => T | |
| tracePromise: <T>(fn: () => Promise<T>, ..._args: unknown[]) => Promise<T> | |
| traceCallback: <T>(fn: () => T, ..._args: unknown[]) => T | |
| } | |
| function createChannel(): Channel { | |
| return { | |
| hasSubscribers: false, | |
| subscribe() {}, | |
| unsubscribe() {}, | |
| publish() {}, | |
| bindStore() {}, | |
| unbindStore() {}, | |
| runStores(fn) { | |
| return fn() | |
| } | |
| } | |
| } | |
| function createTracingChannel(): TracingChannel { | |
| return { | |
| hasSubscribers: false, | |
| subscribe() {}, | |
| unsubscribe() {}, | |
| traceSync(fn) { | |
| return fn() | |
| }, | |
| tracePromise(fn) { | |
| return fn() | |
| }, | |
| traceCallback(fn) { | |
| return fn() | |
| } | |
| runStores: <T>(context: unknown, fn: (...args: any[]) => T, thisArg?: any, ...args: any[]) => T | |
| } | |
| type TracingChannel = { | |
| hasSubscribers: boolean | |
| subscribe: (..._args: unknown[]) => void | |
| unsubscribe: (..._args: unknown[]) => void | |
| traceSync: <T>(fn: (...args: any[]) => T, context?: unknown, thisArg?: any, ...args: any[]) => T | |
| tracePromise: <T>(fn: (...args: any[]) => Promise<T>, context?: unknown, thisArg?: any, ...args: any[]) => Promise<T> | |
| traceCallback: <T>(fn: (...args: any[]) => T, position?: number, context?: unknown, thisArg?: any, ...args: any[]) => T | |
| } | |
| function createChannel(): Channel { | |
| return { | |
| hasSubscribers: false, | |
| subscribe() {}, | |
| unsubscribe() {}, | |
| publish() {}, | |
| bindStore() {}, | |
| unbindStore() {}, | |
| runStores(_context, fn, thisArg, ...args) { | |
| return fn.apply(thisArg, args) | |
| } | |
| } | |
| } | |
| function createTracingChannel(): TracingChannel { | |
| return { | |
| hasSubscribers: false, | |
| subscribe() {}, | |
| unsubscribe() {}, | |
| traceSync(fn, _context, thisArg, ...args) { | |
| return fn.apply(thisArg, args) | |
| }, | |
| tracePromise(fn, _context, thisArg, ...args) { | |
| return fn.apply(thisArg, args) | |
| }, | |
| traceCallback(fn, _position, _context, thisArg, ...args) { | |
| return fn.apply(thisArg, args) | |
| } |
|
Taking a look! |
Summary
Three user-visible improvements:
gemini:antigravity), usingretrieveUserQuotaSummary+fetchAvailableModelswithideType: ANTIGRAVITY. Status-bar bars stay durable viaantigravityAuthConfigured(auth present, not path-gated). Compact mode shows the 5h window only; merge restores previously seen windows without inventing 100% placeholders.zh-TW.jsoncatalog kept in parity with other locales.node:diagnostics_channelso@xterm/addon-ligatures→lru-cache@11no longer hits Vite's empty browser external stub (dev whitescreen and production build failure).Screenshots
Testing
pnpm lint— oxlint clean for this change set; locallint:switch-exhaustivenessalso fails on unrelatedskill-freshness-group.tsxon cleanmainwith Node 22 (wanted Node 24). Please treat as environment noise unless CI reproduces.pnpm typecheckpnpm test(targeted): Antigravity unit tests +service.test.ts+ status-bar visibility + locale/language tests — 116 related tests passed. Windows Claude PTY supplement expectations inservice.test.tsmade platform-aware so they match existingwin32service behavior.pnpm build/electron-vite build— passes with diagnostics-channel shim.verify:localization-catalog/verify:localization-coverage— passAI Review Report
Adversarial review was run before implementation, then re-checked after code changes:
unsupported/ no durable bars — no fake data. zh-TW and diagnostics shim are cross-platform. Shortcuts/labels/paths not changed except existing platform patterns.ANTIGRAVITY_CLIENT_ID/ANTIGRAVITY_CLIENT_SECRETenv for refresh. Tokens stay in Credential Manager / memory during fetch. No secrets in repo.Flags that were fixed during review: no 100% placeholder merge; durability via auth not PATH; split fetcher to satisfy max-lines without disable comments.
Security Audit
gemini:antigravityvia PowerShell; blob parsed for access/refresh tokens only. No credential writes.oauth2.googleapis.com/token. Missing client env still allows in-window access tokens from CM.antigravityAuthConfiguredis boolean. Diagnostics shim is a no-op in-memory channel with no Node FS/network.Notes
ANTIGRAVITY_CLIENT_ID/ANTIGRAVITY_CLIENT_SECRETfor users whose access tokens are expired; fresh CM access tokens work without them.