Refactor Gemini model display: filter old versions, group low/high pкo, and improve naming (#114)
* Refactor Gemini model display: filter old versions, group low/high pro, and improve naming * Fix potential negative minutes and refine Gemini 3 Pro grouping logic based on PR feedback * The account change process has been sped up * Address PR review: enhance error message and parameterize polling interval in process handler * Address PR review: fix potential race condition in database backup logic * fix: prevent renderer white screen and harden sentry gating * feat: localize model labels * chore: enable hoisted node-linker * chore: ignore .agent/.claude/.codex in prettier --------- Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
1 parent
b9497648c6
commit
8ed4593f73
162 files changed
+2981
-2541
No files matched your search
+12
-3
@@ -53,7 +53,10 @@ export function previewGenerateCloudIdentityProfile() {
|
||||
return ipc.client.cloud.previewIdentityProfile();
|
||||
}
|
||||
|
||||
export function bindCloudIdentityProfile(input: { accountId: string; mode: 'capture' | 'generate' }) {
|
||||
export function bindCloudIdentityProfile(input: {
|
||||
accountId: string;
|
||||
mode: 'capture' | 'generate';
|
||||
}) {
|
||||
return ipc.client.cloud.bindIdentityProfile(input);
|
||||
}
|
||||
|
||||
@@ -64,7 +67,10 @@ export function bindCloudIdentityProfileWithPayload(input: {
|
||||
return ipc.client.cloud.bindIdentityProfileWithPayload(input);
|
||||
}
|
||||
|
||||
export function restoreCloudIdentityProfileRevision(input: { accountId: string; versionId: string }) {
|
||||
export function restoreCloudIdentityProfileRevision(input: {
|
||||
accountId: string;
|
||||
versionId: string;
|
||||
}) {
|
||||
return ipc.client.cloud.restoreIdentityProfileRevision(input);
|
||||
}
|
||||
|
||||
@@ -72,7 +78,10 @@ export function restoreCloudBaselineProfile(input: { accountId: string }) {
|
||||
return ipc.client.cloud.restoreBaselineProfile(input);
|
||||
}
|
||||
|
||||
export function deleteCloudIdentityProfileRevision(input: { accountId: string; versionId: string }) {
|
||||
export function deleteCloudIdentityProfileRevision(input: {
|
||||
accountId: string;
|
||||
versionId: string;
|
||||
}) {
|
||||
return ipc.client.cloud.deleteIdentityProfileRevision(input);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ import { useAppConfig } from '@/hooks/useAppConfig';
|
||||
import { useProviderGrouping } from '@/hooks/useProviderGrouping';
|
||||
import { ProviderGroup } from '@/components/ProviderGroup';
|
||||
|
||||
interface ModelQuotaInfo {
|
||||
percentage: number;
|
||||
resetTime: string;
|
||||
}
|
||||
interface CloudAccountCardProps {
|
||||
account: CloudAccount;
|
||||
onRefresh: (id: string) => void;
|
||||
@@ -62,22 +66,14 @@ export function CloudAccountCard({
|
||||
|
||||
// Helpers to get quota color
|
||||
const getQuotaColor = (percentage: number) => {
|
||||
if (percentage > 80) {
|
||||
return 'text-green-500';
|
||||
}
|
||||
if (percentage > 20) {
|
||||
return 'text-yellow-500';
|
||||
}
|
||||
if (percentage > 80) return 'text-green-500';
|
||||
if (percentage > 20) return 'text-yellow-500';
|
||||
return 'text-red-500';
|
||||
};
|
||||
|
||||
const getQuotaBarColor = (percentage: number) => {
|
||||
if (percentage > 80) {
|
||||
return 'bg-emerald-500';
|
||||
}
|
||||
if (percentage > 20) {
|
||||
return 'bg-amber-500';
|
||||
}
|
||||
if (percentage > 80) return 'bg-emerald-500';
|
||||
if (percentage > 20) return 'bg-amber-500';
|
||||
return 'bg-rose-500';
|
||||
};
|
||||
|
||||
@@ -90,63 +86,144 @@ export function CloudAccountCard({
|
||||
|
||||
const formatTimeRemaining = (dateStr: string) => {
|
||||
const targetDate = new Date(dateStr);
|
||||
if (Number.isNaN(targetDate.getTime())) {
|
||||
return null;
|
||||
}
|
||||
if (Number.isNaN(targetDate.getTime())) return null;
|
||||
|
||||
const now = new Date();
|
||||
if (isBefore(targetDate, now)) {
|
||||
return '0h 0m';
|
||||
}
|
||||
if (isBefore(targetDate, now)) return '0h 0m';
|
||||
|
||||
// Calculate difference in hours and minutes
|
||||
const diffHrs = differenceInHours(targetDate, now);
|
||||
const diffMins = differenceInMinutes(targetDate, now) - diffHrs * 60;
|
||||
const diffHrs = Math.max(0, differenceInHours(targetDate, now));
|
||||
const diffMins = Math.max(0, differenceInMinutes(targetDate, now) - diffHrs * 60);
|
||||
if (diffHrs >= 24) {
|
||||
const diffDays = Math.floor(diffHrs / 24);
|
||||
const remainingHrs = diffHrs % 24;
|
||||
return `${diffDays}d ${remainingHrs}h`;
|
||||
}
|
||||
|
||||
return `${diffHrs}h ${diffMins}m`;
|
||||
};
|
||||
|
||||
const getResetTimeLabel = (resetTime?: string) => {
|
||||
if (!resetTime) {
|
||||
return t('cloud.card.resetUnknown');
|
||||
}
|
||||
|
||||
if (!resetTime) return t('cloud.card.resetUnknown');
|
||||
const remaining = formatTimeRemaining(resetTime);
|
||||
if (!remaining) {
|
||||
return t('cloud.card.resetUnknown');
|
||||
}
|
||||
|
||||
if (!remaining) return t('cloud.card.resetUnknown');
|
||||
return `${t('cloud.card.resetPrefix')}: ${remaining}`;
|
||||
};
|
||||
|
||||
const getResetTimeTitle = (resetTime?: string) => {
|
||||
if (!resetTime) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!resetTime) return undefined;
|
||||
const resetDate = new Date(resetTime);
|
||||
if (Number.isNaN(resetDate.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Number.isNaN(resetDate.getTime())) return undefined;
|
||||
return `${t('cloud.card.resetTime')}: ${resetDate.toLocaleString()}`;
|
||||
};
|
||||
|
||||
const modelQuotas = Object.entries(account.quota?.models || {}).filter(
|
||||
// --- Logic for model groups ---
|
||||
const rawModels = Object.entries(account.quota?.models || {}).filter(
|
||||
([modelName]) => config?.model_visibility?.[modelName] !== false,
|
||||
);
|
||||
|
||||
// Group Gemini 3 Pro Low/High if both exist
|
||||
const processedModels: Record<string, ModelQuotaInfo> = {};
|
||||
const hasLow = rawModels.some(([name]) => name.includes('gemini-3-pro-low'));
|
||||
const hasHigh = rawModels.some(([name]) => name.includes('gemini-3-pro-high'));
|
||||
|
||||
for (const [name, info] of rawModels) {
|
||||
if (name.includes('gemini-3-pro-low') && hasHigh) continue;
|
||||
if (name.includes('gemini-3-pro-high') && hasLow) {
|
||||
// Use the lower percentage if both exist, to be safe
|
||||
const lowInfo = rawModels.find(([n]) => n.includes('gemini-3-pro-low'))?.[1];
|
||||
const mergedPercentage = lowInfo
|
||||
? Math.min(info.percentage, lowInfo.percentage)
|
||||
: info.percentage;
|
||||
processedModels['gemini-3-pro-low/high'] = { ...info, percentage: mergedPercentage };
|
||||
continue;
|
||||
}
|
||||
processedModels[name] = info;
|
||||
}
|
||||
|
||||
const geminiModels = Object.entries(processedModels)
|
||||
.filter(([name]) => name.includes('gemini') && !/gemini-[12](\.|$|-)/i.test(name))
|
||||
.sort((a, b) => b[1].percentage - a[1].percentage);
|
||||
|
||||
const claudeModels = Object.entries(processedModels)
|
||||
.filter(([name]) => name.includes('claude'))
|
||||
.sort((a, b) => b[1].percentage - a[1].percentage);
|
||||
|
||||
const hasHighTier = geminiModels.some(
|
||||
([name, info]) => name.includes('gemini-3-pro') && info.percentage > 50,
|
||||
);
|
||||
const hasRenderableModels = geminiModels.length > 0 || claudeModels.length > 0;
|
||||
|
||||
const formatModelName = (name: string) => {
|
||||
return name
|
||||
.replace('models/', '')
|
||||
.replace('gemini-3-pro-low/high', 'Gemini 3 Pro (Low/High)')
|
||||
.replace('gemini-3-pro-preview', 'Gemini 3 Pro Preview')
|
||||
.replace('gemini-3-pro-image', 'Gemini 3 Pro Image')
|
||||
.replace('gemini-3-pro', 'Gemini 3 Pro')
|
||||
.replace('gemini-3-flash', 'Gemini 3 Flash')
|
||||
.replace('claude-sonnet-4-5-thinking', 'Claude 4.5 Sonnet (Thinking)')
|
||||
.replace('claude-sonnet-4-5', 'Claude 4.5 Sonnet')
|
||||
.replace('claude-opus-4-6-thinking', 'Claude 4.6 Opus (Thinking)')
|
||||
.replace('claude-opus-4-5-thinking', 'Claude 4.5 Opus (Thinking)')
|
||||
.replace('claude-3-5-sonnet', 'Claude 3.5 Sonnet')
|
||||
.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map((word) => (word.length > 2 ? word.charAt(0).toUpperCase() + word.slice(1) : word))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const renderModelGroup = (title: string, models: [string, ModelQuotaInfo][]) => {
|
||||
if (models.length === 0) return null;
|
||||
return (
|
||||
<div key={title} className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1">
|
||||
<span className="text-muted-foreground/70 text-[10px] font-bold tracking-wider uppercase">
|
||||
{title}
|
||||
</span>
|
||||
<div className="bg-border/50 h-[1px] flex-1" />
|
||||
</div>
|
||||
{models.map(([modelName, info]) => (
|
||||
<div
|
||||
key={modelName}
|
||||
className="group/item hover:bg-muted/60 hover:border-border/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-1.5 text-sm transition-all"
|
||||
>
|
||||
<span
|
||||
className="text-muted-foreground group-hover/item:text-foreground min-w-0 truncate font-semibold"
|
||||
title={modelName}
|
||||
>
|
||||
{formatModelName(modelName)}
|
||||
</span>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span
|
||||
className="text-muted-foreground text-[9px] leading-none opacity-80"
|
||||
title={getResetTimeTitle(info.resetTime)}
|
||||
>
|
||||
{getResetTimeLabel(info.resetTime)}
|
||||
</span>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className={`font-mono text-xs leading-none font-bold ${getQuotaColor(info.percentage)}`}
|
||||
>
|
||||
{info.percentage}%
|
||||
</span>
|
||||
<div className="bg-muted h-1 w-16 overflow-hidden rounded-full">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColor(info.percentage)}`}
|
||||
style={{ width: `${Math.max(0, Math.min(100, info.percentage))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`group bg-card hover:border-primary/40 flex h-full flex-col overflow-hidden border transition-all duration-200 hover:shadow-sm ${isSelected ? 'ring-primary border-primary/50 ring-2' : ''}`}
|
||||
>
|
||||
<CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2">
|
||||
{/* Selection Checkbox - Visible on hover or selected */}
|
||||
{onToggleSelection && (
|
||||
<div
|
||||
className={`absolute top-2 left-2 z-10 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} bg-background/90 rounded-full p-2 transition-opacity`}
|
||||
@@ -233,6 +310,15 @@ export function CloudAccountCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasHighTier && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="animate-pulse border-blue-500/20 bg-blue-500/10 text-[10px] text-blue-500"
|
||||
>
|
||||
{t('cloud.card.gemini3Ready')}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{account.is_active ? (
|
||||
<Button variant="ghost" size="sm" disabled className="text-green-600 opacity-100">
|
||||
<Power className="mr-1 h-3 w-3" />
|
||||
@@ -258,7 +344,6 @@ export function CloudAccountCard({
|
||||
|
||||
<div className="space-y-2">
|
||||
{providerGroupingsEnabled ? (
|
||||
// Grouped view with provider sections
|
||||
(() => {
|
||||
const accountStats = getAccountStats(account);
|
||||
if (accountStats.visibleModels === 0) {
|
||||
@@ -271,7 +356,6 @@ export function CloudAccountCard({
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{/* Account-level overall stats */}
|
||||
<div className="bg-muted/40 flex items-center justify-between rounded-lg px-3 py-1.5 text-xs">
|
||||
<span className="font-medium">{t('settings.providerGroupings.overall')}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -290,7 +374,6 @@ export function CloudAccountCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Provider groups */}
|
||||
{accountStats.providers.map((providerStats) => (
|
||||
<ProviderGroup
|
||||
key={providerStats.providerKey}
|
||||
@@ -310,43 +393,12 @@ export function CloudAccountCard({
|
||||
</>
|
||||
);
|
||||
})()
|
||||
) : modelQuotas.length > 0 ? (
|
||||
modelQuotas.map(([modelName, info]) => (
|
||||
<div
|
||||
key={modelName}
|
||||
className="hover:bg-muted/60 hover:border-border/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-2 text-sm transition-colors"
|
||||
>
|
||||
<span className="text-muted-foreground min-w-0 truncate" title={modelName}>
|
||||
{modelName.replace('models/', '')}
|
||||
</span>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span
|
||||
className="text-muted-foreground text-[10px] leading-none"
|
||||
title={getResetTimeTitle(info.resetTime)}
|
||||
>
|
||||
{getResetTimeLabel(info.resetTime)}
|
||||
</span>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span
|
||||
className={`font-mono leading-none font-bold ${getQuotaColor(info.percentage)}`}
|
||||
>
|
||||
{getQuotaLabel(info.percentage)}
|
||||
</span>
|
||||
{info.percentage > 0 && (
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{t('cloud.card.left')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-muted h-1.5 w-24 overflow-hidden rounded-full">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColor(info.percentage)}`}
|
||||
style={{ width: `${Math.max(0, Math.min(100, info.percentage))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : hasRenderableModels ? (
|
||||
<div className="space-y-3">
|
||||
{renderModelGroup(t('cloud.card.groupGoogleGemini'), geminiModels)}
|
||||
<div className="pt-1" />
|
||||
{renderModelGroup(t('cloud.card.groupAnthropicClaude'), claudeModels)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex flex-col items-center justify-center py-4">
|
||||
<Box className="mb-2 h-8 w-8 opacity-20" />
|
||||
|
||||
+29
-28
@@ -197,20 +197,25 @@ export async function switchCloudAccount(accountId: string): Promise<void> {
|
||||
account.device_profile = generated;
|
||||
}
|
||||
|
||||
// 1. Ensure token is fresh before injecting
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (account.token.expiry_timestamp < now + 300) {
|
||||
logger.info(`Token for ${account.email} near expiry, refreshing before switch...`);
|
||||
try {
|
||||
const newTokenData = await GoogleAPIService.refreshAccessToken(account.token.refresh_token);
|
||||
account.token.access_token = newTokenData.access_token;
|
||||
account.token.expires_in = newTokenData.expires_in;
|
||||
account.token.expiry_timestamp = now + newTokenData.expires_in;
|
||||
await CloudAccountRepo.updateToken(account.id, account.token);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to refresh token before switch, trying with existing token', e);
|
||||
// 1. Prepare token refresh promise (start it in parallel with process exit)
|
||||
const tokenRefreshPromise = (async () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (account.token.expiry_timestamp < now + 1200) { // Increased buffer to 20m
|
||||
logger.info(`Token for ${account.email} near expiry, refreshing in parallel...`);
|
||||
try {
|
||||
const newTokenData = await GoogleAPIService.refreshAccessToken(
|
||||
account.token.refresh_token,
|
||||
);
|
||||
account.token.access_token = newTokenData.access_token;
|
||||
account.token.expires_in = newTokenData.expires_in;
|
||||
account.token.expiry_timestamp = now + newTokenData.expires_in;
|
||||
await CloudAccountRepo.updateToken(account.id, account.token);
|
||||
logger.info(`Token refreshed for ${account.email}`);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to refresh token in parallel, will try to use existing', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
await executeSwitchFlow({
|
||||
scope: 'cloud',
|
||||
@@ -218,29 +223,25 @@ export async function switchCloudAccount(accountId: string): Promise<void> {
|
||||
applyFingerprint: isIdentityProfileApplyEnabled(),
|
||||
processExitTimeoutMs: 10000,
|
||||
performSwitch: async () => {
|
||||
// 3. Backup Database (New Logic)
|
||||
const dbPaths = getAntigravityDbPaths();
|
||||
// Find the valid DB path
|
||||
let dbPath: string | null = null;
|
||||
for (const p of dbPaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
dbPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait for token refresh to complete before injection if it was started
|
||||
await tokenRefreshPromise;
|
||||
|
||||
if (dbPath) {
|
||||
// 3. Backup Database (Optimized to avoid race conditions)
|
||||
const dbPaths = getAntigravityDbPaths();
|
||||
for (const dbPath of dbPaths) {
|
||||
try {
|
||||
const backupPath = `${dbPath}.backup`;
|
||||
fs.copyFileSync(dbPath, backupPath);
|
||||
await fs.promises.copyFile(dbPath, backupPath);
|
||||
logger.info(`Backed up database to ${backupPath}`);
|
||||
} catch (e) {
|
||||
logger.error('Failed to backup database', e);
|
||||
break; // Success, stop trying other paths
|
||||
} catch (e: any) {
|
||||
// If file not found, just try the next path
|
||||
if (e.code === 'ENOENT') continue;
|
||||
logger.error(`Failed to backup database at ${dbPath}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Inject Token
|
||||
// injectedCloudToken uses direct DB/FS access which is sync better-sqlite3.
|
||||
CloudAccountRepo.injectCloudToken(account);
|
||||
|
||||
// 5. Update usage and active status
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
startAuthFlow,
|
||||
} from './handler';
|
||||
import { CloudAccountSchema } from '../../types/cloudAccount';
|
||||
import {
|
||||
DeviceProfileSchema,
|
||||
DeviceProfilesSnapshotSchema,
|
||||
} from '../../types/account';
|
||||
import { DeviceProfileSchema, DeviceProfilesSnapshotSchema } from '../../types/account';
|
||||
import { CloudAccountRepo } from '../database/cloudHandler';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getSwitchMetricsSnapshot } from '../switchMetrics';
|
||||
|
||||
+25
-20
@@ -34,10 +34,8 @@ export interface DeviceHardeningSnapshot {
|
||||
lastFailureAt: number | null;
|
||||
}
|
||||
|
||||
const UUID_V4_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const SQM_ID_REGEX =
|
||||
/^\{[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\}$/;
|
||||
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const SQM_ID_REGEX = /^\{[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\}$/;
|
||||
|
||||
interface DeviceProfileTemplate {
|
||||
machineIdPrefix: string;
|
||||
@@ -183,7 +181,11 @@ function getTelemetryField(storage: Record<string, unknown>, key: string): strin
|
||||
}
|
||||
|
||||
function ensureTelemetryObject(storage: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!storage.telemetry || typeof storage.telemetry !== 'object' || Array.isArray(storage.telemetry)) {
|
||||
if (
|
||||
!storage.telemetry ||
|
||||
typeof storage.telemetry !== 'object' ||
|
||||
Array.isArray(storage.telemetry)
|
||||
) {
|
||||
storage.telemetry = {};
|
||||
}
|
||||
return storage.telemetry as Record<string, unknown>;
|
||||
@@ -272,9 +274,7 @@ function pickDeviceProfileTemplate(): DeviceProfileTemplate {
|
||||
}
|
||||
|
||||
function deterministicUuidFromSeed(seed: string, namespace: string): string {
|
||||
const hash = createHash('sha256')
|
||||
.update(`${namespace}:${seed}`)
|
||||
.digest();
|
||||
const hash = createHash('sha256').update(`${namespace}:${seed}`).digest();
|
||||
const bytes = Buffer.from(hash.subarray(0, 16));
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
@@ -463,10 +463,18 @@ function buildApplyError(
|
||||
): Error {
|
||||
const message = original instanceof Error ? original.message : String(original);
|
||||
const wrapped = new Error(`device_apply_failed:${reason}:${stage}:${message}`);
|
||||
(wrapped as Error & { deviceFailureReason?: DeviceApplyFailureReason; deviceFailureStage?: string })
|
||||
.deviceFailureReason = reason;
|
||||
(wrapped as Error & { deviceFailureReason?: DeviceApplyFailureReason; deviceFailureStage?: string })
|
||||
.deviceFailureStage = stage;
|
||||
(
|
||||
wrapped as Error & {
|
||||
deviceFailureReason?: DeviceApplyFailureReason;
|
||||
deviceFailureStage?: string;
|
||||
}
|
||||
).deviceFailureReason = reason;
|
||||
(
|
||||
wrapped as Error & {
|
||||
deviceFailureReason?: DeviceApplyFailureReason;
|
||||
deviceFailureStage?: string;
|
||||
}
|
||||
).deviceFailureStage = stage;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
@@ -490,12 +498,8 @@ export function isIdentityProfileApplyEnabled(): boolean {
|
||||
return !isSafeModeActiveNow();
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
const envEnabled = (
|
||||
normalized !== '0' &&
|
||||
normalized !== 'false' &&
|
||||
normalized !== 'off' &&
|
||||
normalized !== 'no'
|
||||
);
|
||||
const envEnabled =
|
||||
normalized !== '0' && normalized !== 'false' && normalized !== 'off' && normalized !== 'no';
|
||||
if (!envEnabled) {
|
||||
return false;
|
||||
}
|
||||
@@ -605,8 +609,9 @@ export function syncStateServiceMachineIdValue(serviceMachineId: string, dbPath?
|
||||
db = new Database(targetDbPath);
|
||||
db.pragma('busy_timeout = 3000');
|
||||
db.exec('CREATE TABLE IF NOT EXISTS ItemTable (key TEXT PRIMARY KEY, value TEXT);');
|
||||
db.prepare("INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('storage.serviceMachineId', ?)")
|
||||
.run(serviceMachineId);
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO ItemTable (key, value) VALUES ('storage.serviceMachineId', ?)",
|
||||
).run(serviceMachineId);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isSqliteBusyError(error) && attempt < SQLITE_RETRY_COUNT) {
|
||||
|
||||
@@ -345,15 +345,18 @@ export async function closeAntigravity(): Promise<void> {
|
||||
* @param timeoutMs {number} The timeout in milliseconds.
|
||||
* @returns {Promise<void>} A promise that resolves when the process exits.
|
||||
*/
|
||||
export async function _waitForProcessExit(timeoutMs: number): Promise<void> {
|
||||
export async function _waitForProcessExit(
|
||||
timeoutMs: number,
|
||||
pollInterval = 100, // Make it configurable, but keep fast 100ms default
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (!(await isProcessRunning())) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
throw new Error('Process did not exit within timeout');
|
||||
throw new Error(`Antigravity process did not exit within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,11 @@ import { type DeviceProfile } from '../types/account';
|
||||
import { logger } from '../utils/logger';
|
||||
import { closeAntigravity, startAntigravity, _waitForProcessExit } from './process/handler';
|
||||
import { applyDeviceProfile } from './device/handler';
|
||||
import { type SwitchFailureReason, recordSwitchFailure, recordSwitchSuccess } from './switchMetrics';
|
||||
import {
|
||||
type SwitchFailureReason,
|
||||
recordSwitchFailure,
|
||||
recordSwitchSuccess,
|
||||
} from './switchMetrics';
|
||||
|
||||
export interface SwitchFlowOptions {
|
||||
scope: 'local' | 'cloud';
|
||||
@@ -47,13 +51,7 @@ function toSwitchFailureReason(stage: string, error: unknown): SwitchFailureReas
|
||||
}
|
||||
|
||||
export async function executeSwitchFlow(options: SwitchFlowOptions): Promise<void> {
|
||||
const {
|
||||
scope,
|
||||
targetProfile,
|
||||
applyFingerprint,
|
||||
processExitTimeoutMs,
|
||||
performSwitch,
|
||||
} = options;
|
||||
const { scope, targetProfile, applyFingerprint, processExitTimeoutMs, performSwitch } = options;
|
||||
|
||||
let stage = 'close';
|
||||
try {
|
||||
|
||||
@@ -20,49 +20,42 @@ const CLAUDE_TO_GEMINI: Record<string, string> = {
|
||||
'claude-haiku-4-5-20251001': 'claude-sonnet-4-5',
|
||||
|
||||
// OpenAI Protocol Mapping
|
||||
'gpt-4': 'gemini-2.5-flash',
|
||||
'gpt-4-turbo': 'gemini-2.5-flash',
|
||||
'gpt-4-turbo-preview': 'gemini-2.5-flash',
|
||||
'gpt-4-0125-preview': 'gemini-2.5-flash',
|
||||
'gpt-4-1106-preview': 'gemini-2.5-flash',
|
||||
'gpt-4-0613': 'gemini-2.5-flash',
|
||||
'gpt-4': 'gemini-3-flash',
|
||||
'gpt-4-turbo': 'gemini-3-flash',
|
||||
'gpt-4-turbo-preview': 'gemini-3-flash',
|
||||
'gpt-4-0125-preview': 'gemini-3-flash',
|
||||
'gpt-4-1106-preview': 'gemini-3-flash',
|
||||
'gpt-4-0613': 'gemini-3-flash',
|
||||
|
||||
'gpt-4o': 'gemini-2.5-flash',
|
||||
'gpt-4o-2024-05-13': 'gemini-2.5-flash',
|
||||
'gpt-4o-2024-08-06': 'gemini-2.5-flash',
|
||||
'gpt-4o': 'gemini-3-flash',
|
||||
'gpt-4o-2024-05-13': 'gemini-3-flash',
|
||||
'gpt-4o-2024-08-06': 'gemini-3-flash',
|
||||
|
||||
'gpt-4o-mini': 'gemini-2.5-flash',
|
||||
'gpt-4o-mini-2024-07-18': 'gemini-2.5-flash',
|
||||
'gpt-4o-mini': 'gemini-3-flash',
|
||||
'gpt-4o-mini-2024-07-18': 'gemini-3-flash',
|
||||
|
||||
'gpt-3.5-turbo': 'gemini-2.5-flash',
|
||||
'gpt-3.5-turbo-16k': 'gemini-2.5-flash',
|
||||
'gpt-3.5-turbo-0125': 'gemini-2.5-flash',
|
||||
'gpt-3.5-turbo-1106': 'gemini-2.5-flash',
|
||||
'gpt-3.5-turbo-0613': 'gemini-2.5-flash',
|
||||
'gpt-3.5-turbo': 'gemini-3-flash',
|
||||
'gpt-3.5-turbo-16k': 'gemini-3-flash',
|
||||
'gpt-3.5-turbo-0125': 'gemini-3-flash',
|
||||
'gpt-3.5-turbo-1106': 'gemini-3-flash',
|
||||
'gpt-3.5-turbo-0613': 'gemini-3-flash',
|
||||
|
||||
// Gemini Protocol Mapping
|
||||
'gemini-2.5-flash-lite': 'gemini-2.5-flash',
|
||||
'gemini-2.5-flash-thinking': 'gemini-2.5-flash-thinking',
|
||||
'gemini-2.5-flash-lite': 'gemini-3-flash',
|
||||
'gemini-3-pro-low': 'gemini-3-pro-preview',
|
||||
'gemini-3-pro-high': 'gemini-3-pro-preview',
|
||||
'gemini-3-pro-preview': 'gemini-3-pro-preview',
|
||||
'gemini-3-pro': 'gemini-3-pro-preview',
|
||||
'gemini-2.5-flash': 'gemini-2.5-flash',
|
||||
'gemini-2.5-flash': 'gemini-3-flash',
|
||||
'gemini-3-flash': 'gemini-3-flash',
|
||||
'gemini-3-pro-image': 'gemini-3-pro-image',
|
||||
'internal-background-task': 'gemini-2.5-flash',
|
||||
'internal-background-task': 'gemini-3-flash',
|
||||
};
|
||||
|
||||
const DYNAMIC_IMAGE_BASE_MODEL = 'gemini-3-pro-image';
|
||||
const DYNAMIC_IMAGE_RESOLUTIONS = ['', '-2k', '-4k'];
|
||||
const DYNAMIC_IMAGE_RATIOS = ['', '-1x1', '-4x3', '-3x4', '-16x9', '-9x16', '-21x9'];
|
||||
const EXTRA_DYNAMIC_MODELS = [
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.5-flash',
|
||||
'gemini-3-flash',
|
||||
'gemini-3-pro-high',
|
||||
'gemini-3-pro-low',
|
||||
];
|
||||
const EXTRA_DYNAMIC_MODELS = ['gemini-3-flash', 'gemini-3-pro-high', 'gemini-3-pro-low'];
|
||||
|
||||
export const MODEL_LIST_CREATED_AT = 1770652800;
|
||||
|
||||
@@ -93,7 +86,7 @@ export function getAllDynamicModels(customMapping: Record<string, string> = {}):
|
||||
modelIds.add(modelId);
|
||||
}
|
||||
|
||||
return [...modelIds].sort();
|
||||
return [...modelIds].filter((id) => !/gemini-[12](\.|$|-)/.test(id)).sort();
|
||||
}
|
||||
|
||||
export function mapClaudeModelToGemini(input: string): string {
|
||||
|
||||
@@ -115,8 +115,12 @@ export class QuotaService {
|
||||
const percentage = Math.floor(fraction * 100);
|
||||
const resetTime = info.quotaInfo.resetTime || '';
|
||||
|
||||
// Only save models we care about
|
||||
if (name.includes('gemini') || name.includes('claude')) {
|
||||
// Only save models we care about, filtering out old versions (< 3.0)
|
||||
const isGemini = name.includes('gemini');
|
||||
const isClaude = name.includes('claude');
|
||||
const isOldGemini = /gemini-[12](\.|$|-)/.test(name);
|
||||
|
||||
if ((isGemini || isClaude) && !isOldGemini) {
|
||||
quotaData.models[name] = { percentage, resetTime };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,9 @@ i18n
|
||||
resetPrefix: 'reset',
|
||||
resetTime: 'Reset time',
|
||||
resetUnknown: 'Unknown',
|
||||
gemini3Ready: 'Gemini 3 Ready',
|
||||
groupGoogleGemini: 'Google Gemini',
|
||||
groupAnthropicClaude: 'Anthropic Claude',
|
||||
},
|
||||
identity: {
|
||||
title: 'Identity Profile',
|
||||
@@ -666,6 +669,9 @@ i18n
|
||||
resetPrefix: '重置',
|
||||
resetTime: '重置时间',
|
||||
resetUnknown: '未知',
|
||||
gemini3Ready: 'Gemini 3 就绪',
|
||||
groupGoogleGemini: 'Google Gemini',
|
||||
groupAnthropicClaude: 'Anthropic Claude',
|
||||
},
|
||||
identity: {
|
||||
title: '身份配置',
|
||||
@@ -1050,6 +1056,9 @@ i18n
|
||||
resetPrefix: 'Сброс',
|
||||
resetTime: 'Время сброса',
|
||||
resetUnknown: 'Неизвестно',
|
||||
gemini3Ready: 'Gemini 3 готов',
|
||||
groupGoogleGemini: 'Google Gemini',
|
||||
groupAnthropicClaude: 'Anthropic Claude',
|
||||
},
|
||||
identity: {
|
||||
title: 'Профиль идентичности',
|
||||
|
||||
+35
-2
@@ -200,8 +200,41 @@ function createWindow({ startHidden }: { startHidden: boolean }) {
|
||||
logger.info('createWindow: setMainWindow done');
|
||||
|
||||
if (inDevelopment && MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
logger.info(`createWindow: loading URL ${MAIN_WINDOW_VITE_DEV_SERVER_URL}`);
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
const devUrl = MAIN_WINDOW_VITE_DEV_SERVER_URL;
|
||||
logger.info(`createWindow: waiting for Vite dev server at ${devUrl}`);
|
||||
|
||||
// Wait for Vite to be ready before loading
|
||||
const waitForVite = async (url: string, maxRetries = 30, delay = 500) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
logger.info(`createWindow: Vite server ready after ${i * delay}ms`);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Server not ready yet
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
logger.error('createWindow: Vite server did not start in time');
|
||||
return false;
|
||||
};
|
||||
|
||||
waitForVite(devUrl).then((ready) => {
|
||||
if (mainWindow.isDestroyed()) {
|
||||
logger.warn('createWindow: BrowserWindow destroyed before Vite URL load');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ready) {
|
||||
logger.info(`createWindow: loading URL ${devUrl}`);
|
||||
mainWindow.loadURL(devUrl);
|
||||
} else {
|
||||
logger.error('createWindow: Failed to connect to Vite server, loading anyway');
|
||||
mainWindow.loadURL(devUrl);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info('createWindow: loading file index.html');
|
||||
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ try {
|
||||
// console.error('Preload: Failed to read config', e);
|
||||
}
|
||||
|
||||
if (sentryEnabled) {
|
||||
if (sentryEnabled && process.env.NODE_ENV === 'production') {
|
||||
// Defer Sentry init to avoid blocking main thread during startup (white screen fix)
|
||||
setTimeout(() => {
|
||||
// console.log('[Preload] Initializing Sentry (Deferred)');
|
||||
|
||||
+11
-8
@@ -1,12 +1,15 @@
|
||||
import * as Sentry from '@sentry/electron/renderer';
|
||||
|
||||
if (window.electron.SENTRY_ENABLED) {
|
||||
// Defer initialization to prioritize initial render
|
||||
setTimeout(() => {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
if (window.electron?.SENTRY_ENABLED && process.env.NODE_ENV === 'production') {
|
||||
import('@sentry/electron/renderer')
|
||||
.then((Sentry) => {
|
||||
setTimeout(() => {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
});
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Sentry initialization failed:', error);
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
import '@/App';
|
||||
@@ -15,13 +15,7 @@ function HomePage() {
|
||||
);
|
||||
}
|
||||
|
||||
function HomePageErrorBoundary({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: unknown;
|
||||
reset: () => void;
|
||||
}) {
|
||||
function HomePageErrorBoundary({ error, reset }: { error: unknown; reset: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const hasShownToastRef = useRef(false);
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Body, Controller, Get, HttpStatus, Inject, Param, Post, Res, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -131,9 +141,7 @@ export class GeminiController {
|
||||
}
|
||||
}
|
||||
|
||||
private parseCombinedModelAction(
|
||||
modelAction: string,
|
||||
): {
|
||||
private parseCombinedModelAction(modelAction: string): {
|
||||
model: string;
|
||||
action: string;
|
||||
} | null {
|
||||
@@ -288,5 +296,4 @@ export class GeminiController {
|
||||
private hasHijack(reply: FastifyReply): reply is FastifyReply & { hijack: () => void } {
|
||||
return typeof (reply as { hijack?: unknown }).hijack === 'function';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,14 +30,12 @@ import {
|
||||
} from '../../../lib/antigravity/ModelMapping';
|
||||
import { getServerConfig } from '../../server-config';
|
||||
|
||||
|
||||
|
||||
@Controller('v1')
|
||||
@UseGuards(ProxyGuard)
|
||||
export class ProxyController {
|
||||
private readonly logger = new Logger(ProxyController.name);
|
||||
|
||||
constructor(@Inject(ProxyService) private readonly proxyService: ProxyService) { }
|
||||
constructor(@Inject(ProxyService) private readonly proxyService: ProxyService) {}
|
||||
|
||||
@Get('models')
|
||||
listModels(@Res() res: FastifyReply) {
|
||||
@@ -223,12 +221,13 @@ export class ProxyController {
|
||||
content:
|
||||
imageParts.length > 0
|
||||
? [
|
||||
{
|
||||
type: 'text',
|
||||
text: body.prompt ?? 'Please edit this image based on the provided instruction.',
|
||||
},
|
||||
...imageParts,
|
||||
]
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
body.prompt ?? 'Please edit this image based on the provided instruction.',
|
||||
},
|
||||
...imageParts,
|
||||
]
|
||||
: (body.prompt ?? 'Please edit this image based on the provided instruction.'),
|
||||
},
|
||||
],
|
||||
@@ -951,7 +950,10 @@ export class ProxyController {
|
||||
parts.push({ text: fallbackPrompt || 'Please generate an image based on this request.' });
|
||||
} else if (typeof userMessage.content === 'string') {
|
||||
parts.push({
|
||||
text: userMessage.content || fallbackPrompt || 'Please generate an image based on this request.',
|
||||
text:
|
||||
userMessage.content ||
|
||||
fallbackPrompt ||
|
||||
'Please generate an image based on this request.',
|
||||
});
|
||||
} else {
|
||||
for (const block of userMessage.content) {
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { getServerConfig } from '../../server-config';
|
||||
import { extractApiKeyToken, hasConfiguredApiKey, RequestHeaders } from '../../guards/api-key-auth.util';
|
||||
import {
|
||||
extractApiKeyToken,
|
||||
hasConfiguredApiKey,
|
||||
RequestHeaders,
|
||||
} from '../../guards/api-key-auth.util';
|
||||
|
||||
@Injectable()
|
||||
export class ProxyGuard implements CanActivate {
|
||||
|
||||
+37
-37
@@ -273,38 +273,38 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0.9900 0 0);
|
||||
--background: oklch(0.99 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(0.9900 0 0);
|
||||
--popover: oklch(0.99 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0 0 0);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.9400 0 0);
|
||||
--secondary: oklch(0.94 0 0);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.9700 0 0);
|
||||
--muted-foreground: oklch(0.4400 0 0);
|
||||
--accent: oklch(0.9400 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.44 0 0);
|
||||
--accent: oklch(0.94 0 0);
|
||||
--accent-foreground: oklch(0 0 0);
|
||||
--destructive: oklch(0.6300 0.1900 23.0300);
|
||||
--destructive: oklch(0.63 0.19 23.03);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.9200 0 0);
|
||||
--input: oklch(0.9400 0 0);
|
||||
--border: oklch(0.92 0 0);
|
||||
--input: oklch(0.94 0 0);
|
||||
--ring: oklch(0 0 0);
|
||||
--chart-1: oklch(0.8100 0.1700 75.3500);
|
||||
--chart-2: oklch(0.5500 0.2200 264.5300);
|
||||
--chart-3: oklch(0.7200 0 0);
|
||||
--chart-4: oklch(0.9200 0 0);
|
||||
--chart-5: oklch(0.5600 0 0);
|
||||
--chart-1: oklch(0.81 0.17 75.35);
|
||||
--chart-2: oklch(0.55 0.22 264.53);
|
||||
--chart-3: oklch(0.72 0 0);
|
||||
--chart-4: oklch(0.92 0 0);
|
||||
--chart-5: oklch(0.56 0 0);
|
||||
--radius: 0.5rem;
|
||||
--sidebar: oklch(0.9900 0 0);
|
||||
--sidebar: oklch(0.99 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0 0 0);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.9400 0 0);
|
||||
--sidebar-accent: oklch(0.94 0 0);
|
||||
--sidebar-accent-foreground: oklch(0 0 0);
|
||||
--sidebar-border: oklch(0.9400 0 0);
|
||||
--sidebar-border: oklch(0.94 0 0);
|
||||
--sidebar-ring: oklch(0 0 0);
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
@@ -331,37 +331,37 @@
|
||||
.dark {
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(1 0 0);
|
||||
--card: oklch(0.1400 0 0);
|
||||
--card: oklch(0.14 0 0);
|
||||
--card-foreground: oklch(1 0 0);
|
||||
--popover: oklch(0.1800 0 0);
|
||||
--popover: oklch(0.18 0 0);
|
||||
--popover-foreground: oklch(1 0 0);
|
||||
--primary: oklch(1 0 0);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.2500 0 0);
|
||||
--secondary: oklch(0.25 0 0);
|
||||
--secondary-foreground: oklch(1 0 0);
|
||||
--muted: oklch(0.2300 0 0);
|
||||
--muted-foreground: oklch(0.7200 0 0);
|
||||
--accent: oklch(0.3200 0 0);
|
||||
--muted: oklch(0.23 0 0);
|
||||
--muted-foreground: oklch(0.72 0 0);
|
||||
--accent: oklch(0.32 0 0);
|
||||
--accent-foreground: oklch(1 0 0);
|
||||
--destructive: oklch(0.6900 0.2000 23.9100);
|
||||
--destructive: oklch(0.69 0.2 23.91);
|
||||
--destructive-foreground: oklch(0 0 0);
|
||||
--border: oklch(0.2600 0 0);
|
||||
--input: oklch(0.3200 0 0);
|
||||
--ring: oklch(0.7200 0 0);
|
||||
--chart-1: oklch(0.8100 0.1700 75.3500);
|
||||
--chart-2: oklch(0.5800 0.2100 260.8400);
|
||||
--chart-3: oklch(0.5600 0 0);
|
||||
--chart-4: oklch(0.4400 0 0);
|
||||
--chart-5: oklch(0.9200 0 0);
|
||||
--border: oklch(0.26 0 0);
|
||||
--input: oklch(0.32 0 0);
|
||||
--ring: oklch(0.72 0 0);
|
||||
--chart-1: oklch(0.81 0.17 75.35);
|
||||
--chart-2: oklch(0.58 0.21 260.84);
|
||||
--chart-3: oklch(0.56 0 0);
|
||||
--chart-4: oklch(0.44 0 0);
|
||||
--chart-5: oklch(0.92 0 0);
|
||||
--radius: 0.5rem;
|
||||
--sidebar: oklch(0.1800 0 0);
|
||||
--sidebar: oklch(0.18 0 0);
|
||||
--sidebar-foreground: oklch(1 0 0);
|
||||
--sidebar-primary: oklch(1 0 0);
|
||||
--sidebar-primary-foreground: oklch(0 0 0);
|
||||
--sidebar-accent: oklch(0.3200 0 0);
|
||||
--sidebar-accent: oklch(0.32 0 0);
|
||||
--sidebar-accent-foreground: oklch(1 0 0);
|
||||
--sidebar-border: oklch(0.3200 0 0);
|
||||
--sidebar-ring: oklch(0.7200 0 0);
|
||||
--sidebar-border: oklch(0.32 0 0);
|
||||
--sidebar-ring: oklch(0.72 0 0);
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
@@ -381,4 +381,4 @@
|
||||
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,8 @@ async function validateRuntimeAnthropicRequestFromRealTokenManager(): Promise<vo
|
||||
},
|
||||
markAsRateLimited: (accountIdOrEmail: string) =>
|
||||
realTokenManager.markAsRateLimited(accountIdOrEmail),
|
||||
markAsForbidden: (accountIdOrEmail: string) => realTokenManager.markAsForbidden(accountIdOrEmail),
|
||||
markAsForbidden: (accountIdOrEmail: string) =>
|
||||
realTokenManager.markAsForbidden(accountIdOrEmail),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -256,7 +257,9 @@ async function validateRuntimeAnthropicRequestFromRealTokenManager(): Promise<vo
|
||||
const captured = capturedBody as Record<string, unknown>;
|
||||
const tokenProjectId = selectedToken?.token?.project_id;
|
||||
const normalizedTokenProjectId =
|
||||
typeof tokenProjectId === 'string' && tokenProjectId.trim() !== '' ? tokenProjectId.trim() : undefined;
|
||||
typeof tokenProjectId === 'string' && tokenProjectId.trim() !== ''
|
||||
? tokenProjectId.trim()
|
||||
: undefined;
|
||||
|
||||
if (normalizedTokenProjectId) {
|
||||
assert.equal(
|
||||
@@ -275,7 +278,10 @@ async function validateRuntimeAnthropicRequestFromRealTokenManager(): Promise<vo
|
||||
'[DEBUG] Observed getNextToken options from ProxyService:',
|
||||
JSON.stringify(observedGetNextTokenOptions, null, 2),
|
||||
);
|
||||
console.log('[DEBUG] Selected token project_id from TokenManagerService:', tokenProjectId ?? null);
|
||||
console.log(
|
||||
'[DEBUG] Selected token project_id from TokenManagerService:',
|
||||
tokenProjectId ?? null,
|
||||
);
|
||||
console.log(
|
||||
'[DEBUG] Runtime captured internal request (from handleAnthropicMessages):',
|
||||
JSON.stringify(captured, null, 2),
|
||||
|
||||
@@ -99,7 +99,10 @@ describe('Account Handler', () => {
|
||||
it('should reuse existing device profile on switch', async () => {
|
||||
const account = await addAccountSnapshot();
|
||||
const accountFilePath = path.join(testAgentDir, 'accounts.json');
|
||||
const allAccounts = JSON.parse(fs.readFileSync(accountFilePath, 'utf-8')) as Record<string, any>;
|
||||
const allAccounts = JSON.parse(fs.readFileSync(accountFilePath, 'utf-8')) as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
allAccounts[account.id].deviceProfile = {
|
||||
machineId: 'existing-machine',
|
||||
macMachineId: 'existing-mac',
|
||||
|
||||
@@ -266,7 +266,6 @@ describe('CloudAccountRepo.syncFromIDE', () => {
|
||||
expect(wroteUnifiedKey).toBe(true);
|
||||
expect(updatedOldKey).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('cloud switch fail-fast path', () => {
|
||||
|
||||
@@ -21,11 +21,7 @@ describe('ProtobufUtils Unified OAuth', () => {
|
||||
const refreshToken = 'refresh-token-def';
|
||||
const expiry = 1700001234;
|
||||
|
||||
const unifiedB64 = ProtobufUtils.createUnifiedOAuthToken(
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiry,
|
||||
);
|
||||
const unifiedB64 = ProtobufUtils.createUnifiedOAuthToken(accessToken, refreshToken, expiry);
|
||||
const unifiedBytes = new Uint8Array(Buffer.from(unifiedB64, 'base64'));
|
||||
const parsed = ProtobufUtils.extractOAuthTokenInfoFromUnifiedState(unifiedBytes);
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import { Observable } from 'rxjs';
|
||||
|
||||
import { ProxyService } from '../../server/modules/proxy/proxy.service';
|
||||
|
||||
const mockTokenManager = { getNextToken: vi.fn(), markAsRateLimited: vi.fn(), markAsForbidden: vi.fn() };
|
||||
const mockTokenManager = {
|
||||
getNextToken: vi.fn(),
|
||||
markAsRateLimited: vi.fn(),
|
||||
markAsForbidden: vi.fn(),
|
||||
};
|
||||
const mockGeminiClient = { streamGenerateInternal: vi.fn(), generateInternal: vi.fn() };
|
||||
|
||||
class TestableProxyService extends ProxyService {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { DeviceProfileSchema, DeviceProfileVersionSchema, type DeviceProfile, type DeviceProfileVersion } from './account';
|
||||
import {
|
||||
DeviceProfileSchema,
|
||||
DeviceProfileVersionSchema,
|
||||
type DeviceProfile,
|
||||
type DeviceProfileVersion,
|
||||
} from './account';
|
||||
|
||||
export interface CloudTokenData {
|
||||
access_token: string;
|
||||
|
||||
@@ -231,9 +231,7 @@ export class ProtobufUtils {
|
||||
|
||||
const field4 = this.encodeLenDelimField(4, timestampMsg);
|
||||
|
||||
const combined = new Uint8Array(
|
||||
field1.length + field2.length + field3.length + field4.length,
|
||||
);
|
||||
const combined = new Uint8Array(field1.length + field2.length + field3.length + field4.length);
|
||||
combined.set(field1, 0);
|
||||
combined.set(field2, field1.length);
|
||||
combined.set(field3, field1.length + field2.length);
|
||||
|
||||
Reference in new issue
Block a user