diff --git a/src/main/runtime/construction/runtime-service-state.ts b/src/main/runtime/construction/runtime-service-state.ts index 2a3e86c4269..a8aed5c690b 100644 --- a/src/main/runtime/construction/runtime-service-state.ts +++ b/src/main/runtime/construction/runtime-service-state.ts @@ -59,6 +59,8 @@ export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWith getTrackedPty: (ptyId) => this.ptysById.get(ptyId) ?? null, getTabTitle: (tabId) => this.tabs.get(tabId)?.title?.trim() || null, getForegroundProcess: (ptyId) => this.ptyController?.getForegroundProcess(ptyId) ?? null, + confirmForegroundProcess: (ptyId) => + this.ptyController?.confirmForegroundProcess?.(ptyId) ?? null, getTitleDisplayClear: (ptyId) => this.getPtyTitleDisplayClear(ptyId) }) diff --git a/src/main/runtime/orca-runtime-tests/terminal-agent-presence.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-agent-presence.spec.ts new file mode 100644 index 00000000000..71db915105e --- /dev/null +++ b/src/main/runtime/orca-runtime-tests/terminal-agent-presence.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest' +import { committingSpawn } from '../bind-first-pane.test-fixture' +import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec' +import { TEST_WORKTREE_PATH, store } from '../orca-runtime-test-fixtures.spec' + +describe('terminal agent presence', () => { + it('finds an idle Claude by a fresh read when p_comm is its version directory', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-claude' }) + const runtime = new OrcaRuntimeService(store) + runtime.setPtyController({ + spawn: committingSpawn(runtime, spawn), + write: () => true, + kill: () => true, + getForegroundProcess: async () => '2.1.310', + confirmForegroundProcess: async () => 'claude' + }) + const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + command: 'claude', + title: 'worker' + }) + // Claude answered over OSC 7501 paints no title glyph. + runtime.onPtyData('pty-claude', '\x1b]0;Orca worker dispatch task ok\x07', 1) + + await expect(runtime.isTerminalRunningAgent(handle)).resolves.toBe(true) + }) +}) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index f4df11653c3..26d44a5f918 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -69,6 +69,7 @@ await import('./orca-runtime-tests/terminal-output-and-worker-recovery-part-07.s await import('./orca-runtime-tests/terminal-handles-and-agent-status.spec') await import('./orca-runtime-tests/terminal-handles-and-agent-status-part-02.spec') await import('./orca-runtime-tests/terminal-settled-prompt-foreground-confirmation.spec') +await import('./orca-runtime-tests/terminal-agent-presence.spec') await import('./orca-runtime-tests/terminal-handles-and-agent-status-part-03.spec') await import('./orca-runtime-tests/terminal-handles-and-agent-status-part-04.spec') await import('./orca-runtime-tests/terminal-handles-and-agent-status-part-05.spec') diff --git a/src/main/runtime/runtime-terminal-agent-presence.ts b/src/main/runtime/runtime-terminal-agent-presence.ts index cedb62719ff..f47c70b4aa0 100644 --- a/src/main/runtime/runtime-terminal-agent-presence.ts +++ b/src/main/runtime/runtime-terminal-agent-presence.ts @@ -4,6 +4,7 @@ import { recognizeAgentProcess } from '../../shared/agent-process-recognition' import { isOpenCodeNativeTitle } from '../../shared/agent-detection' +import { isShellProcess } from '../../shared/shell-process-detection' import { isKnownReadyPromptPreview } from './terminal-wait-detection' import { buildTerminalWaitText } from './terminal-wait-tail-state' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' @@ -31,6 +32,7 @@ type RuntimeTerminalAgentPresenceDependencies = { getTrackedPty(ptyId: string): RuntimePtyWorktreeRecord | null getTabTitle(tabId: string): string | null getForegroundProcess(ptyId: string): Promise | null + confirmForegroundProcess(ptyId: string): Promise | null /** The stale-working timer's display-only clear of the PTY's own title, if one stands. */ getTitleDisplayClear(ptyId: string): TitleDisplayClear | null } @@ -197,14 +199,17 @@ export class RuntimeTerminalAgentPresence { suppressClaude: boolean, retryForegroundWrappers: boolean ): Promise { - const recognized = recognizeAgentProcess(foregroundProcess) + const wrapper = isAgentForegroundWrapperProcess(foregroundProcess) + // Why: macOS p_comm names the native Claude install by its version directory (`2.1.310`). + const recognized = + recognizeAgentProcess(foregroundProcess) ?? + (retryForegroundWrappers && !wrapper && !isShellProcess(foregroundProcess) + ? recognizeAgentProcess(await this.deps.confirmForegroundProcess(ptyId)) + : null) if (recognized) { return !(suppressClaude && isExpectedAgentProcess(recognized.processName, 'claude')) } - if (!isAgentForegroundWrapperProcess(foregroundProcess)) { - return false - } - if (!retryForegroundWrappers) { + if (!wrapper || !retryForegroundWrappers) { return false } const startedAt = Date.now() diff --git a/src/main/runtime/structured-worker-agent-presence.test.ts b/src/main/runtime/structured-worker-agent-presence.test.ts index 19da177b62d..d17fdb37d47 100644 --- a/src/main/runtime/structured-worker-agent-presence.test.ts +++ b/src/main/runtime/structured-worker-agent-presence.test.ts @@ -19,6 +19,7 @@ function presence(isLiveStructuredAgent: (handle: string) => boolean) { getTrackedPty: () => null, getTabTitle: () => null, getForegroundProcess: () => null, + confirmForegroundProcess: () => null, getTitleDisplayClear: () => null }) }