mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 16:03:53 +00:00
* fix(agent-title): treat Claude Code quarter-circle spinners as working
Claude Code 2.1.228 swapped its busy OSC title spinner from braille
(U+2800-U+28FF) to quarter circles (U+25D0/U+25D1). Orca recognized a busy
Claude title only by braille codepoints, so the new frames matched nothing.
The summary-bearing busy frame ("<glyph> Say hi in one word") carries no
"claude" name token, so it resolved to no-status. The tracker's "idle or
permission followed by no-status means the agent exited" rule then fired
mid-turn, confirmPtyAgentExit confirmed it, and the chat surface routed
exitChat -- kicking the tab to the terminal view on every message.
Widen the accepted glyph set via a shared containsAgentSpinnerGlyph helper.
Agent-specific braille frame shapes (Grok, Pi, synthetic Cursor) stay pinned
to their own glyph set.
Fixes #13889
* fix(agent-title): satisfy static analysis and trim scope
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
import {
|
|
AGY_AGENT_NAME_RE,
|
|
CLAUDE_IDLE,
|
|
DROID_AGENT_NAME_RE,
|
|
HERMES_AGENT_NAME_RE,
|
|
containsAgentSpinnerGlyph,
|
|
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 (containsAgentSpinnerGlyph(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
|
|
}
|