Skip to content

feat: real Antigravity usage, zh-TW UI, and renderer diagnostics shim#9360

Open
RX5950XT wants to merge 1 commit into
stablyai:mainfrom
RX5950XT:feat/antigravity-usage-zh-tw
Open

feat: real Antigravity usage, zh-TW UI, and renderer diagnostics shim#9360
RX5950XT wants to merge 1 commit into
stablyai:mainfrom
RX5950XT:feat/antigravity-usage-zh-tw

Conversation

@RX5950XT

Copy link
Copy Markdown

Summary

Three user-visible improvements:

  1. Antigravity usage (status bar) — Stop mirroring Gemini CLI OAuth usage under the Antigravity provider. Fetch real Cloud Code quota from Antigravity credentials (Windows Credential Manager target gemini:antigravity), using retrieveUserQuotaSummary + fetchAvailableModels with ideType: ANTIGRAVITY. Status-bar bars stay durable via antigravityAuthConfigured (auth present, not path-gated). Compact mode shows the 5h window only; merge restores previously seen windows without inventing 100% placeholders.
  2. Traditional Chinese (zh-TW) — First-class UI language for settings / locale selection, with a full zh-TW.json catalog kept in parity with other locales.
  3. Renderer whitescreen / production build fix — Shim node:diagnostics_channel so @xterm/addon-ligatureslru-cache@11 no longer hits Vite's empty browser external stub (dev whitescreen and production build failure).

Screenshots

  • UI change: Status bar Antigravity usage (compact 5h bars + tooltip) and Appearance language picker including 繁體中文.
  • Verified live on Windows with Antigravity signed in; bars and zh-TW switching behaved correctly.
  • No image files attached in this PR body (happy to add if maintainers prefer).

Testing

  • pnpm lint — oxlint clean for this change set; local lint:switch-exhaustiveness also fails on unrelated skill-freshness-group.tsx on clean main with Node 22 (wanted Node 24). Please treat as environment noise unless CI reproduces.
  • pnpm typecheck
  • pnpm test (targeted): Antigravity unit tests + service.test.ts + status-bar visibility + locale/language tests — 116 related tests passed. Windows Claude PTY supplement expectations in service.test.ts made platform-aware so they match existing win32 service behavior.
  • pnpm build / electron-vite buildpasses with diagnostics-channel shim.
  • verify:localization-catalog / verify:localization-coveragepass
  • Added high-quality tests for credential parse/expiry, quota aggregation, usage fetcher, service independence from Gemini, and status-bar visibility auth durability.

AI Review Report

Adversarial review was run before implementation, then re-checked after code changes:

Risk Result
Cross-platform (macOS / Linux / Windows) Usage fetch is intentionally Windows-only today (Credential Manager). Non-Windows returns unsupported / no durable bars — no fake data. zh-TW and diagnostics shim are cross-platform. Shortcuts/labels/paths not changed except existing platform patterns.
SSH / remote / local Antigravity credentials are read from the desktop host Credential Manager (same as other local OAuth quota sources). No assumption that remote worktrees own Antigravity login.
Agents / integrations Antigravity usage is independent of Gemini CLI OAuth and of Antigravity agent hooks. Provider-neutral status-bar plumbing; Antigravity-specific logic is guarded by provider id / auth probe.
Performance One extra parallel fetch in the rate-limit poll cycle; short HTTP timeout; no hot-path UI work.
UI quality Compact bar shows 5h only to avoid overcrowding; weekly remains in tooltip. Tokens/styles unchanged beyond locale registration.
Security OAuth client id/secret not hardcoded — only ANTIGRAVITY_CLIENT_ID / ANTIGRAVITY_CLIENT_SECRET env 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

  • Credentials: Read-only CredRead of Windows Generic credential gemini:antigravity via PowerShell; blob parsed for access/refresh tokens only. No credential writes.
  • Token refresh: Optional env-based OAuth client; refresh token sent only to oauth2.googleapis.com/token. Missing client env still allows in-window access tokens from CM.
  • Network: HTTPS to Google Cloud Code PA endpoints only; Bearer tokens; 15s abort timeout.
  • IPC / renderer: Reuses existing rate-limit state push; new field antigravityAuthConfigured is boolean. Diagnostics shim is a no-op in-memory channel with no Node FS/network.
  • Secrets: No hardcoded OAuth client secrets, tokens, or cookies in source.
  • Follow-up: macOS/Linux credential stores for Antigravity usage if product wants parity later.

Notes

  • Platform: Antigravity usage is Windows-only for now (matches Token-Anxiety-Dashboard). macOS/Linux: no bars until a credential backend exists.
  • Token refresh in packaging may require setting ANTIGRAVITY_CLIENT_ID / ANTIGRAVITY_CLIENT_SECRET for users whose access tokens are expired; fresh CM access tokens work without them.
  • This PR combines three related desktop polish items; maintainers may request a split if preferred.
  • Contributor: @RX5950XT

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.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The 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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the three main changes in the PR.
Description check ✅ Passed The description follows the required template and covers summary, screenshots, testing, review, security, and notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 70a679a0-d0ae-4de3-8db5-c881d393697d

📥 Commits

Reviewing files that changed from the base of the PR and between 54cb772 and 1b8c016.

📒 Files selected for processing (36)
  • electron.vite.config.ts
  • src/main/i18n/main-i18n.ts
  • src/main/rate-limits/antigravity-api-client.ts
  • src/main/rate-limits/antigravity-credentials.test.ts
  • src/main/rate-limits/antigravity-credentials.ts
  • src/main/rate-limits/antigravity-quota-aggregation.test.ts
  • src/main/rate-limits/antigravity-quota-aggregation.ts
  • src/main/rate-limits/antigravity-usage-fetcher.test.ts
  • src/main/rate-limits/antigravity-usage-fetcher.ts
  • src/main/rate-limits/service.test.ts
  • src/main/rate-limits/service.ts
  • src/renderer/src/assets/main.css
  • src/renderer/src/components/settings/appearance-search.ts
  • src/renderer/src/components/stats/GrokUsagePane.test.tsx
  • src/renderer/src/components/status-bar/StatusBar.tsx
  • src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts
  • src/renderer/src/components/status-bar/status-bar-provider-visibility.ts
  • src/renderer/src/components/status-bar/tooltip.tsx
  • src/renderer/src/i18n/I18nProvider.tsx
  • src/renderer/src/i18n/i18n.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh-TW.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/supported-languages.ts
  • src/renderer/src/shims/diagnostics-channel.ts
  • src/renderer/src/store/slices/rate-limits.ts
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/rate-limit-types.test.ts
  • src/shared/rate-limit-types.ts
  • src/shared/ui-language.test.ts
  • src/shared/ui-language.ts
  • src/shared/ui-locale.test.ts
  • src/shared/ui-locale.ts

Comment on lines +178 to +190
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 as unauthorized; 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

Comment on lines +163 to +175
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
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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
}
}

Comment on lines +291 to +323
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +1382 to +1383
const antigravityAuthReadResult = await readAntigravityCredentials()
this.antigravityAuthConfigured = probeAntigravityAuthConfigured(antigravityAuthReadResult)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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

Comment on lines +12 to +51
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()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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:

  1. runStores expects (context, fn[, thisArg[, ...args]]) in Node.js, but the shim expects fn as the first argument. Calling it with a context object will throw TypeError: _context is not a function.
  2. traceSync, tracePromise, and traceCallback drop thisArg and ...args. If a library passes these arguments, fn will 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.

Suggested change
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)
}

@brennanb2025

Copy link
Copy Markdown
Contributor

Taking a look!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants