mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
fix(agents): surface Cline panes in worktree agent rows via a declared identity frame (STA-3906)
cline writes OSC 0 `Cline` and posts no hooks, so the pane fell out of the sidebar entirely: `cline` is absent from AGENT_NAMES (deliberately — those names are token-matched and too common), so getAgentLabel and detectAgentStatusFromTitle both returned null and buildTitleDerivedAgentRow dropped the row. Rather than add a fifth hand-copied identity regex, move the shape Claude already used into a shared registry and declare Cline in it.
This commit is contained in:
@@ -202,6 +202,62 @@ describe('buildTitleDerivedAgentRows', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Why: cline 3.0.55 writes OSC 0 `Cline` and posts no hooks, so the title path is the
|
||||
// only thing that can put the pane in the sidebar at all (STA-3906 / #13823).
|
||||
it('adds an idle row for a registry-declared agent that only writes its own name', () => {
|
||||
const rows = buildWorktreeAgentRows({
|
||||
tabs: [makeTab('tab-1', { title: 'Cline' })],
|
||||
entries: [],
|
||||
retained: [],
|
||||
runtimePaneTitlesByTabId: { 'tab-1': { 1: 'Cline' } },
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-cline'] },
|
||||
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
|
||||
now: 2000
|
||||
})
|
||||
|
||||
expect(rows.map((row) => [row.agentType, row.state, row.entry.lastAssistantMessage])).toEqual([
|
||||
['cline', 'idle', 'Idle']
|
||||
])
|
||||
expect(rows[0]?.paneKey).toBe(makePaneKey('tab-1', LEAF_ID_1))
|
||||
})
|
||||
|
||||
it('does not invent a Cline row from Claude task text that mentions cline', () => {
|
||||
const unowned = buildWorktreeAgentRows({
|
||||
tabs: [makeTab('tab-1')],
|
||||
entries: [],
|
||||
retained: [],
|
||||
runtimePaneTitlesByTabId: { 'tab-1': { 1: '⠋ use cline for the sidebar fix' } },
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-claude-task'] },
|
||||
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
|
||||
now: 2000
|
||||
})
|
||||
expect(unowned).toHaveLength(0)
|
||||
|
||||
const owned = buildWorktreeAgentRows({
|
||||
tabs: [makeTab('tab-1', { launchAgent: 'claude' })],
|
||||
entries: [],
|
||||
retained: [],
|
||||
runtimePaneTitlesByTabId: { 'tab-1': { 1: '⠋ use cline for the sidebar fix' } },
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-claude-task'] },
|
||||
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
|
||||
now: 2000
|
||||
})
|
||||
expect(owned.map((row) => row.agentType)).toEqual(['claude'])
|
||||
})
|
||||
|
||||
it('keeps a cline-named worktree path out of the sidebar', () => {
|
||||
const rows = buildWorktreeAgentRows({
|
||||
tabs: [makeTab('tab-1', { title: '~/orca/workspaces/cline-scratch' })],
|
||||
entries: [],
|
||||
retained: [],
|
||||
runtimePaneTitlesByTabId: { 'tab-1': { 1: '~/orca/workspaces/cline-scratch' } },
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-shell'] },
|
||||
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
|
||||
now: 2000
|
||||
})
|
||||
expect(rows).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('attributes a spinner-only title to the launched agent when the title has no identity', () => {
|
||||
const launchAgent: TuiAgent = 'codex'
|
||||
const rows = buildWorktreeAgentRows({
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '../../../../shared/agent-title-owner'
|
||||
import { resolvePaneAgentOwner } from '../../../../shared/pane-agent-owner'
|
||||
import { isClaudeIdentityFrameTitle } from '../../../../shared/terminal-title-agent-type'
|
||||
import { ALL_TUI_AGENTS, TUI_AGENT_DISPLAY_NAMES } from '../../../../shared/tui-agent-display-names'
|
||||
|
||||
const EMPTY_RUNTIME_TITLES: Record<string, Record<number, string>> = {}
|
||||
const EMPTY_LIVE_PTY_IDS: Record<string, string[]> = {}
|
||||
@@ -44,6 +45,12 @@ const TITLE_AGENT_LABEL_TO_TYPE: Record<string, AgentType> = {
|
||||
OMP: 'omp'
|
||||
}
|
||||
|
||||
// Why: labels that already equal an agent's canonical display name need no second
|
||||
// table — registry-declared agents (agent-identity-frame.ts) resolve straight through.
|
||||
const DISPLAY_NAME_TO_AGENT_TYPE: Record<string, AgentType> = Object.fromEntries(
|
||||
ALL_TUI_AGENTS.map((agent) => [TUI_AGENT_DISPLAY_NAMES[agent], agent as AgentType])
|
||||
)
|
||||
|
||||
const CLAUDE_AGENT_TOKEN_RE = /(?<![\w./\\-])claude(?![\w./\\-])/i
|
||||
|
||||
export function buildTitleDerivedAgentRows(args: {
|
||||
@@ -201,7 +208,8 @@ export function resolveTitleDerivedAgentType(
|
||||
label: string,
|
||||
ownerAgentType?: AgentType | null
|
||||
): AgentType | null {
|
||||
const agentType = TITLE_AGENT_LABEL_TO_TYPE[label] ?? 'unknown'
|
||||
const agentType =
|
||||
TITLE_AGENT_LABEL_TO_TYPE[label] ?? DISPLAY_NAME_TO_AGENT_TYPE[label] ?? 'unknown'
|
||||
if (agentType !== 'claude') {
|
||||
return agentType
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
AGENT_IDENTITY_FRAMES,
|
||||
buildAgentIdentityFrameRe,
|
||||
isAgentIdentityFrameTitleFor,
|
||||
resolveAgentIdentityFrameType
|
||||
} from './agent-identity-frame'
|
||||
import { getAgentLabel } from './agent-title-identity'
|
||||
import { detectAgentStatusFromTitle } from './agent-title-status'
|
||||
import {
|
||||
isClaudeIdentityFrameTitle,
|
||||
resolveExplicitTerminalTitleAgentType,
|
||||
resolveTerminalTitleAgentType
|
||||
} from './terminal-title-agent-type'
|
||||
|
||||
describe('agent identity frames', () => {
|
||||
it('claims a title that is only the agent name, decorated or multiplexer-wrapped', () => {
|
||||
for (const title of ['Cline', 'cline', 'cline.cmd', 'cline.exe', '⠋ Cline', 'zsh | Cline']) {
|
||||
expect(resolveAgentIdentityFrameType(title)).toBe('cline')
|
||||
expect(isAgentIdentityFrameTitleFor(title, 'cline')).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects the name inside task text, a path, or a hyphenated compound', () => {
|
||||
for (const title of [
|
||||
'⠋ use cline for the sidebar fix',
|
||||
'port the cline prompt',
|
||||
'~/cline-scratch',
|
||||
'cline-rules',
|
||||
'src/cline/index.ts',
|
||||
'clinent'
|
||||
]) {
|
||||
expect(resolveAgentIdentityFrameType(title)).not.toBe('cline')
|
||||
expect(isAgentIdentityFrameTitleFor(title, 'cline')).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts at most one status word, never free-form trailing text', () => {
|
||||
expect(isAgentIdentityFrameTitleFor('Cline ready', 'cline')).toBe(true)
|
||||
expect(isAgentIdentityFrameTitleFor('Cline - action required', 'cline')).toBe(true)
|
||||
expect(isAgentIdentityFrameTitleFor('Cline working on the parser', 'cline')).toBe(false)
|
||||
expect(isAgentIdentityFrameTitleFor('Cline ready idle', 'cline')).toBe(false)
|
||||
})
|
||||
|
||||
it('is not declared for agents whose names are too common to anchor on', () => {
|
||||
// Why: `amp`/`pi`/`cn` would claim ordinary shell titles; the registry stays observed-only.
|
||||
expect(AGENT_IDENTITY_FRAMES.amp).toBeUndefined()
|
||||
expect(resolveAgentIdentityFrameType('amp')).toBeNull()
|
||||
expect(resolveAgentIdentityFrameType('crush')).toBeNull()
|
||||
})
|
||||
|
||||
// Why: this is the whole point of the registry — a new runtime is data, not five call sites.
|
||||
it('builds the same frame shape for any declared name', () => {
|
||||
const re = buildAgentIdentityFrameRe({ names: ['widgetcli'], executableSuffix: true })
|
||||
expect(re.test('widgetcli')).toBe(true)
|
||||
expect(re.test('widgetcli.exe')).toBe(true)
|
||||
expect(re.test('widgetcli - action required')).toBe(true)
|
||||
expect(re.test('use widgetcli here')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cline title visibility (STA-3906 / #13823)', () => {
|
||||
// Observed against cline 3.0.55: the CLI emits OSC 0 `Cline` and never varies it.
|
||||
it('resolves the live Cline title to identity, label, and an idle status', () => {
|
||||
expect(getAgentLabel('Cline')).toBe('Cline')
|
||||
expect(resolveTerminalTitleAgentType('Cline')).toBe('cline')
|
||||
expect(resolveExplicitTerminalTitleAgentType('Cline')).toBe('cline')
|
||||
expect(detectAgentStatusFromTitle('Cline')).toBe('idle')
|
||||
})
|
||||
|
||||
it('maps decorated Cline frames to status without inventing status from mentions', () => {
|
||||
expect(detectAgentStatusFromTitle('Cline ready')).toBe('idle')
|
||||
expect(detectAgentStatusFromTitle('Cline working')).toBe('working')
|
||||
expect(detectAgentStatusFromTitle('Cline - action required')).toBe('permission')
|
||||
expect(detectAgentStatusFromTitle('⠋ Cline')).toBe('working')
|
||||
for (const title of ['port the cline prompt', '~/cline-scratch', 'cline-rules']) {
|
||||
expect(detectAgentStatusFromTitle(title)).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps Claude task text that mentions cline as Claude', () => {
|
||||
expect(getAgentLabel('⠋ use cline for the sidebar fix')).toBe('Claude Code')
|
||||
expect(resolveTerminalTitleAgentType('⠋ use cline for the sidebar fix')).toBe('claude')
|
||||
// Why: the spinner still proves activity; the mention must not rebrand the pane.
|
||||
expect(detectAgentStatusFromTitle('⠋ use cline for the sidebar fix')).toBe('working')
|
||||
})
|
||||
})
|
||||
|
||||
// Why: the Claude frame moved onto the shared builder — pin the behavior it had before.
|
||||
describe('isClaudeIdentityFrameTitle after the shared-builder move', () => {
|
||||
it('accepts Claude frames and rejects Claude mentions', () => {
|
||||
for (const title of [
|
||||
'Claude',
|
||||
'claude code',
|
||||
'Claude Code',
|
||||
'✳ Claude Code',
|
||||
'Claude ready',
|
||||
'Claude Code - action required',
|
||||
'zsh | ⠋ Claude Code'
|
||||
]) {
|
||||
expect(isClaudeIdentityFrameTitle(title)).toBe(true)
|
||||
}
|
||||
for (const title of [
|
||||
'⠋ ask claude about the parser',
|
||||
'claude-scratch',
|
||||
'~/claude/worktrees',
|
||||
'Claude Code review the diff'
|
||||
]) {
|
||||
expect(isClaudeIdentityFrameTitle(title)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* One shape for "this title PRESENTS agent X", shared by every consumer that
|
||||
* turns an OSC title into agent identity.
|
||||
*
|
||||
* Why a registry and not another per-agent predicate: an identity frame is the
|
||||
* agent's own name plus, at most, one status word. That shape was already
|
||||
* hand-copied per agent (Claude here, Pi/OMP in pi-compatible-synthetic-title,
|
||||
* Cursor's closed literal set in agent-title-core), so every new runtime meant a
|
||||
* new regex threaded through five call sites. Declaring the names instead keeps
|
||||
* the next runtime a one-row change.
|
||||
*
|
||||
* Why frames and not name tokens: a name inside free-form task text is a
|
||||
* mention, not identity, and must never take a pane away from its owner
|
||||
* (#8940). Anchoring the whole title is what separates the two.
|
||||
*/
|
||||
import { stripLeadingAgentTitleDecorationOrEmpty } from './agent-title-decoration'
|
||||
import { getWrapperTitleSegments } from './terminal-title-wrapper-segments'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
|
||||
/** The status words agents append to their own name — Orca's synthetic titles
|
||||
* use the same vocabulary (synthetic-agent-title.ts), so a frame must accept both. */
|
||||
const IDENTITY_STATUS_WORDS = ['ready', 'idle', 'done', 'working', 'thinking', 'running'] as const
|
||||
|
||||
// Why: Windows titles can surface the launcher process name (`cline.cmd`).
|
||||
const EXECUTABLE_SUFFIX = String.raw`(?:\.(?:exe|cmd|bat|ps1))?`
|
||||
|
||||
export type AgentIdentityFrameSpec = {
|
||||
/** Lowercase names the agent presents itself under, longest first. */
|
||||
names: readonly string[]
|
||||
/** Accept a Windows launcher extension after the name. */
|
||||
executableSuffix?: boolean
|
||||
}
|
||||
|
||||
export function buildAgentIdentityFrameRe(spec: AgentIdentityFrameSpec): RegExp {
|
||||
const names = spec.names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
||||
const suffix = spec.executableSuffix ? EXECUTABLE_SUFFIX : ''
|
||||
return new RegExp(
|
||||
`^(?:${names})${suffix}(?:\\s+(?:${IDENTITY_STATUS_WORDS.join('|')}))?(?:\\s*-\\s*action required)?$`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Agents identified by the title they write about themselves. Only add a runtime
|
||||
* here once its CLI is observed emitting the title — an unobserved name widens
|
||||
* the false-positive surface for every ordinary shell title.
|
||||
*/
|
||||
export const AGENT_IDENTITY_FRAMES: Partial<Record<TuiAgent, AgentIdentityFrameSpec>> = {
|
||||
claude: { names: ['claude code', 'claude'] },
|
||||
// Verified against cline 3.0.55: it emits OSC 0 `Cline` and never varies it,
|
||||
// so Orca sees identity but no status transitions (STA-3906).
|
||||
cline: { names: ['cline'], executableSuffix: true }
|
||||
}
|
||||
|
||||
const IDENTITY_FRAME_RES = new Map<TuiAgent, RegExp>(
|
||||
Object.entries(AGENT_IDENTITY_FRAMES).map(([agent, spec]) => [
|
||||
agent as TuiAgent,
|
||||
buildAgentIdentityFrameRe(spec)
|
||||
])
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether `title` presents `agent`, once multiplexer prefixes and leading status
|
||||
* decoration are stripped.
|
||||
*
|
||||
* Why segments: a multiplexer prefix (`zsh | ⠋ Claude Code`) would otherwise read
|
||||
* as task text and cost a genuine agent pane its identity.
|
||||
*/
|
||||
export function isAgentIdentityFrameTitleFor(
|
||||
title: string | null | undefined,
|
||||
agent: TuiAgent
|
||||
): boolean {
|
||||
const re = IDENTITY_FRAME_RES.get(agent)
|
||||
if (!re || typeof title !== 'string') {
|
||||
return false
|
||||
}
|
||||
return getWrapperTitleSegments(title).some((segment) =>
|
||||
re.test(stripLeadingAgentTitleDecorationOrEmpty(segment).trim().toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
/** The agent a title presents, or null when the title is task text, a path, or a bare shell title. */
|
||||
export function resolveAgentIdentityFrameType(title: string | null | undefined): TuiAgent | null {
|
||||
if (typeof title !== 'string') {
|
||||
return null
|
||||
}
|
||||
for (const agent of IDENTITY_FRAME_RES.keys()) {
|
||||
if (isAgentIdentityFrameTitleFor(title, agent)) {
|
||||
return agent
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
isPiAgentTitle,
|
||||
titleHasAgentName
|
||||
} from './agent-title-core'
|
||||
import { resolveAgentIdentityFrameType } from './agent-identity-frame'
|
||||
import { isOpenCodeNativeTitle } from './opencode-terminal-title'
|
||||
import { getPiCompatibleSyntheticAgentLabel } from './pi-compatible-synthetic-title'
|
||||
import { TUI_AGENT_DISPLAY_NAMES } from './tui-agent-display-names'
|
||||
|
||||
/**
|
||||
* Returns true when the terminal title matches Claude Code's title conventions.
|
||||
@@ -32,9 +34,15 @@ export function isClaudeAgent(title: string): boolean {
|
||||
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')
|
||||
// Why: named non-Claude agents carry braille spinners too. Gate them by their
|
||||
// identity frame, not a name token, so Claude task text that merely mentions
|
||||
// one (`⠋ use cline for the fix`) stays Claude.
|
||||
const frameAgent = resolveAgentIdentityFrameType(title)
|
||||
return (
|
||||
(frameAgent === null || frameAgent === 'claude') &&
|
||||
!isCursorAgentTitle(title) &&
|
||||
!lower.includes('openclaude')
|
||||
)
|
||||
}
|
||||
|
||||
const trimmedTitle = title.trimStart()
|
||||
@@ -102,6 +110,12 @@ export function getAgentLabel(title: string): string | null {
|
||||
if (titleHasAgentName(title, 'aider')) {
|
||||
return 'Aider'
|
||||
}
|
||||
// Why: registry-declared agents identify themselves by frame only, so this must
|
||||
// run before Claude's generic braille heuristic but after the token matches above.
|
||||
const frameAgent = resolveAgentIdentityFrameType(title)
|
||||
if (frameAgent && frameAgent !== 'claude') {
|
||||
return TUI_AGENT_DISPLAY_NAMES[frameAgent]
|
||||
}
|
||||
// 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)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveAgentIdentityFrameType } from './agent-identity-frame'
|
||||
import {
|
||||
AGY_AGENT_NAME_RE,
|
||||
BRAILLE_SPINNER_RE,
|
||||
@@ -196,7 +197,16 @@ export function detectAgentStatusFromTitle(title: string): AgentStatus | null {
|
||||
const hasHermesAgentName = HERMES_AGENT_NAME_RE.test(title)
|
||||
const hasAgyAgentName = AGY_AGENT_NAME_RE.test(title)
|
||||
const hasLegacyAgentName = containsLegacyAgentName(title)
|
||||
if (!hasLegacyAgentName && !hasDroidAgentName && !hasHermesAgentName && !hasAgyAgentName) {
|
||||
// Why: registry-declared agents are deliberately absent from AGENT_NAMES — their
|
||||
// names are too common to token-match — so their whole-title frame is the evidence.
|
||||
const hasIdentityFrame = resolveAgentIdentityFrameType(title) !== null
|
||||
if (
|
||||
!hasLegacyAgentName &&
|
||||
!hasDroidAgentName &&
|
||||
!hasHermesAgentName &&
|
||||
!hasAgyAgentName &&
|
||||
!hasIdentityFrame
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (containsAny(title, ['action required', 'permission', 'waiting'])) {
|
||||
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
HERMES_AGENT_NAME_RE,
|
||||
titleHasAgentName
|
||||
} from './agent-name-token-match'
|
||||
import { isAgentIdentityFrameTitleFor, resolveAgentIdentityFrameType } from './agent-identity-frame'
|
||||
import { containsAgentSpinnerGlyph, isCursorAgentTitle } from './agent-title-core'
|
||||
import { stripLeadingAgentTitleDecorationOrEmpty } from './agent-title-decoration'
|
||||
import { isOpenCodeNativeTitle } from './opencode-terminal-title'
|
||||
import { getWrapperTitleSegments } from './terminal-title-wrapper-segments'
|
||||
import {
|
||||
getPiCompatibleSyntheticAgentLabel,
|
||||
isLegacyPiCompatibleTitle
|
||||
} from './pi-compatible-synthetic-title'
|
||||
import { TUI_AGENT_DISPLAY_NAMES, ALL_TUI_AGENTS } from './tui-agent-display-names'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
|
||||
export const CLAUDE_IDLE = '\u2733' // ✳ (eight-spoked asterisk — Claude Code idle prefix)
|
||||
@@ -97,9 +97,15 @@ export function isClaudeAgent(title: string): boolean {
|
||||
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')
|
||||
// Why: named non-Claude agents carry braille spinners too. Gate them by their
|
||||
// identity frame, not a name token, so Claude task text that merely mentions
|
||||
// one (`⠋ use cline for the fix`) stays Claude.
|
||||
const frameAgent = resolveAgentIdentityFrameType(title)
|
||||
return (
|
||||
(frameAgent === null || frameAgent === 'claude') &&
|
||||
!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
|
||||
@@ -184,6 +190,12 @@ export function getAgentLabel(title: string): string | null {
|
||||
if (titleHasAgentName(title, 'aider')) {
|
||||
return 'Aider'
|
||||
}
|
||||
// Why: registry-declared agents identify themselves by frame only, so this must
|
||||
// run before Claude's generic braille heuristic but after the token matches above.
|
||||
const frameAgent = resolveAgentIdentityFrameType(title)
|
||||
if (frameAgent && frameAgent !== 'claude') {
|
||||
return TUI_AGENT_DISPLAY_NAMES[frameAgent]
|
||||
}
|
||||
// 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)) {
|
||||
@@ -229,6 +241,12 @@ const TITLE_LABEL_TO_AGENT: Partial<Record<string, TuiAgent>> = {
|
||||
OMP: 'omp'
|
||||
}
|
||||
|
||||
// Why: labels that already equal an agent's canonical display name need no second
|
||||
// table — registry-declared agents (agent-identity-frame.ts) resolve straight through.
|
||||
const DISPLAY_NAME_TO_AGENT = new Map<string, TuiAgent>(
|
||||
ALL_TUI_AGENTS.map((agent) => [TUI_AGENT_DISPLAY_NAMES[agent], agent])
|
||||
)
|
||||
|
||||
function hasGenericClaudeStatusPrefix(title: string): boolean {
|
||||
return (
|
||||
containsAgentSpinnerGlyph(title) ||
|
||||
@@ -239,10 +257,6 @@ function hasGenericClaudeStatusPrefix(title: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
// Claude's own name plus, at most, one of its status words — never free-form task text.
|
||||
const CLAUDE_IDENTITY_FRAME_RE =
|
||||
/^claude(?: code)?(?:\s+(?:ready|idle|done|working|thinking|running))?(?:\s*-\s*action required)?$/
|
||||
|
||||
/**
|
||||
* Whether a title PRESENTS Claude rather than merely mentioning it, once its leading
|
||||
* status decoration is stripped. A "claude" token inside free-form task text is a mention,
|
||||
@@ -250,13 +264,7 @@ const CLAUDE_IDENTITY_FRAME_RE =
|
||||
* keep using `resolveExplicitTerminalTitleAgentType`, whose token match is looser.
|
||||
*/
|
||||
export function isClaudeIdentityFrameTitle(title: string): boolean {
|
||||
// Why segments: a multiplexer prefix (`zsh | ⠋ Claude Code`) would otherwise read as task
|
||||
// text and cost a genuine Claude pane its identity.
|
||||
return getWrapperTitleSegments(title).some((segment) =>
|
||||
CLAUDE_IDENTITY_FRAME_RE.test(
|
||||
stripLeadingAgentTitleDecorationOrEmpty(segment).trim().toLowerCase()
|
||||
)
|
||||
)
|
||||
return isAgentIdentityFrameTitleFor(title, 'claude')
|
||||
}
|
||||
|
||||
function isGenericClaudeStatusClaim(title: string, titleAgent: TuiAgent | null): boolean {
|
||||
@@ -269,7 +277,10 @@ function isGenericClaudeStatusClaim(title: string, titleAgent: TuiAgent | null):
|
||||
|
||||
export function resolveTerminalTitleAgentType(title: string): TuiAgent | null {
|
||||
const label = getAgentLabel(title)
|
||||
return label ? (TITLE_LABEL_TO_AGENT[label] ?? null) : null
|
||||
if (!label) {
|
||||
return null
|
||||
}
|
||||
return TITLE_LABEL_TO_AGENT[label] ?? DISPLAY_NAME_TO_AGENT.get(label) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user