Fix Antigravity provider detection, Codex fork double-counting, and tab ordering

Antigravity:
- Handle ephemeral port (--https_server_port 0) via lsof fallback
- Force reparse when cached turns are 0 (server may have been unavailable)
- Persist precomputed costUSD in session cache for correct pricing
- Map gemini-pro-agent to gemini-3.1-pro pricing
- Add display names for gemini-pro-agent and gemini-3.5-flash-low

Codex:
- Detect forked sessions via forked_from_id in session_meta
- Skip replayed parent events within 5s of fork creation time
- Use parent session ID in dedup key so parent+fork don't double-count

Menubar:
- Sort provider tabs by cost descending instead of enum declaration order
This commit is contained in:
iamtoruk 2026-05-21 11:46:11 -07:00
parent 17eada2aa1
commit 95de63a436
4 changed files with 65 additions and 7 deletions

View file

@ -120,10 +120,19 @@ struct AgentTabStrip: View {
let detectedKeys = Set(
todayAll.current.providers.keys.map { $0.lowercased() }
)
return ProviderFilter.allCases.filter { filter in
let detected = ProviderFilter.allCases.filter { filter in
if filter == .all { return true }
return filter.providerKeys.contains(where: detectedKeys.contains)
}
let costs = Dictionary(uniqueKeysWithValues: detected.map { ($0, cost(for: $0) ?? 0) })
return detected.sorted { a, b in
if a == .all { return true }
if b == .all { return false }
let ca = costs[a, default: 0]
let cb = costs[b, default: 0]
if ca != cb { return ca > cb }
return a.rawValue < b.rawValue
}
}
private func cost(for filter: ProviderFilter) -> Double? {

View file

@ -1531,7 +1531,7 @@ function providerCallToCachedCall(call: ParsedProviderCall): CachedCall {
webSearchRequests: call.webSearchRequests,
cacheCreationOneHourTokens: 0,
},
costUSD: call.provider === 'mistral-vibe' ? call.costUSD : undefined,
costUSD: (call.provider === 'mistral-vibe' || call.provider === 'antigravity') ? call.costUSD : undefined,
speed: call.speed,
timestamp: call.timestamp,
tools: call.tools,
@ -1681,6 +1681,10 @@ function getOrCreateProviderSection(cache: SessionCache, provider: string): Prov
}
function cachedFileNeedsProviderReparse(providerName: string, cached: CachedFile): boolean {
// Antigravity data comes from the live server, not from the .pb file.
// A 0-turn cache entry may just mean the server was unavailable last run.
if (providerName === 'antigravity' && cached.turns.length === 0) return true
if (providerName !== 'gemini') return false
return cached.turns.some(turn =>

View file

@ -111,7 +111,7 @@ function isLikelyCsrfToken(value: string): boolean {
return value.length >= 16 && /^[A-Za-z0-9._~:/+=-]+$/.test(value)
}
export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | null {
export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | { port: 0; csrfToken: string } | null {
const lower = line.toLowerCase()
if (!lower.includes('language_server') || !lower.includes('antigravity')) return null
@ -121,7 +121,7 @@ export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | n
if (!isLikelyCsrfToken(csrfToken)) return null
const port = Number(rawPort)
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null
if (!Number.isInteger(port) || port < 0 || port > 65535) return null
return { port, csrfToken }
}
@ -222,10 +222,36 @@ async function readProcessCommandLines(): Promise<string[]> {
return output.split('\n')
}
async function resolveEphemeralPort(csrfToken: string): Promise<ServerInfo | null> {
if (process.platform === 'win32') return null
try {
const pidOutput = await execFileText('pgrep', ['-f', 'language_server.*antigravity'])
const pid = pidOutput.trim().split('\n')[0]
if (!pid) return null
const lsofOutput = await execFileText('lsof', ['-a', '-i', '-P', '-n', '-p', pid])
for (const line of lsofOutput.split('\n')) {
if (!line.includes('LISTEN')) continue
const match = line.match(/:(\d+)\s+\(LISTEN\)/)
if (match) {
const port = Number(match[1])
if (port > 0) return { port, csrfToken }
}
}
} catch { /* best-effort */ }
return null
}
async function detectServer(): Promise<ServerInfo | null> {
if (cachedServer !== undefined) return cachedServer
try {
cachedServer = parseAntigravityServerInfo(await readProcessCommandLines())
const info = parseAntigravityServerInfo(await readProcessCommandLines())
if (info && info.port > 0) {
cachedServer = info as ServerInfo
} else if (info && info.port === 0) {
cachedServer = await resolveEphemeralPort(info.csrfToken)
} else {
cachedServer = null
}
return cachedServer
} catch { /* process discovery failed or timed out */ }
cachedServer = null
@ -291,8 +317,13 @@ async function getModelMap(server: ServerInfo): Promise<ModelMap> {
}
// Strip Antigravity-specific suffixes so the pricing DB can match
const PRICING_ALIASES: Record<string, string> = {
'gemini-pro': 'gemini-3.1-pro',
}
function normalizePricingModel(model: string): string {
return model.replace(/-(high|low|agent)$/, '')
const stripped = model.replace(/-(high|low|agent)$/, '')
return PRICING_ALIASES[stripped] ?? stripped
}
async function discoverSessions(): Promise<SessionSource[]> {
@ -424,11 +455,13 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
}
const modelDisplayNames: Record<string, string> = {
'gemini-pro-agent': 'Gemini Pro',
'gemini-3-pro': 'Gemini 3 Pro',
'gemini-3.1-pro-high': 'Gemini 3.1 Pro',
'gemini-3.1-pro-low': 'Gemini 3.1 Pro (Low)',
'gemini-3-flash': 'Gemini 3 Flash',
'gemini-3-flash-agent': 'Gemini 3 Flash',
'gemini-3.5-flash-low': 'Gemini 3.5 Flash',
'gemini-3.1-flash-image': 'Gemini 3.1 Flash',
'gemini-3.1-flash-lite': 'Gemini 3.1 Flash Lite',
'claude-opus-4-6-thinking': 'Opus 4.6',

View file

@ -44,6 +44,7 @@ type CodexEntry = {
model_provider?: string
originator?: string
session_id?: string
forked_from_id?: string
model?: string
name?: string
content?: Array<{ type?: string; text?: string }>
@ -224,6 +225,7 @@ function parseCodexLine(line: string | Buffer): CodexEntry | null {
model_provider: getRawJsonStringField(pHead, 'model_provider'),
originator: getRawJsonStringField(pHead, 'originator'),
session_id: getRawJsonStringField(pHead, 'session_id'),
forked_from_id: getRawJsonStringField(pHead, 'forked_from_id'),
model: getRawJsonStringField(pHead, 'model'),
name: getRawJsonStringField(pHead, 'name'),
},
@ -315,6 +317,8 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
let sessionModel: string | undefined
let sessionId = ''
let forkedFromId = ''
let forkCutoff = ''
// Null sentinel rather than `0` so the FIRST event is never confused
// with a duplicate. A session that only emits last_token_usage (no
// total_token_usage) reports cumulativeTotal=0 on every event; with a
@ -345,6 +349,10 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
if (entry.type === 'session_meta') {
sessionId = entry.payload?.session_id ?? basename(source.path, '.jsonl')
forkedFromId = entry.payload?.forked_from_id ?? ''
if (forkedFromId && entry.timestamp) {
forkCutoff = new Date(new Date(entry.timestamp).getTime() + 5000).toISOString()
}
sessionModel = entry.payload?.model ?? sessionModel
continue
}
@ -383,6 +391,10 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
}
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
// Forked sessions replay the parent's entire event history with
// timestamps clustered at the fork creation time. Skip replayed
// events (within 5s of fork) to avoid double-counting.
if (forkCutoff && entry.timestamp && entry.timestamp < forkCutoff) continue
const info = entry.payload.info
if (!info) {
if (pendingOutputChars === 0 && pendingUserMessage.length === 0) continue
@ -479,7 +491,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
const model = resolveModel(entry.payload, sessionModel)
const timestamp = entry.timestamp ?? ''
const dedupKey = `codex:${sessionId}:${timestamp}:${cumulativeTotal}`
const dedupKey = `codex:${forkedFromId || sessionId}:${cumulativeTotal}`
if (seenKeys.has(dedupKey)) continue
seenKeys.add(dedupKey)