Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions mobile/src/components/accounts-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,7 @@ const RateLimitResetCreditsSchema = z

export const ProviderRateLimitsSchema = z
.object({
provider: z.enum([
'claude',
'codex',
'gemini',
'opencode-go',
'kimi',
'minimax',
'grok',
'antigravity'
]),
provider: z.enum(['claude', 'codex', 'opencode-go', 'kimi', 'minimax', 'grok', 'antigravity']),
session: RateLimitWindowSchema.nullable(),
weekly: RateLimitWindowSchema.nullable(),
fableWeekly: RateLimitWindowSchema.nullable().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ export function createSettings(overrides: TestSettingsOverrides = {}): GlobalSet
opencodeWorkspaceId: '',
minimaxGroupId: '',
minimaxUsageModels: 'general',
geminiCliOAuthEnabled: false,
agentCmdOverrides: {},
keepComputerAwakeWhileAgentsRun: false,
confirmClosePinnedTab: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export function createResetRateLimitState(
return {
claude: null,
codex,
gemini: null,
opencodeGo: null,
kimi: null,
antigravity: null,
Expand Down
1 change: 0 additions & 1 deletion src/main/codex-accounts/service-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ export function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalS
opencodeWorkspaceId: '',
minimaxGroupId: '',
minimaxUsageModels: 'general',
geminiCliOAuthEnabled: false,
agentCmdOverrides: {},
keepComputerAwakeWhileAgentsRun: false,
confirmClosePinnedTab: true,
Expand Down
1 change: 0 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2651,7 +2651,6 @@ void app.whenReady().then(async () => {
models: settings.minimaxUsageModels
}
})
rateLimits.setGeminiCliOAuthEnabledResolver(() => store!.getSettings().geminiCliOAuthEnabled)
rateLimits.setNetworkProxySettingsResolver(() => store!.getSettings())
keybindings = new KeybindingService({
homePath: app.getPath('home'),
Expand Down
11 changes: 11 additions & 0 deletions src/main/persistence-initial-load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ describe('Store', () => {
expect(store.getRepos()).toEqual([])
}, 15_000)

it('removes the deprecated Gemini CLI OAuth usage setting on load', async () => {
writeDataFile({ settings: { geminiCliOAuthEnabled: true } })

const store = await createStore()

expect(store.getSettings()).not.toHaveProperty('geminiCliOAuthEnabled')
store.flush()
const persisted = readDataFile() as { settings?: Record<string, unknown> }
expect(persisted.settings).not.toHaveProperty('geminiCliOAuthEnabled')
})

it('clone-reads and synchronously persists the main-owned Codex reset ledger', async () => {
const store = await createStore()
const ledger = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function readLegacyTerminalScrollbackSettings(
type RetiredGlobalSettings = {
terminalScrollbackBytes?: unknown
enableGitHubAttribution?: unknown
geminiCliOAuthEnabled?: unknown
}

export function stripRetiredGlobalSettings(
Expand All @@ -67,10 +68,12 @@ export function stripRetiredGlobalSettings(
const {
terminalScrollbackBytes: _legacyScrollbackBytes,
enableGitHubAttribution: _legacyGitHubAttribution,
geminiCliOAuthEnabled: _deprecatedGeminiCliOAuthEnabled,
...rest
} = (settings ?? {}) as Partial<GlobalSettings> & RetiredGlobalSettings
void _legacyScrollbackBytes
void _legacyGitHubAttribution
void _deprecatedGeminiCliOAuthEnabled
return rest
}

Expand Down
33 changes: 33 additions & 0 deletions src/main/rate-limits/antigravity-endpoint-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export type AgyQuotaEndpoint = {
pid: number
port: number
csrfToken: string | null
}

export async function inspectAgyProcessCommands(
pids: Iterable<number>,
inspect: (pid: number) => Promise<string>
): Promise<Map<number, string>> {
const entries = await Promise.all(
[...pids].map(async (pid) => {
try {
return [pid, await inspect(pid)] as const
} catch {
return null
}
})
)
return new Map(entries.filter((entry): entry is readonly [number, string] => entry !== null))
}

export function deduplicateAgyQuotaEndpoints(endpoints: AgyQuotaEndpoint[]): AgyQuotaEndpoint[] {
const unique = new Map<string, AgyQuotaEndpoint>()
for (const endpoint of endpoints) {
const key = `${endpoint.pid}:${endpoint.port}`
const current = unique.get(key)
if (!current || (!current.csrfToken && endpoint.csrfToken)) {
unique.set(key, endpoint)
}
}
return [...unique.values()]
}
113 changes: 113 additions & 0 deletions src/main/rate-limits/antigravity-quota-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { RateLimitBucket, RateLimitWindow } from '../../shared/rate-limit-types'

type AgySummaryBucket = {
bucketId?: unknown
remainingFraction?: unknown
resetTime?: unknown
}

type AgySummaryGroup = {
buckets?: unknown
}

type AgySummaryResponse = {
response?: {
groups?: unknown
}
}

export type AgyQuotaWindows = {
session: RateLimitWindow | null
weekly: RateLimitWindow | null
buckets: RateLimitBucket[]
}

const AGY_BUCKET_SPECS = {
'gemini-5h': { name: 'Gemini 5h', windowMinutes: 300 },
'gemini-weekly': { name: 'Gemini wk', windowMinutes: 10_080 },
'3p-5h': { name: 'Claude/GPT 5h', windowMinutes: 300 },
'3p-weekly': { name: 'Claude/GPT wk', windowMinutes: 10_080 }
} as const

const AGY_BUCKET_ORDER = ['gemini-5h', 'gemini-weekly', '3p-5h', '3p-weekly'] as const

type AgyBucketId = (typeof AGY_BUCKET_ORDER)[number]

function isSummaryBucket(value: unknown): value is AgySummaryBucket & { bucketId: AgyBucketId } {
if (!value || typeof value !== 'object') {
return false
}
const bucket = value as AgySummaryBucket
return (
typeof bucket.bucketId === 'string' &&
Object.hasOwn(AGY_BUCKET_SPECS, bucket.bucketId) &&
typeof bucket.remainingFraction === 'number' &&
Number.isFinite(bucket.remainingFraction) &&
(bucket.resetTime === undefined || typeof bucket.resetTime === 'string')
)
}

function toBucket(bucket: AgySummaryBucket & { bucketId: AgyBucketId }): RateLimitBucket {
const spec = AGY_BUCKET_SPECS[bucket.bucketId]
const remainingFraction = Math.min(1, Math.max(0, bucket.remainingFraction as number))
const resetsAt = typeof bucket.resetTime === 'string' ? Date.parse(bucket.resetTime) : Number.NaN
return {
name: spec.name,
usedPercent: (1 - remainingFraction) * 100,
windowMinutes: spec.windowMinutes,
resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
resetDescription: null
}
}

function mostConstrainedWindow(
buckets: RateLimitBucket[],
windowMinutes: number
): RateLimitWindow | null {
const matching = buckets.filter((bucket) => bucket.windowMinutes === windowMinutes)
if (matching.length === 0) {
return null
}
const mostConstrained = matching.reduce((current, candidate) =>
candidate.usedPercent > current.usedPercent ? candidate : current
)
const { name: _name, ...window } = mostConstrained
return window
}

export function parseAgyQuotaSummary(data: unknown): AgyQuotaWindows | null {
const groups = (data as AgySummaryResponse | null)?.response?.groups
if (!Array.isArray(groups)) {
return null
}
const byId = new Map<AgyBucketId, AgySummaryBucket & { bucketId: AgyBucketId }>()
for (const group of groups) {
const rawBuckets = (group as AgySummaryGroup | null)?.buckets
if (!Array.isArray(rawBuckets)) {
continue
}
for (const rawBucket of rawBuckets) {
if (!isSummaryBucket(rawBucket)) {
continue
}
const current = byId.get(rawBucket.bucketId)
if (
!current ||
(rawBucket.remainingFraction as number) < (current.remainingFraction as number)
) {
byId.set(rawBucket.bucketId, rawBucket)
}
}
}
const buckets = AGY_BUCKET_ORDER.map((id) => byId.get(id))
.filter((bucket): bucket is AgySummaryBucket & { bucketId: AgyBucketId } => Boolean(bucket))
.map(toBucket)
if (buckets.length === 0) {
return null
}
return {
session: mostConstrainedWindow(buckets, 300),
weekly: mostConstrainedWindow(buckets, 10_080),
buckets
}
}
Loading