Merge pull request #340 from kaitranntt/feat/agy-project-id-display

feat(dashboard): add project_id display for Antigravity accounts
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-01-15 12:13:21 -05:00
commit 14196ee19b
7 files changed
+137 -16

No files matched your search

+43 -4
View File
@@ -47,6 +47,8 @@ export interface AccountInfo {
pausedAt?: string;
/** Account tier: free or paid (Pro/Ultra combined) */
tier?: AccountTier;
/** GCP Project ID (Antigravity only) - read-only, fetched from auth token */
projectId?: string;
}
/** Provider accounts configuration */
@@ -293,7 +295,8 @@ export function registerAccount(
provider: CLIProxyProvider,
tokenFile: string,
email?: string,
nickname?: string
nickname?: string,
projectId?: string
): AccountInfo {
const registry = loadAccountsRegistry();
@@ -350,7 +353,7 @@ export function registerAccount(
const isFirstAccount = Object.keys(providerAccounts.accounts).length === 0;
// Create or update account
providerAccounts.accounts[accountId] = {
const accountMeta: Omit<AccountInfo, 'id' | 'provider' | 'isDefault'> = {
email,
nickname: accountNickname,
tokenFile,
@@ -358,6 +361,13 @@ export function registerAccount(
lastUsedAt: new Date().toISOString(),
};
// Include projectId for Antigravity accounts
if (provider === 'agy' && projectId) {
accountMeta.projectId = projectId;
}
providerAccounts.accounts[accountId] = accountMeta;
// Set as default if first account
if (isFirstAccount) {
providerAccounts.default = accountId;
@@ -374,6 +384,7 @@ export function registerAccount(
tokenFile,
createdAt: providerAccounts.accounts[accountId].createdAt,
lastUsedAt: providerAccounts.accounts[accountId].lastUsedAt,
projectId: providerAccounts.accounts[accountId].projectId,
};
}
@@ -629,6 +640,20 @@ export function discoverExistingAccounts(): void {
// Skip if token file already registered (under any accountId)
const existingTokenFiles = Object.values(providerAccounts.accounts).map((a) => a.tokenFile);
if (existingTokenFiles.includes(file)) {
// Token file exists - check if we need to update projectId for agy accounts
const projectIdValue =
typeof data.project_id === 'string' && data.project_id.trim()
? data.project_id.trim()
: null;
if (provider === 'agy' && projectIdValue) {
const existingEntry = Object.entries(providerAccounts.accounts).find(
([, meta]) => meta.tokenFile === file
);
// Update if missing or changed
if (existingEntry && existingEntry[1].projectId !== projectIdValue) {
existingEntry[1].projectId = projectIdValue;
}
}
continue;
}
@@ -671,13 +696,24 @@ export function discoverExistingAccounts(): void {
// Register account with auto-generated nickname
// Use mtime as lastUsedAt (when token was last modified = last auth/refresh)
const lastModified = stats.mtime || stats.birthtime || new Date();
providerAccounts.accounts[accountId] = {
const accountMeta: Omit<AccountInfo, 'id' | 'provider' | 'isDefault'> = {
email,
nickname: generateNickname(email),
tokenFile: file,
createdAt: stats.birthtime?.toISOString() || new Date().toISOString(),
lastUsedAt: lastModified.toISOString(),
};
// Read project_id for Antigravity accounts (read-only field from auth token)
const discoveredProjectId =
typeof data.project_id === 'string' && data.project_id.trim()
? data.project_id.trim()
: null;
if (provider === 'agy' && discoveredProjectId) {
accountMeta.projectId = discoveredProjectId;
}
providerAccounts.accounts[accountId] = accountMeta;
} catch {
// Skip invalid files
continue;
@@ -693,7 +729,7 @@ export function discoverExistingAccounts(): void {
if (!freshRegistry.providers[prov]) {
freshRegistry.providers[prov] = discovered;
} else {
// Merge accounts, preferring fresh registry's existing entries
// Merge accounts, preferring fresh registry's existing entries but updating projectId
const freshProviderAccounts = freshRegistry.providers[prov];
if (!freshProviderAccounts) continue;
for (const [id, meta] of Object.entries(discovered.accounts)) {
@@ -703,6 +739,9 @@ export function discoverExistingAccounts(): void {
if (!freshProviderAccounts.default || freshProviderAccounts.default === 'default') {
freshProviderAccounts.default = id;
}
} else if (meta.projectId && !freshProviderAccounts.accounts[id].projectId) {
// Update existing account with projectId if discovered from auth file
freshProviderAccounts.accounts[id].projectId = meta.projectId;
}
}
}
+8 -1
View File
@@ -229,8 +229,15 @@ export function registerAccountFromToken(
const content = fs.readFileSync(tokenPath, 'utf-8');
const data = JSON.parse(content);
const email = data.email || undefined;
const projectId = data.project_id || undefined;
return registerAccount(provider, newestFile, email, nickname || generateNickname(email));
return registerAccount(
provider,
newestFile,
email,
nickname || generateNickname(email),
projectId
);
} catch {
return null;
}
@@ -25,6 +25,8 @@ import {
Pause,
Play,
AlertCircle,
AlertTriangle,
FolderCode,
} from 'lucide-react';
import {
cn,
@@ -167,6 +169,54 @@ export function AccountItem({
</Badge>
)}
</div>
{/* Project ID for Antigravity accounts - read-only */}
{account.provider === 'agy' && (
<div className="flex items-center gap-1.5 mt-1">
{account.projectId ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<FolderCode className="w-3 h-3" aria-hidden="true" />
<span
className={cn(
'font-mono max-w-[180px] truncate',
privacyMode && PRIVACY_BLUR_CLASS
)}
title={account.projectId}
>
{account.projectId}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">GCP Project ID (read-only)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-500">
<AlertTriangle className="w-3 h-3" aria-label="Warning" />
<span>Project ID: N/A</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[250px]">
<div className="text-xs space-y-1">
<p className="font-medium text-amber-600">Missing Project ID</p>
<p>
This may cause errors. Remove the account and re-add it to fetch the
project ID.
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
{account.lastUsedAt && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
<Clock className="w-3 h-3" />
@@ -3,10 +3,11 @@
*/
import type React from 'react';
import { ChevronRight } from 'lucide-react';
import { ChevronRight, AlertTriangle } from 'lucide-react';
import { cn, STATUS_COLORS } from '@/lib/utils';
import { PROVIDER_COLORS } from '@/lib/provider-config';
import { ProviderIcon } from '@/components/shared/provider-icon';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import type { ProviderStats } from '../types';
import { getSuccessRate, cleanEmail } from '../utils';
import { InlineStatsBadge } from './inline-stats-badge';
@@ -102,16 +103,35 @@ export function ProviderCard({
</div>
</div>
{/* Account color dots */}
<div className="flex gap-1 mt-3">
{stats.accounts.slice(0, 5).map((acc) => (
<div
key={acc.id}
className="w-2 h-2 rounded-full"
style={{ backgroundColor: acc.color }}
title={privacyMode ? '••••••' : cleanEmail(acc.email)}
/>
))}
{/* Account color dots with warning for agy accounts missing projectId */}
<div className="flex gap-1 mt-3 items-center">
{stats.accounts.slice(0, 5).map((acc) => {
const isMissingProjectId = stats.provider === 'agy' && !acc.projectId;
return (
<div key={acc.id} className="relative">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: acc.color }}
title={privacyMode ? '••••••' : cleanEmail(acc.email)}
/>
{isMissingProjectId && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<AlertTriangle
className="absolute -top-1 -right-1 w-2.5 h-2.5 text-amber-500"
aria-label="Missing Project ID"
/>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
Missing Project ID - re-add account to fix
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
);
})}
{stats.accounts.length > 5 && (
<span className="text-[10px] text-muted-foreground ml-1">
+{stats.accounts.length - 5}
@@ -99,6 +99,7 @@ export function useAuthMonitorData(): AuthMonitorData {
failureCount: failure,
lastUsedAt: realStats?.lastUsedAt ?? account.lastUsedAt,
color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length],
projectId: account.projectId,
};
accountsList.push(row);
providerData.accounts.push(row);
@@ -12,6 +12,8 @@ export interface AccountRow {
failureCount: number;
lastUsedAt?: string;
color: string;
/** GCP Project ID (Antigravity only) - read-only */
projectId?: string;
}
export interface ProviderStats {
+2
View File
@@ -83,6 +83,8 @@ export interface OAuthAccount {
pausedAt?: string;
/** Account tier: free or paid (Pro/Ultra combined) */
tier?: 'free' | 'paid' | 'unknown';
/** GCP Project ID (Antigravity only) - read-only */
projectId?: string;
}
export interface AuthStatus {