mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 04:14:57 +00:00
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:
7 files changed
+137
-16
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user