fix(agent-status): stop an Antigravity pane reading as Gemini CLI

Antigravity's models are named "Gemini <n.n> <Name>" — the real `agy models`
output is already parsed in commit-message-agent-spec.test.ts — so an agy pane's
own title carries a whole `gemini` token. getAgentLabel checks Gemini CLI before
Antigravity, first match wins, so the model name won and the pane read as Gemini
CLI. Measured on '⠋ agy · Gemini 3.7 Flash · high': geminiGlyphs false,
geminiToken true, agyToken true, label 'Gemini CLI'. Even
'Antigravity · Gemini 3.7 Flash' resolved to Gemini CLI.

This surfaced as the tab bar and the sidebar disagreeing about the same pane,
because the two reach different copies of the chain and apply different
precedence to its result.

Defer only the token path: if a title carries an agy/antigravity token, the
bare-`gemini` branch declines. The four Gemini OSC glyphs stay decisive, and agy
emits none of them. Same shape as the existing isPiAgentTitle veto directly
above, which exists because substring matching made paths like 'gemini-project'
masquerade as Gemini CLI.

Narrowing the token rather than reordering the chain, deliberately: a real
recorded pane title from local terminal history is
'STA-4011 Linux Antigravity Commit Messages - grok' — a Grok pane whose task
text contains the token Antigravity. It resolves correctly only because grok is
checked before antigravity, so hoisting the Antigravity branch would break it.
That title ships as a regression case.

Both copies of the chain are fixed; the sidebar reaches one and the tab the
other, so fixing one alone would only move the disagreement.
This commit is contained in:
Brennan Benson
2026-08-19 16:17:55 -07:00
parent fcdbcf85d0
commit 233b2a1100
3 changed files with 63 additions and 0 deletions
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { getAgentLabel } from './agent-title-identity'
import { resolveExplicitTerminalTitleAgentType } from './terminal-title-agent-type'
import { GEMINI_IDLE, GEMINI_WORKING } from './agent-title-core'
// Why: Antigravity's models are named "Gemini <n.n> <Name>" (see the real `agy models`
// output parsed in commit-message-agent-spec.test.ts), so an agy pane's own title carries a
// whole `gemini` token. Gemini CLI is checked before Antigravity in getAgentLabel, so the
// model name used to win and an agy pane read as Gemini CLI.
describe('agy titles carrying a Gemini model name', () => {
const AGY_TITLES = [
'⠋ agy · Gemini 3.7 Flash · high',
'Antigravity · Gemini 3.7 Flash',
'agy · Gemini 3.5 Flash (High)'
]
it.each(AGY_TITLES)('labels %s as Antigravity', (title) => {
expect(getAgentLabel(title)).toBe('Antigravity')
})
// Why: the two chain copies are reached by different surfaces — the sidebar goes through
// getAgentLabel, the tab through resolveExplicitTerminalTitleAgentType. A fix in one only
// moves the disagreement rather than removing it.
it.each(AGY_TITLES)('resolves %s as antigravity on the tab path too', (title) => {
expect(resolveExplicitTerminalTitleAgentType(title)).toBe('antigravity')
})
})
describe('titles that must keep their existing label', () => {
// Why: a real recorded pane title from terminal history. A Grok pane whose TASK TEXT
// contains the token "Antigravity" — it resolves correctly only because grok is checked
// before antigravity, which is why this fix narrows a token instead of reordering.
it('keeps a grok pane whose task text names Antigravity', () => {
expect(getAgentLabel('STA-4011 Linux Antigravity Commit Messages - grok')).toBe('Grok')
})
// Why: the four Gemini OSC glyphs are decisive and stay so; agy emits none of them.
it('still labels a glyph-bearing Gemini CLI title', () => {
expect(getAgentLabel(`${GEMINI_WORKING} Gemini CLI`)).toBe('Gemini CLI')
expect(getAgentLabel(`${GEMINI_IDLE} Gemini CLI`)).toBe('Gemini CLI')
})
// Why: a Gemini CLI title with no agy token must be unaffected by the narrowing.
it('still labels a bare gemini token title', () => {
expect(getAgentLabel('gemini ready')).toBe('Gemini CLI')
})
})
+8
View File
@@ -67,6 +67,14 @@ export function isGeminiTerminalTitle(title: string): boolean {
if (isPiAgentTitle(title)) {
return false
}
// Why: Antigravity's models are named "Gemini <n.n> <Name>", so an agy pane's own
// title carries a whole `gemini` token. Gemini CLI is checked before Antigravity in
// getAgentLabel, so without this the model name wins and an agy pane reads as Gemini
// CLI. Only the token path defers — the four Gemini OSC glyphs stay decisive, and agy
// emits none of them.
if (titleHasAgentName(title, 'antigravity') || AGY_AGENT_NAME_RE.test(title)) {
return false
}
return titleHasAgentName(title, 'gemini')
}
+8
View File
@@ -48,6 +48,14 @@ export function isGeminiTerminalTitle(title: string): boolean {
if (isPiAgentTitle(title)) {
return false
}
// Why: Antigravity's models are named "Gemini <n.n> <Name>", so an agy pane's own
// title carries a whole `gemini` token. Gemini CLI is checked before Antigravity in
// getAgentLabel, so without this the model name wins and an agy pane reads as Gemini
// CLI. Only the token path defers — the four Gemini OSC glyphs stay decisive, and agy
// emits none of them.
if (titleHasAgentName(title, 'antigravity') || AGY_AGENT_NAME_RE.test(title)) {
return false
}
return titleHasAgentName(title, 'gemini')
}