Skip to content

Conversation

@NoeFabris
Copy link
Owner

@NoeFabris NoeFabris commented Feb 9, 2026

Summary

  • extract the auth login menu UI rework from fix: improve blocked-account verification UX and recovery flow #395 into a focused change (section headings, clearer account labels, improved terminal selection UX)
  • add verify-account and verify-all actions in the auth flow so you can run verification probes from the menu
  • when runtime requests return VALIDATION_REQUIRED, automatically disable that account, persist verification-required metadata, switch away from it, and show a guiding toast
  • surface verification-required responses with extracted Google verification URLs and optional browser open during manual verification

Linked Issues

Testing

  • npm run typecheck
  • npm test
  • npm run build

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Walkthrough

Adds a Google account verification subsystem (types, URL normalization, verification request/response handling, token refresh, and browser-open helpers). Integrates verification state into account storage and AccountManager (fields, persistence, mark/clear methods). Embeds verification flows into CLI and plugin UI (new "verify" and "verify-all" actions, prompts, per-account verification handling, and optional browser opening). Extends select UI with non-selectable headings, ANSI-safe truncation, optional screen clearing, and windowed rendering. Also updates several UI prompts and account-action flows. Lines changed: +871/-70.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'feat: add account-specific verification action in auth menu' accurately reflects the main change: adding verification actions to the auth menu UI.
Description check ✅ Passed The description comprehensively explains the PR's objectives: extracting UI rework, adding verify actions, handling VALIDATION_REQUIRED responses, and surfacing verification guidance.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from #335: detects VALIDATION_REQUIRED responses, marks accounts as verification-required, automatically disables affected accounts, switches to alternative accounts, and surfaces verification URLs to users.
Out of Scope Changes check ✅ Passed The PR includes changes to UI components (select.ts, auth-menu.ts) and account management (accounts.ts, storage.ts) that extend beyond the immediate #335 requirements but are justified by the PR's stated objective to extract #395 UI rework alongside verification handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/plugin/ui/select.ts (1)

200-206: Window hint is appended only when using the default help text.

When options.help is set, the windowHint is computed but never shown (line 203 short-circuits via ??). If a caller supplies custom help but still wants position hints for long lists, they'd have to include it themselves. This is probably fine for now but worth noting.

src/plugin.ts (3)

317-435: extractVerificationErrorDetails is thorough but consider decomposing for maintainability.

This function (~120 lines) handles plain text, full JSON, SSE data: lines, recursive object walking with cycle detection, regex URL extraction, and multiple fallback heuristics. It works correctly but is dense. Extracting helpers like parseSSEPayloads(body), walkForVerificationFields(obj), and fallbackTextDetection(body) would improve readability and testability.


2745-2831: verify-all flow is well-implemented with incremental progress output.

The sequential per-account verification with inline status updates (process.stdout.write followed by console.log) gives good user feedback. Storage is saved once at the end (line 2809) rather than per-account, which is efficient. The summary with blocked URLs is helpful for user action.

One minor note: the storageUpdated flag on line 2759 shadows the outer storageUpdated from the quota-check block (line 2603). Since they're in different if branches within the same while loop, there's no actual conflict, but it's a bit confusing. Consider renaming one for clarity.


2869-2903: Account is marked as verification-required even when the user completes browser verification in the same session.

After the user opens the verification URL in their browser and presumably completes verification (lines 2892–2899), the account remains marked as verificationRequired and disabled. The user would need to re-run the verify action to clear the state. This is actually fine — verification is async and the probe can't know if the user completed it in the browser. But consider adding a hint like "Run verify again after completing the browser step" to guide the user.

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 29b1a5b and eaf8175.

📒 Files selected for processing (2)
  • src/plugin.ts
  • src/plugin/ui/select.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/plugin/ui/select.ts (1)
src/plugin/ui/ansi.ts (1)
  • ANSI (6-25)
src/plugin.ts (4)
src/plugin/types.ts (1)
  • PluginClient (42-42)
src/plugin/auth.ts (2)
  • parseRefreshParts (12-19)
  • formatRefreshParts (24-28)
src/plugin/token.ts (2)
  • refreshAccessToken (85-169)
  • AntigravityTokenRefreshError (60-80)
src/constants.ts (3)
  • ANTIGRAVITY_DEFAULT_PROJECT_ID (71-71)
  • ANTIGRAVITY_HEADERS (94-98)
  • ANTIGRAVITY_ENDPOINT_PROD (34-34)
🔇 Additional comments (12)
src/plugin/ui/select.ts (4)

9-10: Clean extension of the public API for headings and display customization.

The kind?: 'heading' property and the new SelectOptions fields (help, clearScreen) are well-designed additions that integrate cleanly with the existing item model. The JSDoc annotations are helpful.

Also applies to: 17-23


28-69: ANSI utilities are solid for the use case, with a minor robustness note.

ANSI_REGEX only matches SGR sequences (\x1b[…m). This is fine for stripping color/style codes, but won't catch cursor-movement or erase sequences if they ever appear in label text. Given that items are authored internally, this is acceptable.

truncateAnsi correctly walks the string preserving escape sequences while counting visible characters. The reset-append logic on line 64 is a good safeguard against dangling open styles.


130-145: Windowed rendering math looks correct.

fixedLines correctly accounts for header (1) + subtitle block (0 or 3) + help + footer (2). The -1 safety margin on line 135 prevents a trailing newline from scrolling the viewport. Windowing around the cursor with clamped bounds is straightforward and correct.


253-261: findNextSelectable correctly skips headings; termination is guaranteed.

The pre-check at lines 95–101 ensures ≥ 2 selectable items exist before this code is reachable, so the do-while will always find a selectable item and terminate.

src/plugin.ts (8)

270-274: Clean type definition for verification probe results.

The three-state discriminator (ok | blocked | error) with optional verifyUrl is a clear and minimal API.


276-315: URL normalization and selection helpers are well-designed.

normalizeGoogleVerificationUrl correctly validates against accounts.google.com which prevents open-redirect misuse. selectBestVerificationUrl deduplicates and scores with a reasonable heuristic.


437-547: Verification probe implementation is solid; minor note on quota impact.

The probe uses maxOutputTokens: 1 and temperature: 0 to minimize cost — good. The 20-second abort timeout (line 503) prevents hanging. One thing to be aware of: each verification probe is a real API call that counts against quota, so verify-all with many accounts could consume a non-trivial number of requests. This is acceptable given it's a manual user action, just worth noting in user-facing docs.


549-584: Infinite input loop in promptAccountIndexForVerification lacks a bail-out on EOF.

If stdin closes (e.g., piped input ends), rl.question() will reject or return '', and the !answer check on line 565 will return undefined, which safely cancels. So this is actually handled — the empty-string bail-out covers EOF. Looks good.


599-667: markStoredAccountVerificationRequired / clearStoredAccountVerificationRequired are well-structured.

The changed tracking is a smart optimization to avoid unnecessary disk writes. clearStoredAccountVerificationRequired correctly guards re-enabling behind enableIfRequired && wasVerificationRequired to avoid unintended side-effects on accounts disabled for other reasons.

One subtlety: clearStoredAccountVerificationRequired sets verificationRequired to false (line 645) rather than undefined/delete. This means the property will persist in serialized storage. If storage schema doesn't expect false values, downstream consumers should check === true rather than truthiness. The rest of the codebase (line 2565) does check acc.verificationRequired with truthiness — which would correctly treat false as non-verification-required. So this is fine.


2255-2295: 403 VALIDATION_REQUIRED detection in the hot path looks correct.

The flow properly:

  1. Clones the response to read the body without consuming it (line 2256)
  2. Extracts verification details
  3. Disables the account with a 10-minute cooldown
  4. Shows a debounced toast to avoid spam
  5. Switches to the next available account

One minor observation: lines 2250–2253 reset rate-limit/failure state (the "success" path), and then lines 2263–2265 re-mark the account as rate-limited. This is redundant but harmless — the reset is immediately overwritten.


2563-2585: Account status now correctly surfaces the verification-required state.

Good addition of the verification-required status in the account mapping, and the logic correctly prioritizes it over rate-limit/active checks (line 2565–2567 checks verificationRequired first before examining rate limits).


2833-2907: Single-account verify flow: good UX with optional browser open.

The flow correctly handles all three verification states, offers to open the browser for blocked accounts, and re-enables accounts on successful verification. The fallback to account.verificationUrl (line 2884) when the fresh probe doesn't return a URL is a nice touch for resilience.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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 and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR reworks the interactive auth login menu to add section headings, clearer per-account labels, and verification actions (verify one / verify all). It also introduces a verification probe that refreshes an account�s token and makes a small Antigravity request to detect VALIDATION_REQUIRED, then persists a new verificationRequired* metadata block (and disables the account) so rotation can avoid it and the UI can surface �needs verification�.

The main integration is in src/plugin.ts, where runtime 403 responses are inspected for VALIDATION_REQUIRED and cause the account to be disabled + marked verification-required, and where the auth login flow can run the probe(s) on demand and optionally open a returned Google verification URL in a browser.

Merge blockers found:

  • openBrowser(url) executes shell commands with an untrusted URL interpolated from remote response bodies, which is a command-injection vector.
  • Verification-required handling is additionally recorded as a rate-limit/cooldown state, which can misclassify the condition in selection/status logic.
  • AccountManager.markAccountVerificationRequired() rewrites verificationRequiredAt on every call, causing churn/extra persistence even when the account is already in that state.

Confidence Score: 2/5

  • This PR should not be merged until the browser-opening path is made safe against command injection and verification-required state handling is corrected.
  • The new verify flows can surface a URL extracted from remote API responses and pass it into exec() via openBrowser(), which is a concrete RCE vector for users who accept the prompt. Additionally, verification-required is currently recorded as both a disable/verification flag and a rate-limit/cooldown, which can misclassify state in rotation/status code. Aside from these issues, the UI/menu changes and schema additions appear consistent.
  • src/plugin.ts, src/plugin/accounts.ts

Important Files Changed

Filename Overview
src/plugin.ts Adds verification probing/extraction + auth-menu verify actions and persists verification-required state; introduces a command injection risk via openBrowser(url) and conflates verification-required with rate-limit state in request handling.
src/plugin/accounts.ts Extends managed account metadata with verification-required fields and adds mark/clear helpers; markAccountVerificationRequired currently rewrites verificationRequiredAt on every call causing churn.
src/plugin/cli.ts Extends login menu modes to include verify/verify-all and wires auth-menu actions to these modes; no functional issues found.
src/plugin/storage.ts Adds validation-required cooldown reason and persists verification-required metadata in account storage schema; changes look consistent with existing v3 storage handling.
src/plugin/ui/auth-menu.ts Reworks auth menu UI to add verify actions, headings, and improved account labels/status badges (including verification-required).
src/plugin/ui/select.ts Adds headings, help text, clear-screen option, viewport-windowed rendering, and ANSI-aware truncation/reset logic to avoid wrapping issues (prior ANSI-bleed thread already noted).

Sequence Diagram

sequenceDiagram
  autonumber
  participant User as User (CLI)
  participant Menu as auth-menu/select
  participant Plugin as createAntigravityPlugin
  participant Storage as antigravity-accounts.json
  participant AM as AccountManager
  participant API as Antigravity API
  participant Browser as System browser

  User->>Menu: opencode auth login
  Menu->>Plugin: promptLoginMode(existingAccounts)

  alt Verify one account
    Menu-->>Plugin: {mode:"verify", verifyAccountIndex?}
    Plugin->>Plugin: verifyAccountAccess(account)
    Plugin->>API: streamGenerateContent alt=sse (ping)
    API-->>Plugin: 200 OK
    Plugin->>Plugin: clearStoredAccountVerificationRequired(enable=true)
    Plugin->>Storage: saveAccounts(if changed)
    Plugin->>AM: clearAccountVerificationRequired(index, wasRequired)
  else Verify blocked (403 VALIDATION_REQUIRED)
    API-->>Plugin: 403 + body
    Plugin->>Plugin: extractVerificationErrorDetails(body)
    Plugin->>Plugin: markStoredAccountVerificationRequired(enabled=false, metadata)
    Plugin->>Storage: saveAccounts(if changed)
    Plugin->>AM: markAccountVerificationRequired(index, reason, url)
    opt Open URL
      Plugin->>User: promptOpenVerificationUrl()
      User-->>Plugin: yes
      Plugin->>Browser: openBrowser(verifyUrl)
    end
  end

  opt Runtime request path
    Plugin->>API: fetch(prepared.request)
    API-->>Plugin: 403 VALIDATION_REQUIRED
    Plugin->>Plugin: extractVerificationErrorDetails(body)
    Plugin->>AM: markAccountVerificationRequired(...)
    Plugin->>AM: markAccountCoolingDown(..., "validation-required")
    Plugin->>AM: markRateLimited(...)
    Plugin-->>Plugin: switch to next account
  end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 9, 2026

Additional Comments (2)

src/plugin/ui/auth-menu.ts
Removed manage action

showAuthMenu no longer returns { type: 'manage' }, but promptLoginMode still routes 'toggle' account actions to { mode: "manage" } (src/plugin/cli.ts:134). This makes the TTY menu inconsistent with the non-TTY fallback (which can no longer select manage) and effectively removes the ability to enter “manage” from the main menu. If “manage” is still intended, re-add a Manage entry in AuthMenuAction/menu items; otherwise remove the manage mode path (and any callers) so toggle/enable-disable remains reachable in both flows.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/ui/auth-menu.ts
Line: 17:21

Comment:
**Removed manage action**

`showAuthMenu` no longer returns `{ type: 'manage' }`, but `promptLoginMode` still routes `'toggle'` account actions to `{ mode: "manage" }` (`src/plugin/cli.ts:134`). This makes the TTY menu inconsistent with the non-TTY fallback (which can no longer select manage) and effectively removes the ability to enter “manage” from the main menu. If “manage” is still intended, re-add a Manage entry in `AuthMenuAction`/menu items; otherwise remove the `manage` mode path (and any callers) so toggle/enable-disable remains reachable in both flows.

How can I resolve this? If you propose a fix, please make it concise.

src/plugin/cli.ts
Manage option lost in fallback

In promptLoginModeFallback the prompt removed the (m)anage option and no longer returns { mode: "manage" }, but LoginMode still includes "manage" and the TTY flow still supports toggling accounts via { mode: "manage", toggleAccountIndex } (src/plugin/cli.ts:134 + src/plugin.ts:2607). This means non-TTY environments can’t enable/disable accounts anymore, which is a functional regression for CI/remote shells and pipes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/cli.ts
Line: 60:64

Comment:
**Manage option lost in fallback**

In `promptLoginModeFallback` the prompt removed the `(m)anage` option and no longer returns `{ mode: "manage" }`, but `LoginMode` still includes `"manage"` and the TTY flow still supports toggling accounts via `{ mode: "manage", toggleAccountIndex }` (`src/plugin/cli.ts:134` + `src/plugin.ts:2607`). This means non-TTY environments can’t enable/disable accounts anymore, which is a functional regression for CI/remote shells and pipes.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +64 to +66
return out + suffix;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

ANSI truncation breaks styling

truncateAnsi() appends the suffix without ensuring ANSI styles are reset, so a truncated colored label can "bleed" its color/bold into the rest of the menu (including following lines). This will happen whenever an item/message/subtitle is longer than the terminal width.

Consider appending ANSI.reset before the suffix (or tracking open SGR state) so truncated strings always end in a reset.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/ui/select.ts
Line: 64:66

Comment:
**ANSI truncation breaks styling**

`truncateAnsi()` appends the suffix without ensuring ANSI styles are reset, so a truncated colored label can "bleed" its color/bold into the rest of the menu (including following lines). This will happen whenever an item/message/subtitle is longer than the terminal width.

Consider appending `ANSI.reset` before the suffix (or tracking open SGR state) so truncated strings always end in a reset.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Additional Comments (1)

src/plugin.ts
Wrong enabled flag saved

existingAccounts passes enabled: acc.enabled !== false, which flips the meaning (it’s true when enabled). But promptLoginMode() forwards that into AccountInfo.enabled, and showAuthMenu() treats enabled === false as disabled. With the current mapping, disabled accounts will be displayed as enabled and vice‑versa, and the "[disabled]" badge will be wrong.

This should pass through the stored value (acc.enabled) or normalize consistently across UI and storage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 2580:2582

Comment:
**Wrong enabled flag saved**

`existingAccounts` passes `enabled: acc.enabled !== false`, which flips the meaning (it’s `true` when enabled). But `promptLoginMode()` forwards that into `AccountInfo.enabled`, and `showAuthMenu()` treats `enabled === false` as disabled. With the current mapping, disabled accounts will be displayed as enabled and vice‑versa, and the "[disabled]" badge will be wrong.

This should pass through the stored value (`acc.enabled`) or normalize consistently across UI and storage.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +364 to +368
const walk = (value: unknown, key?: string): void => {
if (typeof value === "string") {
const normalizedValue = decodeEscapedText(value);
const lowerValue = normalizedValue.toLowerCase();
const lowerKey = key?.toLowerCase() ?? "";
Copy link
Contributor

Choose a reason for hiding this comment

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

Verification timestamp always changes

markStoredAccountVerificationRequired() compares verificationRequiredAt to a freshly computed Date.now() and will therefore mark the account as changed on every call (and continually rewrite storage), even if the account was already in verificationRequired state with the same reason/url. This makes storageUpdated effectively always true in the verify flows and can cause unnecessary disk writes.

A simple fix is to only set verificationRequiredAt when transitioning from not-required → required (or if it was previously unset), rather than every time the function runs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 364:368

Comment:
**Verification timestamp always changes**

`markStoredAccountVerificationRequired()` compares `verificationRequiredAt` to a freshly computed `Date.now()` and will therefore mark the account as changed on every call (and continually rewrite storage), even if the account was already in `verificationRequired` state with the same reason/url. This makes `storageUpdated` effectively always true in the verify flows and can cause unnecessary disk writes.

A simple fix is to only set `verificationRequiredAt` when transitioning from not-required → required (or if it was previously unset), rather than every time the function runs.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +303 to +336
unique.sort((a, b) => {
const score = (value: string): number => {
let total = 0;
if (value.includes("plt=")) total += 4;
if (value.includes("/signin/continue")) total += 3;
if (value.includes("continue=")) total += 2;
if (value.includes("service=cloudcode")) total += 1;
return total;
};
return score(b) - score(a);
});
return unique[0];
}

function extractVerificationErrorDetails(bodyText: string): {
validationRequired: boolean;
message?: string;
verifyUrl?: string;
} {
const decodedBody = decodeEscapedText(bodyText);
const lowerBody = decodedBody.toLowerCase();
let validationRequired = lowerBody.includes("validation_required");
let message: string | undefined;
const verificationUrls = new Set<string>();

const collectUrlsFromText = (text: string): void => {
for (const match of text.matchAll(/https:\/\/accounts\.google\.com\/[^\s"'<>]+/gi)) {
if (match[0]) {
verificationUrls.add(match[0]);
}
}
};

collectUrlsFromText(decodedBody);
Copy link
Contributor

Choose a reason for hiding this comment

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

Verify prompt index mismatch

promptAccountIndexForVerification() returns the 0-based index the user typed minus 1, but it’s passed existingAccounts built from existingStorage.accounts.map((acc, idx) => ({ index: idx, ... })), and the selection flow later uses that returned value to index existingStorage.accounts. If existingAccounts is ever filtered/reordered (e.g. non-account rows, future sorting), this will verify the wrong account. To keep this robust, validate/resolve the selection by the stored account.index (or pass the full existingStorage.accounts and use its indices directly), rather than relying on list position.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 303:336

Comment:
**Verify prompt index mismatch**

`promptAccountIndexForVerification()` returns the 0-based index the user typed minus 1, but it’s passed `existingAccounts` built from `existingStorage.accounts.map((acc, idx) => ({ index: idx, ... }))`, and the selection flow later uses that returned value to index `existingStorage.accounts`. If `existingAccounts` is ever filtered/reordered (e.g. non-account rows, future sorting), this will verify the wrong account. To keep this robust, validate/resolve the selection by the stored `account.index` (or pass the full `existingStorage.accounts` and use its indices directly), rather than relying on list position.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugin.ts (1)

244-268: ⚠️ Potential issue | 🟡 Minor

Potential command injection in openBrowser with URL arguments.

openBrowser passes the URL directly into shell commands via exec() with string interpolation (e.g., exec(`open "${url}"`)). While this function is pre-existing, this PR introduces a new call path at line 2892 where the URL originates from API error responses (via extractVerificationErrorDetailsselectBestVerificationUrl).

Although normalizeGoogleVerificationUrl validates that the hostname is accounts.google.com, the URL's query string could theoretically contain shell metacharacters (e.g., $, backticks) that survive URL.toString(). The risk is mitigated by the hostname validation, but a safer approach would use execFile with argument arrays instead of shell interpolation.

Safer alternative for openBrowser
+import { execFile } from "node:child_process";
+
 async function openBrowser(url: string): Promise<boolean> {
   try {
     if (process.platform === "darwin") {
-      exec(`open "${url}"`);
+      execFile("open", [url]);
       return true;
     }
     if (process.platform === "win32") {
-      exec(`start "" "${url}"`);
+      execFile("cmd", ["/c", "start", "", url]);
       return true;
     }
     // ... similar for other platforms

Also applies to: 2889-2898

🤖 Fix all issues with AI agents
In `@src/plugin/storage.ts`:
- Line 180: Rename the CooldownReason variant from "validation-required" to
"verification-required" in the CooldownReason type declaration (symbol:
CooldownReason) and update all places that construct or compare that string
(e.g., the place in plugin.ts that currently emits/compares
"validation-required") so they use "verification-required" consistently; ensure
any switch/case, equality checks, stored values, and user-facing mappings are
updated to the new literal to keep type and runtime strings aligned.
🧹 Nitpick comments (6)
src/plugin/accounts.ts (1)

1015-1018: Consider omitting falsy verification fields from persisted storage.

Currently verificationRequired: false and undefined values for the other fields will be serialized. Other fields in this mapping use conditional expressions (e.g., line 1008 for rateLimitResetTimes). For consistency and to avoid storing noise:

Optional cleanup
-        verificationRequired: a.verificationRequired,
-        verificationRequiredAt: a.verificationRequiredAt,
-        verificationRequiredReason: a.verificationRequiredReason,
-        verificationUrl: a.verificationUrl,
+        verificationRequired: a.verificationRequired || undefined,
+        verificationRequiredAt: a.verificationRequiredAt,
+        verificationRequiredReason: a.verificationRequiredReason,
+        verificationUrl: a.verificationUrl,
src/plugin.ts (5)

317-435: extractVerificationErrorDetails is thorough but complex — consider a brief doc comment.

The function handles multiple response formats (plain JSON, SSE data: lines, nested structures) with recursive walking. The visited set (line 363) correctly prevents infinite traversal, and the broad lowerKey === "url" match (line 383) is safely gated by normalizeGoogleVerificationUrl downstream.

One subtle behavior: line 374 captures only the first string value with a message-like key (!message && ...). If the most descriptive message appears after a less useful one in the walk order (object key iteration order), it will be missed. This is an acceptable trade-off for simplicity.


437-547: Hardcoded model "gemini-3-flash" in verification probe — consider extracting as a constant.

The verification probe (lines 494-499) hardcodes the model name "gemini-3-flash" in two places within the request body. If this model is renamed or deprecated, the verification check will silently break (returning "error" instead of the true verification status).

Suggested extraction
+const VERIFICATION_PROBE_MODEL = "gemini-3-flash";
+
 async function verifyAccountAccess(
   // ...
 ): Promise<VerificationProbeResult> {
   // ...
   const requestBody = {
-    model: "gemini-3-flash",
+    model: VERIFICATION_PROBE_MODEL,
     request: {
-      model: "gemini-3-flash",
+      model: VERIFICATION_PROBE_MODEL,
       contents: [{ role: "user", parts: [{ text: "ping" }] }],
       generationConfig: { maxOutputTokens: 1, temperature: 0 },
     },
   };

590-666: Duplication of verification state management logic between plugin.ts and accounts.ts.

markStoredAccountVerificationRequired / clearStoredAccountVerificationRequired in plugin.ts duplicate the logic in AccountManager.markAccountVerificationRequired / clearAccountVerificationRequired in accounts.ts. The plugin.ts versions operate on plain storage objects while accounts.ts versions operate on ManagedAccount instances.

This duplication is understandable since the login flow works with raw storage before the AccountManager is initialized, but it creates a maintenance risk — changes to verification logic must be synchronized in two places. Consider extracting shared pure-logic helpers that both can call, or documenting the relationship.


598-634: markStoredAccountVerificationRequired: timestamp comparison is always true.

Line 611: if (account.verificationRequiredAt !== timestamp)timestamp is freshly obtained from Date.now() on line 610, so this will virtually always differ from the stored value. The changed flag tracking here adds complexity without meaningful optimization. This is not a bug, but the granular change-tracking for verificationRequiredAt is effectively a no-op guard.


2744-2907: Verify flow is well-implemented — one readability suggestion.

The verify-all and verify-single paths are comprehensive and correctly sync both storage and in-memory state. A minor readability improvement: this ~160-line block could be extracted into helper functions (e.g., handleVerifyAll, handleVerifySingle) to reduce nesting depth in the already-large authorize method.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8118348 and 29b1a5b.

📒 Files selected for processing (4)
  • src/plugin.ts
  • src/plugin/accounts.ts
  • src/plugin/storage.ts
  • src/plugin/ui/auth-menu.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/plugin/storage.ts (1)
src/plugin/accounts.ts (1)
  • CooldownReason (12-12)
src/plugin.ts (6)
src/plugin/types.ts (1)
  • PluginClient (42-42)
src/plugin/auth.ts (2)
  • parseRefreshParts (12-19)
  • formatRefreshParts (24-28)
src/plugin/token.ts (1)
  • AntigravityTokenRefreshError (60-80)
src/constants.ts (3)
  • ANTIGRAVITY_DEFAULT_PROJECT_ID (71-71)
  • ANTIGRAVITY_HEADERS (94-98)
  • ANTIGRAVITY_ENDPOINT_PROD (34-34)
src/plugin/rotation.ts (1)
  • getHealthTracker (441-446)
src/plugin/storage.ts (1)
  • saveAccounts (651-677)
src/plugin/ui/auth-menu.ts (2)
src/plugin/ui/ansi.ts (1)
  • ANSI (6-25)
src/plugin/ui/select.ts (2)
  • MenuItem (3-12)
  • select (77-312)
🔇 Additional comments (10)
src/plugin/storage.ts (1)

196-200: LGTM — verification fields on AccountMetadataV3.

The new optional fields (verificationRequired, verificationRequiredAt, verificationRequiredReason, verificationUrl) are well-typed, properly optional, and align with the ManagedAccount additions in accounts.ts and the persistence logic in saveToDisk.

src/plugin/accounts.ts (3)

166-169: LGTM — ManagedAccount verification fields.

Fields correctly mirror AccountMetadataV3 and are properly optional.


383-386: LGTM — Verification fields loaded from storage.

Straight-through mapping from stored metadata to in-memory account.


827-876: LGTM — markAccountVerificationRequired and clearAccountVerificationRequired are well-structured.

Both methods correctly guard against invalid indices, update state atomically, and trigger persistence through setAccountEnabled or requestSaveToDisk as appropriate. The clearAccountVerificationRequired method's conditional re-enable logic (only when enableAccount is true and the account was previously verification-blocked) is a nice touch.

One minor observation: clearAccountVerificationRequired sets verificationRequired = false (line 864) instead of undefined, which means "verificationRequired": false will be serialized to the JSON file. This is harmless but adds a small amount of noise to the stored data compared to omitting the key entirely.

src/plugin/ui/auth-menu.ts (3)

56-65: LGTM — Menu restructuring with section headings.

The use of kind: 'heading' for non-selectable section titles and separators between logical groups is clean and aligns with the select.ts contract. The dummy { type: 'cancel' } value for non-selectable items is a reasonable pattern given the MenuItem<T> interface requires a value.


68-74: LGTM — Enhanced account label rendering.

Numbered labels with status badges provide good visual clarity. The ordering (number, name, current badge, status badge, disabled badge) reads naturally.


106-127: LGTM — showAccountDetails with composite header and verify action.

The header/subtitle restructuring improves context visibility, and the new "Verify account access" action integrates naturally.

src/plugin.ts (3)

270-315: LGTM — Verification URL types and normalization.

normalizeGoogleVerificationUrl correctly validates the hostname to prevent open-redirect attacks, and selectBestVerificationUrl uses a reasonable scoring heuristic for selecting the most useful verification URL.


2254-2294: Verification-required handling on 403 looks correct.

The flow correctly:

  1. Clones the response to read the body without consuming the original
  2. Uses the extracted details to mark the account
  3. Applies a 10-minute cooldown and rate limit
  4. Switches to another account

One consideration: the 10-minute hardcoded cooldown (line 2260) means users who complete verification quickly still have to wait or manually re-enable through the auth menu. Since clearAccountVerificationRequired can re-enable the account, and the verify flow is now available, this seems like an acceptable trade-off.


2562-2584: LGTM — Verification status takes priority over rate-limited status.

The if (acc.verificationRequired) check before the rate-limit status determination correctly surfaces the root cause rather than the downstream cooldown.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

}

export type CooldownReason = "auth-failure" | "network-error" | "project-error";
export type CooldownReason = "auth-failure" | "network-error" | "project-error" | "validation-required";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of "validation-required" and "verification-required"
echo "=== Occurrences of 'validation-required' ==="
rg -n "validation-required" --type=ts

echo ""
echo "=== Occurrences of 'verification-required' ==="
rg -n "verification-required" --type=ts

Repository: NoeFabris/opencode-antigravity-auth

Length of output: 998


Inconsistent naming: "validation-required" vs "verification-required"

The CooldownReason type uses "validation-required" but the rest of the codebase consistently uses "verification" terminology: AccountStatus has 'verification-required' (src/plugin/ui/auth-menu.ts), user-facing display shows "[needs verification]" (auth-menu.ts:49), and debug output uses "verification-required" (plugin.ts:2275). This mismatch is confusing and error-prone for future consumers of CooldownReason.

Required changes

Update src/plugin/storage.ts line 180:

-export type CooldownReason = "auth-failure" | "network-error" | "project-error" | "validation-required";
+export type CooldownReason = "auth-failure" | "network-error" | "project-error" | "verification-required";

Update src/plugin.ts line 2263:

-accountManager.markAccountCoolingDown(account, cooldownMs, "validation-required");
+accountManager.markAccountCoolingDown(account, cooldownMs, "verification-required");
📝 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
export type CooldownReason = "auth-failure" | "network-error" | "project-error" | "validation-required";
export type CooldownReason = "auth-failure" | "network-error" | "project-error" | "verification-required";
🤖 Prompt for AI Agents
In `@src/plugin/storage.ts` at line 180, Rename the CooldownReason variant from
"validation-required" to "verification-required" in the CooldownReason type
declaration (symbol: CooldownReason) and update all places that construct or
compare that string (e.g., the place in plugin.ts that currently emits/compares
"validation-required") so they use "verification-required" consistently; ensure
any switch/case, equality checks, stored values, and user-facing mappings are
updated to the new literal to keep type and runtime strings aligned.

@NoeFabris NoeFabris merged commit 0f60480 into dev Feb 10, 2026
1 of 2 checks passed
@NoeFabris NoeFabris deleted the feat/account-verify-menu-pr395 branch February 10, 2026 00:26
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +827 to +835
markAccountVerificationRequired(accountIndex: number, reason?: string, verifyUrl?: string): boolean {
const account = this.accounts[accountIndex];
if (!account) {
return false;
}

account.verificationRequired = true;
account.verificationRequiredAt = nowMs();
account.verificationRequiredReason = reason?.trim() || undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Verification timestamp churn

AccountManager.markAccountVerificationRequired() overwrites verificationRequiredAt on every call (account.verificationRequiredAt = nowMs()), even if the account was already marked verificationRequired. This makes repeated 403/VALIDATION_REQUIRED responses continuously change the stored metadata and trigger extra disk writes/toast logic downstream. Consider only setting verificationRequiredAt when transitioning from not-required

to required (or when it�s currently unset), similar to the storage helper in src/plugin.ts.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/accounts.ts
Line: 827:835

Comment:
**Verification timestamp churn**

`AccountManager.markAccountVerificationRequired()` overwrites `verificationRequiredAt` on every call (`account.verificationRequiredAt = nowMs()`), even if the account was already marked `verificationRequired`. This makes repeated 403/`VALIDATION_REQUIRED` responses continuously change the stored metadata and trigger extra disk writes/toast logic downstream. Consider only setting `verificationRequiredAt` when transitioning from not-required 

to required (or when it�s currently unset), similar to the storage helper in `src/plugin.ts`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +2255 to +2266
if (response.status === 403) {
const errorBodyText = await response.clone().text().catch(() => "");
const extracted = extractVerificationErrorDetails(errorBodyText);

if (extracted.validationRequired) {
const verificationReason = extracted.message ?? "Google requires account verification.";
const cooldownMs = 10 * 60 * 1000;

accountManager.markAccountVerificationRequired(account.index, verificationReason, extracted.verifyUrl);
accountManager.markAccountCoolingDown(account, cooldownMs, "validation-required");
accountManager.markRateLimited(account, cooldownMs, family, headerStyle, model);

Copy link
Contributor

Choose a reason for hiding this comment

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

Validation-required treated as rate-limit

When a request gets 403 + VALIDATION_REQUIRED, the code marks the account as cooling down and also calls accountManager.markRateLimited(account, cooldownMs, family, headerStyle, model). This makes a verification block look like a temporary quota/rate-limit and can affect rotation and UI status (e.g. showing �rate-limited�) even though the account is actually disabled for manual verification.

If the intent is to remove the account from rotation until the user verifies, the enabled=false + verificationRequired=true state already does that; treating it as a rate limit can mislead selection logic and status reporting.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 2255:2266

Comment:
**Validation-required treated as rate-limit**

When a request gets 403 + `VALIDATION_REQUIRED`, the code marks the account as cooling down and also calls `accountManager.markRateLimited(account, cooldownMs, family, headerStyle, model)`. This makes a verification block look like a temporary quota/rate-limit and can affect rotation and UI status (e.g. showing �rate-limited�) even though the account is actually disabled for manual verification.

If the intent is to remove the account from rotation until the user verifies, the `enabled=false` + `verificationRequired=true` state already does that; treating it as a rate limit can mislead selection logic and status reporting.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Additional Comments (1)

src/plugin.ts
Command injection via URL

openBrowser() interpolates url into a shell command (exec(�open "${url}"�), xdg-open, wslview, etc.). In this PR, that URL can come from remote API error bodies (extractVerificationErrorDetails() �3 verifyUrl �2 openBrowser()), so a crafted URL containing quotes/backticks can execute arbitrary commands on the user�s machine. This will happen whenever the backend returns a verification URL that includes shell metacharacters.

Safer options are to use execFile/spawn with argument arrays (no shell interpolation), or strictly validate/escape URLs before passing them to the shell.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 244:264

Comment:
**Command injection via URL**

`openBrowser()` interpolates `url` into a shell command (`exec(�open "${url}"�)`, `xdg-open`, `wslview`, etc.). In this PR, that URL can come from remote API error bodies (`extractVerificationErrorDetails()` �3 `verifyUrl` �2 `openBrowser()`), so a crafted URL containing quotes/backticks can execute arbitrary commands on the user�s machine. This will happen whenever the backend returns a verification URL that includes shell metacharacters.

Safer options are to use `execFile`/`spawn` with argument arrays (no shell interpolation), or strictly validate/escape URLs before passing them to the shell.

How can I resolve this? If you propose a fix, please make it concise.

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.

1 participant