import { GEMINI_WORKING, GEMINI_SILENT_WORKING, GEMINI_IDLE, GEMINI_PERMISSION, containsBrailleSpinner } from './agent-title-glyphs' export { CLAUDE_IDLE, GEMINI_WORKING, GEMINI_SILENT_WORKING, GEMINI_IDLE, GEMINI_PERMISSION, BRAILLE_SPINNER_RE, QUARTER_CIRCLE_SPINNER_RE, containsBrailleSpinner, containsQuarterCircleSpinner, containsAgentSpinnerGlyph } from './agent-title-glyphs' import { isDeepSeekBuildTerminalTitle } from './dsb-terminal-title' import { isDshTerminalTitle } from './dsh-terminal-title' export { DSH_WHALE, isDshTerminalTitle } from './dsh-terminal-title' import { isQoderTerminalTitle } from './qoder-terminal-title' import { AGY_AGENT_NAME_RE, DROID_AGENT_NAME_RE, HERMES_AGENT_NAME_RE, titleHasAgentName, titleHasAnyLegacyAgentName } from './agent-name-token-match' import { stripLeadingAgentTitleDecorationOrEmpty } from './agent-title-decoration' import { isLegacyPiCompatibleTitle } from './pi-compatible-synthetic-title' import { memoizeTitleClassification } from './terminal-title-classification-memo' import { getWrapperTitleSegments } from './terminal-title-wrapper-segments' export { AGY_AGENT_NAME_RE, DROID_AGENT_NAME_RE, HERMES_AGENT_NAME_RE, titleHasAgentName } export type AgentStatus = 'working' | 'permission' | 'idle' const CLAUDE_COMMAND_RE = String.raw`(?:.*[\\/])?claude(?:\.(?:exe|cmd|bat|ps1))?` export const CLAUDE_MANAGEMENT_TITLE_RE = new RegExp( String.raw`^\s*(?:"${CLAUDE_COMMAND_RE}"|'${CLAUDE_COMMAND_RE}'|${CLAUDE_COMMAND_RE})\s+agents\s*$`, 'i' ) const STRONG_IDLE_KEYWORDS = ['ready', 'idle', 'done'] as const const STRONG_WORKING_KEYWORDS = ['working', 'thinking', 'running'] as const // Why: plain `\b` matches inside hyphenated tokens and cwd paths such as // "~/codex/ready"; the left side also blocks path separators for Windows/Unix. export const STRONG_IDLE_KEYWORDS_RE = new RegExp( `(? ", 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') } /** Pure in `title` — memoized so repeated selector reads skip the regex ladder. */ export const isGeminiTerminalTitle: (title: string) => boolean = memoizeTitleClassification( computeIsGeminiTerminalTitle ) export function isPiTerminalTitle(title: string): boolean { return isLegacyPiCompatibleTitle(title) && !containsBrailleSpinner(title) } export function isPiAgentTitle(title: string): boolean { return isLegacyPiCompatibleTitle(title) } export function containsLegacyAgentName(title: string): boolean { return titleHasAnyLegacyAgentName(title) } export function containsAgentName(title: string): boolean { return ( containsLegacyAgentName(title) || AGY_AGENT_NAME_RE.test(title) || DROID_AGENT_NAME_RE.test(title) || HERMES_AGENT_NAME_RE.test(title) ) } export function containsAny(title: string, words: readonly string[]): boolean { const lower = title.toLowerCase() return words.some((word) => lower.includes(word)) } export function isClaudeManagementTitle(title: string): boolean { return CLAUDE_MANAGEMENT_TITLE_RE.test(title) } export function isCursorNativeAgentTitle(title: string): boolean { return title.trim().toLowerCase() === CURSOR_NATIVE_TITLE_LOWER } const CLAUDE_IDENTITY_FRAME_RE = /^claude(?: code)?(?:\s+(?:ready|idle|done|working|thinking|running))?(?:\s*-\s*action required)?$/ export function isClaudeIdentityFrameSegment(title: string): boolean { return CLAUDE_IDENTITY_FRAME_RE.test( stripLeadingAgentTitleDecorationOrEmpty(title).trim().toLowerCase() ) } export function isClaudeIdentityFrameTitle(title: string): boolean { return getWrapperTitleSegments(title).some(isClaudeIdentityFrameSegment) } // Why: `cursor` is also an ordinary editor noun that other agents type into their own // task-summary titles, so a name token is not identity. Cursor's identifying titles are // a closed set (the native literal plus the labels Orca synthesizes from Cursor hooks), // so match that vocabulary instead. export function isCursorAgentTitle(title: string | null | undefined): boolean { if (typeof title !== 'string') { return false } const trimmed = title.trim() const lower = trimmed.toLowerCase() if ( lower === CURSOR_NATIVE_TITLE_LOWER || lower === 'cursor ready' || lower === 'cursor - action required' ) { return true } // Why: display labels can mention Cursor in another agent's task text. Only // treat the controlled synthetic Cursor spinner title as Cursor identity. return /^[\u2800-\u28ff] Cursor Agent$/u.test(trimmed) } // Why: cursor-agent re-emits its bare native title every redraw, which would stomp // Orca's hook-synthesized spinner state, but only once a Cursor-owned title already // owns the pane. A hookless Cursor pane still needs the literal once, for identity. export function shouldSuppressCursorNativeTitle(lastEmittedTitle: string | null): boolean { return lastEmittedTitle !== null && isCursorAgentTitle(lastEmittedTitle) }