fix(agent-status): label Cursor by identity, not a bare "cursor" token (#8445)

* fix(agent-status): label Cursor by identity, not a bare "cursor" token

The worktree card, status bar, and mobile all derive an agent label from the
terminal title via getAgentLabel / resolveTerminalTitleAgentType. Both matched
Cursor with `titleHasAgentName(title, 'cursor')`, a whole-token match. But
`cursor` is ordinary editor vocabulary, so a Claude/Codex tab working on Orca's
own code (title like `⠋ preserve cursor visibility across replays`) got
mislabeled as Cursor. The generic braille-spinner Claude fallback even had a
`!lower.includes('cursor')` guard that then dropped the title to no label at
all.

Gate Cursor on its closed identity title set (`isCursorAgentTitle`) instead —
the same predicate @cursor orchestration routing uses. A real cursor-agent
terminal still resolves as Cursor across working/idle/permission; a non-Cursor
tab that merely mentions a text cursor reverts to its true agent. Relax the
braille guard to the same predicate so those titles land on Claude, not null.

Makes display consistent with routing (the follow-up flagged in #8436).

* refactor(agent-status): address review on Cursor identity labeling

- Trim the four Cursor `// Why:` comments in both parallel resolvers
  (agent-title-identity.ts, terminal-title-agent-type.ts) to AGENTS.md's
  one-to-two-line rule; use identical wording so future drift is visible.
- Add a direct isClaudeAgent assertion in terminal-title-agent-type.test.ts
  pinning that file's parallel copy (previously only covered transitively),
  plus Cursor Agent / "Cursor - action required" activity-facet assertions.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing 2026-07-12 19:43:43 -07:00 committed by GitHub
parent 4a76a1c894
commit 8abe093e86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 77 additions and 15 deletions

View File

@ -533,6 +533,19 @@ describe('getAgentLabel', () => {
expect(getAgentLabel('Aider idle')).toBe('Aider')
expect(getAgentLabel('Devin working')).toBe('Devin')
})
// Why: `cursor` is ordinary editor vocabulary in another agent's task title, so a
// bare token is not Cursor identity — a Claude tab working on cursor code was being
// mislabeled Cursor on the worktree card. Match Cursor's closed title set only.
it('labels Cursor by identity, not a bare "cursor" token in another agent title', () => {
expect(getAgentLabel('Cursor Agent')).toBe('Cursor')
expect(getAgentLabel('⠋ Cursor Agent')).toBe('Cursor')
expect(getAgentLabel('Cursor ready')).toBe('Cursor')
expect(getAgentLabel('Cursor - action required')).toBe('Cursor')
expect(getAgentLabel('⠋ preserve cursor visibility across replays')).toBe('Claude Code')
expect(getAgentLabel('⠋ Codex: fix cursor offsets')).toBe('Codex')
expect(getAgentLabel('Terminal Cursor and Orca slows down')).toBeNull()
})
})
describe('isClaudeAgent', () => {
@ -542,6 +555,13 @@ describe('isClaudeAgent', () => {
expect(isClaudeAgent('OpenClaude ready')).toBe(false)
})
// Why: a braille Claude title merely mentioning a text cursor is still Claude, so its
// prompt-cache paths must fire; only Cursor's own identity titles are excluded.
it('counts cursor-mentioning Claude braille titles as Claude, excludes real Cursor', () => {
expect(isClaudeAgent('⠋ preserve cursor visibility across replays')).toBe(true)
expect(isClaudeAgent('⠋ Cursor Agent')).toBe(false)
})
it('does not classify non-prefix Claude mentions as Claude agent titles', () => {
expect(isClaudeAgent('ask claude later')).toBe(false)
expect(getAgentLabel('ask claude later')).toBeNull()

View File

@ -5,6 +5,7 @@ import {
HERMES_AGENT_NAME_RE,
containsBrailleSpinner,
isClaudeManagementTitle,
isCursorAgentTitle,
isGeminiTerminalTitle,
isPiAgentTitle,
titleHasAgentName
@ -30,7 +31,9 @@ export function isClaudeAgent(title: string): boolean {
return true
}
if (containsBrailleSpinner(title)) {
return !lower.includes('cursor') && !lower.includes('openclaude')
// 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()
@ -93,8 +96,9 @@ export function getAgentLabel(title: string): string | null {
if (titleHasAgentName(title, 'aider')) {
return 'Aider'
}
// Why: match explicit names before Claude's generic braille heuristic.
if (titleHasAgentName(title, 'cursor')) {
// 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)) {

View File

@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest'
import {
isClaudeAgent,
isGrokRotatingWorkingTitle,
resolveExplicitTerminalTitleAgentType
resolveExplicitTerminalTitleAgentType,
resolveTerminalTitleAgentType
} from './terminal-title-agent-type'
describe('isGrokRotatingWorkingTitle', () => {
@ -57,4 +59,44 @@ describe('resolveExplicitTerminalTitleAgentType', () => {
expect(resolveExplicitTerminalTitleAgentType('Terminal 1')).toBeNull()
expect(resolveExplicitTerminalTitleAgentType('zsh')).toBeNull()
})
// Why: `cursor` is ordinary editor vocabulary, so a name token is not identity.
// A Claude/Codex tab working on cursor code must not commit to Cursor identity.
it('resolves Cursor by its identity titles, never a bare cursor token', () => {
expect(resolveExplicitTerminalTitleAgentType('Cursor Agent')).toBe('cursor')
expect(resolveExplicitTerminalTitleAgentType('⠋ Cursor Agent')).toBe('cursor')
expect(resolveExplicitTerminalTitleAgentType('Cursor ready')).toBe('cursor')
expect(resolveExplicitTerminalTitleAgentType('Cursor - action required')).toBe('cursor')
// A Claude tab whose task text mentions a text cursor is not Cursor identity.
expect(
resolveExplicitTerminalTitleAgentType('⠋ preserve cursor visibility across replays')
).toBeNull()
expect(resolveExplicitTerminalTitleAgentType('~/cursor-rules')).toBeNull()
})
})
describe('resolveTerminalTitleAgentType', () => {
// Why: the activity facet keeps Claude's braille prefix as Claude — but only when
// the "cursor" it mentions is task text, not Cursor's own identity title.
it('labels cursor-mentioning agent tabs by their true agent, real Cursor as cursor', () => {
expect(resolveTerminalTitleAgentType('⠋ Cursor Agent')).toBe('cursor')
expect(resolveTerminalTitleAgentType('Cursor Agent')).toBe('cursor')
expect(resolveTerminalTitleAgentType('Cursor ready')).toBe('cursor')
expect(resolveTerminalTitleAgentType('Cursor - action required')).toBe('cursor')
expect(resolveTerminalTitleAgentType('⠋ preserve cursor visibility across replays')).toBe(
'claude'
)
expect(resolveTerminalTitleAgentType('⠋ Codex: fix cursor offsets')).toBe('codex')
})
})
// Why: this module carries its own isClaudeAgent copy parallel to agent-title-identity.ts;
// both got the identical isCursorAgentTitle guard, so pin this copy directly to catch drift.
describe('isClaudeAgent', () => {
it('excludes real Cursor identity titles, keeps cursor-mentioning Claude braille titles', () => {
expect(isClaudeAgent('⠋ Cursor Agent')).toBe(false)
expect(isClaudeAgent('Cursor ready')).toBe(false)
expect(isClaudeAgent('⠋ preserve cursor visibility across replays')).toBe(true)
expect(isClaudeAgent('⠋ OpenClaude')).toBe(false)
})
})

View File

@ -4,6 +4,7 @@ import {
HERMES_AGENT_NAME_RE,
titleHasAgentName
} from './agent-name-token-match'
import { isCursorAgentTitle } from './agent-title-core'
import {
getPiCompatibleSyntheticAgentLabel,
isLegacyPiCompatibleTitle
@ -97,9 +98,9 @@ export function isClaudeAgent(title: string): boolean {
return true
}
if (containsBrailleSpinner(title)) {
// Why: named non-Claude agents can carry braille spinners too; Claude-only
// prompt-cache paths must not fire for those explicit agent titles.
return !lower.includes('cursor') && !lower.includes('openclaude')
// 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')
}
// Why: permission/action-required Claude titles can omit the usual prefixes.
// Token-match so cwd/worktree titles like "claude-scratch" do not become
@ -179,14 +180,9 @@ export function getAgentLabel(title: string): string | null {
if (titleHasAgentName(title, 'aider')) {
return 'Aider'
}
// Why: the cursor-agent native title is the literal string "Cursor Agent"
// (verified against the 2026.04.17 release) — Orca synthesizes the same
// label from hook events so the braille-spinner + agent-name path lights
// up working/permission/idle transitions in the renderer. Match before
// `isClaudeAgent` because Claude's generic braille heuristic would
// otherwise claim every "⠋ Cursor Agent" frame as Claude. Token-match so a
// cwd like "~/cursor-rules" can't masquerade as a Cursor agent.
if (titleHasAgentName(title, 'cursor')) {
// 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'
}
// Why: synthesized "⠋ Droid" working title needs to be matched before Claude's braille heuristic.