Files
orca/src/shared/agent-title-status.ts
T
ff5833dd4f Stop hidden terminal panes from exhausting renderer memory (hidden-view parking, byte mode, default off)
Extracts the self-contained "hidden-view parking" slice from PR #7214
(orca-performance) as a standalone fix for the Windows renderer OOM/hang
under multi-agent load (#7175 / STA-476): long-hidden terminal panes
unmount after a 30s hysteresis while a pane-less byte watcher keeps
bell/title/agent-completion side effects alive; reveal rides the existing
tab-move reattach path.

Byte mode only: no main-process side-effect emitter ships here, so
constants.ts pins terminalMainSideEffectAuthority / terminalHiddenDeliveryGate /
terminalModelQueryAuthority to false (the single intentional divergence from
#7214, reconciled by taking #7214's values when its emitter lands). The
parking kill switch terminalHiddenViewParking also ships default-OFF
(opt-in) until the memory win is confirmed in a soak.

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-04 14:26:39 -07:00

195 lines
5.4 KiB
TypeScript

import {
AGY_AGENT_NAME_RE,
BRAILLE_SPINNER_RE,
CLAUDE_IDLE,
CURSOR_NATIVE_TITLE_LOWER,
DROID_AGENT_NAME_RE,
GEMINI_IDLE,
GEMINI_PERMISSION,
GEMINI_SILENT_WORKING,
GEMINI_WORKING,
HERMES_AGENT_NAME_RE,
STRONG_IDLE_KEYWORDS_RE,
STRONG_WORKING_KEYWORDS_RE,
STRONG_WORKING_KEYWORDS_RE_GLOBAL,
containsAgentName,
containsAny,
containsBrailleSpinner,
containsLegacyAgentName,
isClaudeManagementTitle,
isGeminiTerminalTitle,
isPiAgentTitle,
isPiTerminalTitle
} from './agent-title-core'
import type { AgentStatus } from './agent-title-core'
import { getPiCompatibleSyntheticAgentStatus } from './pi-compatible-synthetic-title'
/**
* Strip working-status indicators so stale exit titles stop reporting working.
*/
export function clearWorkingIndicators(title: string): string {
let cleaned = title
cleaned = cleaned.replace(GEMINI_WORKING, '')
cleaned = cleaned.replace(GEMINI_SILENT_WORKING, '')
cleaned = cleaned.replace(BRAILLE_SPINNER_RE, '')
if (cleaned.startsWith('. ')) {
cleaned = cleaned.slice(2)
}
if (containsAgentName(cleaned)) {
cleaned = cleaned.replace(STRONG_WORKING_KEYWORDS_RE_GLOBAL, '')
}
cleaned = cleaned.replace(/\s{2,}/g, ' ').trim()
return cleaned || title
}
/**
* Tracks agent status transitions from terminal title changes.
*/
export function createAgentStatusTracker(
onBecameIdle: (title: string) => void,
onBecameWorking?: () => void,
onAgentExited?: () => void,
initialTitle?: string
): {
handleTitle: (title: string) => void
seedTitle: (title: string) => void
reset: () => void
} {
// Why: trackers restored mid-session need a last-known status without firing
// callbacks, or a hidden working agent can miss its later idle transition.
let lastStatus: AgentStatus | null =
initialTitle !== undefined ? detectAgentStatusFromTitle(initialTitle) : null
return {
handleTitle(title: string): void {
const newStatus = detectAgentStatusFromTitle(title)
if (lastStatus === 'working' && newStatus !== null && newStatus !== 'working') {
onBecameIdle(title)
}
if (lastStatus !== 'working' && newStatus === 'working') {
onBecameWorking?.()
}
// Why: reverting to a plain shell prompt after idle/permission means the
// agent exited; while working it can just be a transient internal title.
if (lastStatus !== null && lastStatus !== 'working' && newStatus === null) {
lastStatus = null
onAgentExited?.()
}
if (newStatus !== null) {
lastStatus = newStatus
}
},
seedTitle(title: string): void {
lastStatus = detectAgentStatusFromTitle(title)
},
reset(): void {
lastStatus = null
}
}
}
/**
* Normalize high-churn agent titles into stable display labels before storage.
*/
export function normalizeTerminalTitle(title: string): string {
if (!title) {
return title
}
if (isGeminiTerminalTitle(title)) {
const status = detectAgentStatusFromTitle(title)
if (status === 'permission') {
return `${GEMINI_PERMISSION} Gemini CLI`
}
if (status === 'working') {
return `${GEMINI_WORKING} Gemini CLI`
}
if (status === 'idle') {
return `${GEMINI_IDLE} Gemini CLI`
}
}
// Why: Pi animates every 80ms; collapse frames while preserving status.
if (isPiAgentTitle(title)) {
const status = detectAgentStatusFromTitle(title)
if (status === 'working') {
return '\u280b Pi'
}
if (status === 'idle') {
return 'Pi'
}
}
return title
}
export function detectAgentStatusFromTitle(title: string): AgentStatus | null {
if (!title || isClaudeManagementTitle(title)) {
return null
}
if (title.trim().toLowerCase() === CURSOR_NATIVE_TITLE_LOWER) {
return null
}
if (title.includes(GEMINI_PERMISSION)) {
return 'permission'
}
if (title.includes(GEMINI_WORKING) || title.includes(GEMINI_SILENT_WORKING)) {
return 'working'
}
if (title.includes(GEMINI_IDLE)) {
return 'idle'
}
// Why: resolve synthetic Pi/OMP permission/idle labels before the broader
// Pi and braille-spinner checks below.
const piCompatibleSyntheticAgentStatus = getPiCompatibleSyntheticAgentStatus(title)
if (piCompatibleSyntheticAgentStatus) {
return piCompatibleSyntheticAgentStatus
}
if (title.startsWith(`${CLAUDE_IDLE} `) || title === CLAUDE_IDLE) {
return 'idle'
}
if (isPiTerminalTitle(title)) {
return 'idle'
}
if (containsBrailleSpinner(title)) {
return 'working'
}
const hasDroidAgentName = DROID_AGENT_NAME_RE.test(title)
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) {
return null
}
if (containsAny(title, ['action required', 'permission', 'waiting'])) {
return 'permission'
}
// Why: boundary-aware regexes avoid cwd/path and substring false positives.
if (STRONG_IDLE_KEYWORDS_RE.test(title)) {
return 'idle'
}
if (STRONG_WORKING_KEYWORDS_RE.test(title)) {
return 'working'
}
if (title.startsWith('. ')) {
return 'working'
}
if (title.startsWith('* ')) {
return 'idle'
}
// Why: Droid hook events are authoritative; native name-only titles should
// not turn a still-sleeping execute tool into completion.
if (hasDroidAgentName && !hasLegacyAgentName) {
return null
}
return 'idle'
}