From 8abe093e86e58eb2f5a1d506ae0dd091b25cb280 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:43:43 -0700 Subject: [PATCH] fix(agent-status): label Cursor by identity, not a bare "cursor" token (#8445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --------- Co-authored-by: Orca --- src/renderer/src/lib/agent-status.test.ts | 20 +++++++++ src/shared/agent-title-identity.ts | 10 +++-- src/shared/terminal-title-agent-type.test.ts | 44 +++++++++++++++++++- src/shared/terminal-title-agent-type.ts | 18 ++++---- 4 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/lib/agent-status.test.ts b/src/renderer/src/lib/agent-status.test.ts index 820741031..327f080dc 100644 --- a/src/renderer/src/lib/agent-status.test.ts +++ b/src/renderer/src/lib/agent-status.test.ts @@ -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() diff --git a/src/shared/agent-title-identity.ts b/src/shared/agent-title-identity.ts index 89dbded12..9f7746b01 100644 --- a/src/shared/agent-title-identity.ts +++ b/src/shared/agent-title-identity.ts @@ -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)) { diff --git a/src/shared/terminal-title-agent-type.test.ts b/src/shared/terminal-title-agent-type.test.ts index 6c9918394..d28dd43f5 100644 --- a/src/shared/terminal-title-agent-type.test.ts +++ b/src/shared/terminal-title-agent-type.test.ts @@ -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) + }) }) diff --git a/src/shared/terminal-title-agent-type.ts b/src/shared/terminal-title-agent-type.ts index e40098ee4..ae7c97c09 100644 --- a/src/shared/terminal-title-agent-type.ts +++ b/src/shared/terminal-title-agent-type.ts @@ -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.