Prevent OMP terminals from being mislabeled as Gemini (#7447)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-07-05 13:39:38 -07:00 committed by GitHub
parent dbf1da0f44
commit 72a23809fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 87 additions and 15 deletions

View File

@ -247,9 +247,10 @@ function notifyStoreSubscribers(): void {
vi.mock('@/lib/agent-status', async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>()
const isGeminiTerminalTitle = actual.isGeminiTerminalTitle as (title: string) => boolean
return {
...actual,
isGeminiTerminalTitle: vi.fn(() => false),
isGeminiTerminalTitle: vi.fn((title: string) => isGeminiTerminalTitle(title)),
isClaudeAgent: vi.fn(() => false),
detectAgentStatusFromTitle: vi.fn((title: string) => {
if (/Claude (working|done)/.test(title)) {
@ -12612,6 +12613,39 @@ describe('connectPanePty', () => {
)
})
it('keeps GPU rendering enabled for OMP titles whose cwd is Gemini', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-omp-gemini-cwd')
transportFactoryQueue.push(transport)
enableActiveRuntimeEnvironment()
mockStoreState.tabsByWorktree = {
'wt-1': [{ id: 'tab-1', ptyId: 'tab-pty', launchAgent: 'omp' }]
}
const pane = createPane(1)
const manager = createManager(1)
manager.getActivePane.mockReturnValue({ id: 1 })
const deps = createDeps()
connectPanePty(pane as never, manager as never, deps as never)
const titleHandler = createdTransportOptions[0]?.onTitleChange as
| ((title: string, rawTitle: string) => void)
| undefined
if (!titleHandler) {
throw new Error('Expected onTitleChange to be registered')
}
manager.setPaneGpuRendering.mockClear()
titleHandler('\u280b Pi', '\u280b π: gemini')
expect(manager.setPaneGpuRendering).toHaveBeenCalledTimes(1)
expect(manager.setPaneGpuRendering).toHaveBeenCalledWith(1, true)
expect(deps.setRuntimePaneTitle).toHaveBeenCalledWith('tab-1', 1, '\u280b OMP')
expect(deps.updateTabTitle).toHaveBeenCalledWith('tab-1', '\u280b OMP')
})
it('leaves local IPC OSC 9999 status ownership in the main runtime', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-local')

View File

@ -397,6 +397,22 @@ describe('normalizeTerminalTitle', () => {
it('collapses Pi spinner and idle titles to stable labels', () => {
expect(normalizeTerminalTitle('⠋ π - my-project')).toBe('⠋ Pi')
expect(normalizeTerminalTitle('π - my-project')).toBe('Pi')
expect(normalizeTerminalTitle('⠋ π: my-project')).toBe('⠋ Pi')
expect(normalizeTerminalTitle('π: my-project')).toBe('Pi')
expect(normalizeTerminalTitle('π -')).toBe('Pi')
expect(normalizeTerminalTitle('π:')).toBe('Pi')
expect(normalizeTerminalTitle('π ')).toBe('Pi')
})
it('does not collapse Pi-compatible titles whose cwd mentions Gemini', () => {
expect(normalizeTerminalTitle('⠋ π - gemini')).toBe('⠋ Pi')
expect(normalizeTerminalTitle('π - gemini')).toBe('Pi')
expect(normalizeTerminalTitle('⠋ π: gemini')).toBe('⠋ Pi')
expect(normalizeTerminalTitle('π: gemini')).toBe('Pi')
expect(normalizeTerminalTitle('⠋ π gemini')).toBe('⠋ Pi')
expect(normalizeTerminalTitle('π gemini')).toBe('Pi')
expect(normalizeTerminalTitle('⠋ π - gemini-project')).toBe('⠋ Pi')
expect(normalizeTerminalTitle('π - gemini-project')).toBe('Pi')
})
})
@ -409,6 +425,17 @@ describe('isGeminiTerminalTitle', () => {
it('does not match other terminal titles', () => {
expect(isGeminiTerminalTitle('⠂ Claude Code')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π - gemini')).toBe(false)
expect(isGeminiTerminalTitle('π - gemini')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π: gemini')).toBe(false)
expect(isGeminiTerminalTitle('π: gemini')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π gemini')).toBe(false)
expect(isGeminiTerminalTitle('π gemini')).toBe(false)
expect(isGeminiTerminalTitle('π -')).toBe(false)
expect(isGeminiTerminalTitle('π:')).toBe(false)
expect(isGeminiTerminalTitle('π ')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π - gemini-project')).toBe(false)
expect(isGeminiTerminalTitle('/tmp/gemini/working')).toBe(false)
expect(isGeminiTerminalTitle('bash')).toBe(false)
})
})

View File

@ -16,7 +16,8 @@ import {
} from './agent-name-token-match'
import {
getPiCompatibleSyntheticAgentLabel,
getPiCompatibleSyntheticAgentStatus
getPiCompatibleSyntheticAgentStatus,
isLegacyPiCompatibleTitle
} from './pi-compatible-synthetic-title'
// Re-export so existing `agent-detection` importers keep working.
@ -96,26 +97,30 @@ export const STRONG_WORKING_KEYWORDS_RE = new RegExp(
// compounds like "is-working-cap" that `detectAgentStatusFromTitle` would
// correctly refuse to classify as working.
const STRONG_WORKING_KEYWORDS_RE_GLOBAL = new RegExp(STRONG_WORKING_KEYWORDS_RE.source, 'gi')
const PI_IDLE_PREFIX = '\u03c0 - ' // π - (Pi titlebar extension idle format)
export function isGeminiTerminalTitle(title: string): boolean {
return (
// Why: Gemini OSC glyphs are stronger evidence than any cwd/session text.
if (
title.includes(GEMINI_PERMISSION) ||
title.includes(GEMINI_WORKING) ||
title.includes(GEMINI_SILENT_WORKING) ||
title.includes(GEMINI_IDLE) ||
title.toLowerCase().includes('gemini')
)
title.includes(GEMINI_IDLE)
) {
return true
}
// Why: Pi/OMP titles include cwd/session text; substring matching made
// paths like "gemini-project" masquerade as Gemini CLI.
if (isPiAgentTitle(title)) {
return false
}
return titleHasAgentName(title, 'gemini')
}
export function isPiTerminalTitle(title: string): boolean {
return title.startsWith(PI_IDLE_PREFIX)
return isLegacyPiCompatibleTitle(title) && !containsBrailleSpinner(title)
}
function isPiAgentTitle(title: string): boolean {
return (
isPiTerminalTitle(title) || (containsBrailleSpinner(title) && title.includes(PI_IDLE_PREFIX))
)
return isLegacyPiCompatibleTitle(title)
}
function containsBrailleSpinner(title: string): boolean {

View File

@ -5,13 +5,13 @@ import {
SYNTHETIC_AGENT_TITLE_PROFILES,
type SyntheticAgentTitleProfile
} from './synthetic-agent-title'
import { isLegacyPiCompatibleTitle } from './pi-compatible-synthetic-title'
type TitleProfileMatch = {
profile: SyntheticAgentTitleProfile
}
const COMPATIBLE_IDLE_TITLE_RE = /(?<![\w./\\-])(?:ready|idle|done)(?![\w-])/i
const LEGACY_PI_COMPATIBLE_TITLE_RE = /^\s*(?:[\u2800-\u28ff]\s+)?π\s*(?:[-:]|\s)\s*.+/u
/**
* Resolves the synthetic title profile matching a given agent label.
@ -37,7 +37,7 @@ function getProfileForTitle(title: string): TitleProfileMatch | null {
if (labelProfile) {
return labelProfile
}
if (LEGACY_PI_COMPATIBLE_TITLE_RE.test(title)) {
if (isLegacyPiCompatibleTitle(title)) {
return getProfileForTitleLabel('Pi')
}
return null
@ -60,7 +60,7 @@ function getSourceTitleStatus(title: string): 'working' | 'permission' | 'idle'
if (detectedStatus) {
return detectedStatus
}
if (LEGACY_PI_COMPATIBLE_TITLE_RE.test(title)) {
if (isLegacyPiCompatibleTitle(title)) {
return 'idle'
}
return null

View File

@ -4,6 +4,8 @@ export type PiCompatibleSyntheticAgentStatus = 'working' | 'permission' | 'idle'
const PI_COMPATIBLE_SYNTHETIC_TITLE_RE =
/^\s*(?:[\u2800-\u28ff]\s+)?(pi|omp)(?:\s+-\s+action required|\s+(?:ready|idle|done))?\s*$/i
const PI_COMPATIBLE_IDLE_RE = /(?<![\w./\\-])(?:ready|idle|done)(?![\w-])/i
// Why: legacy Pi/OMP-compatible shells can emit the delimiter before cwd text exists.
const LEGACY_PI_COMPATIBLE_TITLE_RE = /^\s*(?:[\u2800-\u28ff]\s+)?π(?:\s*[-:]|\s)\s*.*$/u
function containsBrailleSpinner(title: string): boolean {
for (const char of title) {
@ -47,3 +49,7 @@ export function getPiCompatibleSyntheticAgentStatus(
}
return null
}
export function isLegacyPiCompatibleTitle(title: string): boolean {
return LEGACY_PI_COMPATIBLE_TITLE_RE.test(title)
}