Files
orca/src/shared/agent-title-identity.ts
T
Neil 5ee90c8d59 fix(agents): recognize OpenCode native OC | tab titles (#9102)
* fix(agents): recognize OpenCode native OC | tab titles

OpenCode's native OSC titles use `OC | <task>` without an `opencode`
token, so title classifiers left tabs as Claude/unknown. Map the native
marker (optional mux prefix) to OpenCode identity in both title
classifiers, exclude it from isClaudeAgent, and cover lookalikes plus
stale Claude launch reclaim.

Builds on and supersedes #8590 (credit @gatsby74). Fixes #8478.

* fix(agents): drop renderer import from #8478 shared repro

tsconfig.node includes src/shared tests; importing agent-status pulled
renderer modules outside the node project and failed typecheck. Assert
opencode identity via shared title classifiers only (OpenCode TUI sets
"OpenCode" and `OC | ${title}`).

* fix(runtime): fill browser cert failure map in mobile snapshot fixtures

Main's #9104 reads browserCertificateFailuresByPageId in buildMobileBrowserTab
but left partial AppState test helpers without the field, breaking PR Checks
merge commits. Default the map in fixtures and use optional chaining so partial
state cannot throw.
2026-07-16 19:53:18 -07:00

122 lines
3.6 KiB
TypeScript

import {
AGY_AGENT_NAME_RE,
CLAUDE_IDLE,
DROID_AGENT_NAME_RE,
HERMES_AGENT_NAME_RE,
containsBrailleSpinner,
isClaudeManagementTitle,
isCursorAgentTitle,
isGeminiTerminalTitle,
isPiAgentTitle,
titleHasAgentName
} from './agent-title-core'
import { isOpenCodeNativeTitle } from './opencode-terminal-title'
import { getPiCompatibleSyntheticAgentLabel } from './pi-compatible-synthetic-title'
/**
* Returns true when the terminal title matches Claude Code's title conventions.
* Used to scope prompt-cache-timer behavior to Claude sessions only.
*/
export function isClaudeAgent(title: string): boolean {
if (!title || isClaudeManagementTitle(title) || isOpenCodeNativeTitle(title)) {
return false
}
const lower = title.toLowerCase()
// Why: Claude title prefixes are stronger than task text, which can mention
// other agents without changing the owning CLI.
if (title.startsWith(`${CLAUDE_IDLE} `) || title === CLAUDE_IDLE) {
return true
}
if (title.startsWith('. ') || title.startsWith('* ')) {
return true
}
if (containsBrailleSpinner(title)) {
// Why: named non-Claude agents carry braille spinners too. Gate Cursor by its
// identity title, not the token, so a Claude title mentioning a cursor stays Claude.
return !isCursorAgentTitle(title) && !lower.includes('openclaude')
}
const trimmedTitle = title.trimStart()
return (
trimmedTitle.toLowerCase().startsWith('claude') && titleHasAgentName(trimmedTitle, 'claude')
)
}
export function getAgentLabel(title: string): string | null {
if (isClaudeManagementTitle(title)) {
return null
}
// Why: the native marker owns the whole title; its session text may name or
// include status glyphs from other agents without changing OpenCode identity.
if (isOpenCodeNativeTitle(title)) {
return 'OpenCode'
}
// Why: Claude task titles can mention another CLI; the prefix is the identity
// signal, not arbitrary task text.
if (
title.startsWith(`${CLAUDE_IDLE} `) ||
title === CLAUDE_IDLE ||
title.startsWith('. ') ||
title.startsWith('* ')
) {
return 'Claude Code'
}
if (isGeminiTerminalTitle(title)) {
return 'Gemini CLI'
}
// Why: Pi-compatible synthetic titles can carry braille spinners, which the
// generic agent-title heuristics would otherwise claim first.
const piCompatibleSyntheticAgentLabel = getPiCompatibleSyntheticAgentLabel(title)
if (piCompatibleSyntheticAgentLabel) {
return piCompatibleSyntheticAgentLabel
}
if (isPiAgentTitle(title)) {
return 'Pi'
}
if (titleHasAgentName(title, 'codex')) {
return 'Codex'
}
if (titleHasAgentName(title, 'openclaude')) {
return 'OpenClaude'
}
if (titleHasAgentName(title, 'copilot')) {
return 'GitHub Copilot'
}
if (titleHasAgentName(title, 'grok')) {
return 'Grok'
}
if (titleHasAgentName(title, 'devin')) {
return 'Devin'
}
if (titleHasAgentName(title, 'antigravity') || AGY_AGENT_NAME_RE.test(title)) {
return 'Antigravity'
}
if (titleHasAgentName(title, 'opencode')) {
return 'OpenCode'
}
if (titleHasAgentName(title, 'mimo')) {
return 'MiMo Code'
}
if (titleHasAgentName(title, 'aider')) {
return 'Aider'
}
// Why: `cursor` is ordinary editor vocabulary, not identity. Match Cursor's closed
// title set (mirrors @cursor routing), before `isClaudeAgent` claims the braille frame.
if (isCursorAgentTitle(title)) {
return 'Cursor'
}
if (DROID_AGENT_NAME_RE.test(title)) {
return 'Droid'
}
if (HERMES_AGENT_NAME_RE.test(title)) {
return 'Hermes'
}
if (isClaudeAgent(title)) {
return 'Claude Code'
}
return null
}