mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
* fix(omp): read Pi/OMP static state-title markers and retire stale spinners OMP 17.2.12 replaced its animated braille title frames with static markers on WSL/ConPTY (`π : working`, `π > idle`, `π ! needs input`). Orca read all three as idle, so a working OMP pane lost its status, and a synthetic title spinner started by an earlier hook kept rotating after its status row was gone. Classify the markers from one shared table so a later upstream punctuation change is a row, not a reparse, and stop the spinner when the hook row it stands in for is cleared or dismissed. Fixes #13890 * test(omp): preserve static state titles during normalization --------- Co-authored-by: Merge Sim <sim@local>
317 lines
12 KiB
TypeScript
317 lines
12 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
detectAgentStatusFromTitle,
|
|
extractAllOscTitles,
|
|
extractLastOscTitle,
|
|
getAgentLabel,
|
|
isCursorAgentTitle,
|
|
MAX_OSC_TITLE_CHARS,
|
|
MAX_OSC_TITLES_PER_CHUNK,
|
|
normalizeTerminalTitle
|
|
} from './agent-detection'
|
|
import {
|
|
hasCompatibleAgentTitleIdentity,
|
|
normalizeCompatibleAgentStatusEntryForOwner,
|
|
normalizeCompatibleAgentTitleForOwner,
|
|
resolveCompatibleAgentTypeForOwner
|
|
} from './agent-title-owner'
|
|
import { SYNTHETIC_AGENT_TITLE_PROFILES } from './synthetic-agent-title'
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('OSC title extraction', () => {
|
|
it('extracts the last OSC title from BEL-terminated PTY data', () => {
|
|
expect(extractLastOscTitle('\x1b]0;First\x07noise\x1b]2;Second\x07')).toBe('Second')
|
|
})
|
|
|
|
it('extracts all OSC titles including ST-terminated titles', () => {
|
|
expect(extractAllOscTitles('\x1b]0;First\x1b\\noise\x1b]2;Second\x07')).toEqual([
|
|
'First',
|
|
'Second'
|
|
])
|
|
})
|
|
|
|
it('ignores incomplete OSC titles until a later chunk supplies the terminator', () => {
|
|
expect(extractAllOscTitles('\x1b]0;Incomplete title')).toEqual([])
|
|
expect(extractLastOscTitle('\x1b]0;Incomplete title')).toBeNull()
|
|
})
|
|
|
|
it('recovers when an abandoned incomplete OSC title is followed by a fresh title', () => {
|
|
const data = '\x1b]0;abandoned\x1b]0;Fresh title\x07'
|
|
|
|
expect(extractLastOscTitle(data)).toBe('Fresh title')
|
|
expect(extractAllOscTitles(data)).toEqual(['Fresh title'])
|
|
})
|
|
|
|
it('scans large PTY chunks without regex match iteration', () => {
|
|
const matchAll = vi.spyOn(String.prototype, 'matchAll')
|
|
const data = `${'pasted terminal noise \x1b]x;ignored\x07 '.repeat(10_000)}\x1b]0;Agent working\x07`
|
|
|
|
expect(extractLastOscTitle(data)).toBe('Agent working')
|
|
expect(extractAllOscTitles(data).at(-1)).toBe('Agent working')
|
|
expect(matchAll).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('caps oversized OSC titles before downstream title processing', () => {
|
|
const title = `${'a'.repeat(MAX_OSC_TITLE_CHARS)}${'b'.repeat(10_000)}`
|
|
const data = `before\x1b]0;${title}\x07after`
|
|
|
|
const extracted = extractLastOscTitle(data)
|
|
|
|
expect(extracted).toHaveLength(MAX_OSC_TITLE_CHARS)
|
|
expect(extracted?.startsWith('a'.repeat(MAX_OSC_TITLE_CHARS / 2))).toBe(true)
|
|
expect(extracted?.endsWith('b'.repeat(MAX_OSC_TITLE_CHARS / 2))).toBe(true)
|
|
expect(extractAllOscTitles(data)).toEqual([extracted])
|
|
})
|
|
|
|
it('retains only the newest titles when one chunk contains limit +1', () => {
|
|
const data = Array.from(
|
|
{ length: MAX_OSC_TITLES_PER_CHUNK + 1 },
|
|
(_, index) => `\x1b]0;title-${index}\x07`
|
|
).join('')
|
|
|
|
const titles = extractAllOscTitles(data)
|
|
|
|
expect(titles).toHaveLength(MAX_OSC_TITLES_PER_CHUNK)
|
|
expect(titles[0]).toBe('title-1')
|
|
expect(titles.at(-1)).toBe(`title-${MAX_OSC_TITLES_PER_CHUNK}`)
|
|
})
|
|
})
|
|
|
|
describe('MiMo title detection', () => {
|
|
it.each([
|
|
['MiMo Code', 'idle'],
|
|
['mimo ready', 'idle'],
|
|
['mimo working', 'working'],
|
|
['\u280b MiMo Code', 'working']
|
|
] as const)('classifies %s', (title, expectedStatus) => {
|
|
expect(getAgentLabel(title)).toBe('MiMo Code')
|
|
expect(detectAgentStatusFromTitle(title)).toBe(expectedStatus)
|
|
})
|
|
|
|
it.each(['~/mimo/working', 'mimo-code-fixtures ready'])(
|
|
'does not classify path or hyphen false positive %s',
|
|
(title) => {
|
|
expect(getAgentLabel(title)).toBeNull()
|
|
expect(detectAgentStatusFromTitle(title)).toBeNull()
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('OpenCode native title detection', () => {
|
|
// Why: `OC | …` names no agent token, so title-derived display and target surfaces
|
|
// previously dropped OpenCode panes. Runtime sends corroborate the title separately.
|
|
it.each(['OC | Implement the Kitty IME preview', 'tmux | OC | Implement the Kitty IME preview'])(
|
|
'classifies the native session title %j as title-derived idle OpenCode',
|
|
(title) => {
|
|
expect(getAgentLabel(title)).toBe('OpenCode')
|
|
expect(detectAgentStatusFromTitle(title)).toBe('idle')
|
|
}
|
|
)
|
|
|
|
// Why: a spinner glyph is the one status decoration OpenCode adds to the marker, and
|
|
// its gate runs before the marker's, so an animating frame still reads working (#8940).
|
|
it('reads a spinner-decorated native frame as working', () => {
|
|
const title = 'OC | ⠋ ask claude about this'
|
|
expect(getAgentLabel(title)).toBe('OpenCode')
|
|
expect(detectAgentStatusFromTitle(title)).toBe('working')
|
|
})
|
|
|
|
it.each(['OC | ✦ Gemini CLI', 'OC | ✋ review Gemini permission handling'])(
|
|
'does not let a Gemini glyph in OpenCode session text override identity for %j',
|
|
(title) => {
|
|
expect(getAgentLabel(title)).toBe('OpenCode')
|
|
expect(detectAgentStatusFromTitle(title)).toBe('idle')
|
|
}
|
|
)
|
|
|
|
// Why: the text after the marker is OpenCode's generated session summary — subject
|
|
// matter, not status. Routing it through the keyword gates would let an ordinary task
|
|
// name ("stop the flaky test") park a live pane in working/permission forever, so the
|
|
// marker asserts presence only. Do not "fix" these into keyword-derived statuses.
|
|
it.each([
|
|
'OC | ready to review',
|
|
'OC | permission prompt keeps reappearing',
|
|
'OC | waiting on the flaky test',
|
|
'OC | stop the suite from running'
|
|
])('keeps the status word inside the session summary %j inert', (title) => {
|
|
expect(getAgentLabel(title)).toBe('OpenCode')
|
|
expect(detectAgentStatusFromTitle(title)).toBe('idle')
|
|
})
|
|
|
|
it.each(['OC |', 'OC|Build', 'oc | lowercase lookalike', 'OCTOPUS | build'])(
|
|
'does not treat the lookalike title %j as an agent',
|
|
(title) => {
|
|
expect(detectAgentStatusFromTitle(title)).toBeNull()
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('Pi-compatible title detection', () => {
|
|
it.each([
|
|
['\u280b OMP', 'OMP', 'working'],
|
|
['OMP ready', 'OMP', 'idle'],
|
|
['OMP', 'OMP', 'idle'],
|
|
['OMP - action required', 'OMP', 'permission'],
|
|
['\u280b Pi', 'Pi', 'working'],
|
|
['Pi ready', 'Pi', 'idle'],
|
|
// Why: titles stored by the old collapse are still bare "Pi"; re-detection
|
|
// from stored lastOscTitle must still classify idle, not neutral.
|
|
['Pi', 'Pi', 'idle'],
|
|
['Pi - action required', 'Pi', 'permission']
|
|
] as const)('classifies synthesized %s', (title, expectedLabel, expectedStatus) => {
|
|
expect(getAgentLabel(title)).toBe(expectedLabel)
|
|
expect(detectAgentStatusFromTitle(title)).toBe(expectedStatus)
|
|
})
|
|
|
|
// Why: OMP 17.2.12+ writes static state markers on WSL/ConPTY (#13890). The label after
|
|
// the marker is cwd/session text, so glyphs another agent uses for status must not win.
|
|
it.each([
|
|
['π : my-project', 'working', 'π : my-project'],
|
|
['π > my-project', 'idle', 'π > my-project'],
|
|
['π ! my-project', 'permission', 'π ! my-project'],
|
|
['π > gemini ✦ ⏲ ◇ ✋', 'idle', 'π > gemini ✦ ⏲ ◇ ✋'],
|
|
['zsh | π : my-project', 'working', 'zsh | π : my-project'],
|
|
['zsh | π > gemini ✦ ⏲ ◇ ✋', 'idle', 'zsh | π > gemini ✦ ⏲ ◇ ✋']
|
|
] as const)('classifies native OMP title %j as %s', (title, expectedStatus, expectedDisplay) => {
|
|
expect(detectAgentStatusFromTitle(title)).toBe(expectedStatus)
|
|
expect(normalizeTerminalTitle(title)).toBe(expectedDisplay)
|
|
})
|
|
|
|
it('re-detects status after display-title normalization for Pi idle frames', () => {
|
|
// Normalization now preserves the session name and cwd (#16093).
|
|
expect(normalizeTerminalTitle('π - my-project')).toBe('π - my-project')
|
|
expect(detectAgentStatusFromTitle(normalizeTerminalTitle('π - my-project'))).toBe('idle')
|
|
expect(detectAgentStatusFromTitle(normalizeTerminalTitle('\u280b π - my-project'))).toBe(
|
|
'working'
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
['\u280b Pi', 'omp', '\u280b OMP'],
|
|
['Pi ready', 'omp', 'OMP ready'],
|
|
['Pi - action required', 'omp', 'OMP - action required'],
|
|
// Why: the brand is swapped for the owner's label in place — the pane still reads as its
|
|
// launch owner, but the session name and cwd it chose survive (#16093).
|
|
['π - tmp', 'omp', 'OMP - tmp'],
|
|
['π: tmp', 'omp', 'OMP: tmp'],
|
|
['\u280b π: tmp', 'omp', '\u280b OMP: tmp'],
|
|
['\u280b π - tmp', 'omp', '\u280b OMP - tmp'],
|
|
['\u280b OMP', 'pi', '\u280b Pi'],
|
|
[
|
|
'lucky-echidna | \u283c π - Diagnose Orca terminal title flicker - test',
|
|
'omp',
|
|
'lucky-echidna | \u283c OMP - Diagnose Orca terminal title flicker - test'
|
|
],
|
|
['lucky-echidna | Pi ready', 'omp', 'OMP ready'],
|
|
['Codex | Pi ready', 'omp', 'OMP ready'],
|
|
// Why: the wrapped whole reads as a braille Claude title, but the re-ownable
|
|
// synthetic pane suffix must still win.
|
|
['lucky-echidna | ⠋ OMP', 'omp', '⠋ OMP'],
|
|
[
|
|
'lucky-echidna | \u283c π - Diagnose | test',
|
|
'omp',
|
|
'lucky-echidna | \u283c OMP - Diagnose | test'
|
|
]
|
|
] as const)('normalizes %s to the authoritative %s owner', (title, owner, expectedTitle) => {
|
|
expect(normalizeCompatibleAgentTitleForOwner(title, owner)).toBe(expectedTitle)
|
|
})
|
|
|
|
it('preserves Pi-compatible custom titles and unrelated owners', () => {
|
|
expect(normalizeCompatibleAgentTitleForOwner('Fix pi bugs', 'omp')).toBe('Fix pi bugs')
|
|
// Why: a wrapped title with no re-ownable identity in any " | " segment
|
|
// passes through untouched instead of collapsing to the owner label.
|
|
expect(normalizeCompatibleAgentTitleForOwner('lucky-echidna | Fix pi bugs', 'omp')).toBe(
|
|
'lucky-echidna | Fix pi bugs'
|
|
)
|
|
expect(normalizeCompatibleAgentTitleForOwner('xxPi ready', 'omp')).toBe('xxPi ready')
|
|
expect(normalizeCompatibleAgentTitleForOwner('\u280b Pi', 'codex')).toBe('\u280b Pi')
|
|
})
|
|
|
|
it('identifies titles whose compatible identity can be re-owned', () => {
|
|
expect(hasCompatibleAgentTitleIdentity('Pi ready')).toBe(true)
|
|
expect(hasCompatibleAgentTitleIdentity('π - tmp')).toBe(true)
|
|
expect(hasCompatibleAgentTitleIdentity('\u280b OMP')).toBe(true)
|
|
expect(hasCompatibleAgentTitleIdentity('Fix pi bugs')).toBe(false)
|
|
expect(hasCompatibleAgentTitleIdentity('\u280b Codex')).toBe(false)
|
|
})
|
|
|
|
it('normalizes Pi-compatible status identity and terminal title to the owner', () => {
|
|
const status = normalizeCompatibleAgentStatusEntryForOwner(
|
|
{
|
|
state: 'working',
|
|
prompt: '',
|
|
updatedAt: 1,
|
|
stateStartedAt: 1,
|
|
agentType: 'pi',
|
|
paneKey: 'tab-1:leaf-1',
|
|
terminalTitle: '\u280b Pi',
|
|
stateHistory: []
|
|
},
|
|
'omp'
|
|
)
|
|
|
|
expect(status.agentType).toBe('omp')
|
|
expect(status.terminalTitle).toBe('\u280b OMP')
|
|
expect(resolveCompatibleAgentTypeForOwner('codex', 'omp')).toBe('codex')
|
|
})
|
|
|
|
it.each(['~/omp/working', 'omp-harness ready', '~/pi/working', 'pi-scratch ready'])(
|
|
'does not classify path or hyphen false positive %s',
|
|
(title) => {
|
|
expect(getAgentLabel(title)).toBeNull()
|
|
expect(detectAgentStatusFromTitle(title)).toBeNull()
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('Cursor agent title identity', () => {
|
|
// Why: the accepted vocabulary is the set of labels Orca actually synthesizes for Cursor.
|
|
// Pin it to that profile so renaming a label there cannot silently drop @cursor to zero
|
|
// recipients (and desync the auto-Enter suppression that shares this predicate).
|
|
it('accepts every label Orca synthesizes for Cursor', () => {
|
|
const profile = SYNTHETIC_AGENT_TITLE_PROFILES.cursor
|
|
|
|
for (const label of [
|
|
profile.workingLabel,
|
|
`⠋ ${profile.workingLabel}`,
|
|
profile.idleLabel,
|
|
profile.permissionLabel
|
|
]) {
|
|
expect(isCursorAgentTitle(label)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it.each([
|
|
'Cursor Agent',
|
|
' cursor agent ',
|
|
'⠋ Cursor Agent',
|
|
'⣿ Cursor Agent',
|
|
'Cursor ready',
|
|
'Cursor - action required'
|
|
])('accepts the native or Orca-synthesized Cursor title %j', (title) => {
|
|
expect(isCursorAgentTitle(title)).toBe(true)
|
|
})
|
|
|
|
// Why: "cursor" is ordinary editor vocabulary in another agent's task-summary title,
|
|
// so a whole-token name match is not Cursor identity.
|
|
it.each([
|
|
'⠋ fix the text cursor blink',
|
|
'✳ Fix the text cursor blink',
|
|
'. fix cursor position',
|
|
'* cursor rendering done',
|
|
'Terminal Cursor and Orca slows down',
|
|
'cursor-agent',
|
|
'cursor.exe',
|
|
'~/cursor-rules',
|
|
'',
|
|
null,
|
|
undefined
|
|
])('rejects the non-Cursor title %j', (title) => {
|
|
expect(isCursorAgentTitle(title)).toBe(false)
|
|
})
|
|
})
|