From 23871f40e321dab75dc3a019a55331c88ecae647 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 20 May 2026 02:20:58 -0400 Subject: [PATCH] Fix Windows agent launch readiness matching (#2404) --- src/renderer/src/lib/agent-ready-wait.test.ts | 43 +++++++++++++++++++ src/renderer/src/lib/agent-ready-wait.ts | 7 +-- src/renderer/src/lib/new-workspace.ts | 7 +-- src/shared/agent-process-recognition.test.ts | 14 +++++- src/shared/agent-process-recognition.ts | 15 +++++++ 5 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 src/renderer/src/lib/agent-ready-wait.test.ts diff --git a/src/renderer/src/lib/agent-ready-wait.test.ts b/src/renderer/src/lib/agent-ready-wait.test.ts new file mode 100644 index 000000000..06d2d7028 --- /dev/null +++ b/src/renderer/src/lib/agent-ready-wait.test.ts @@ -0,0 +1,43 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { waitForAgentReady } from './agent-ready-wait' +import { useAppStore } from '@/store' +import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspection' + +vi.mock('@/store', () => ({ + useAppStore: { + getState: vi.fn() + } +})) + +vi.mock('@/runtime/runtime-terminal-inspection', () => ({ + inspectRuntimeTerminalProcess: vi.fn() +})) + +vi.mock('@/lib/tui-agent-startup', () => ({ + isShellProcess: vi.fn(() => false) +})) + +describe('waitForAgentReady', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('window', { setTimeout }) + vi.mocked(useAppStore.getState).mockReturnValue({ + ptyIdsByTabId: { 'tab-1': ['pty-1'] }, + runtimePaneTitlesByTabId: {}, + tabsByWorktree: {}, + settings: {} + } as never) + }) + + it('recognizes a Windows foreground process reported as a full executable path', async () => { + vi.mocked(inspectRuntimeTerminalProcess).mockResolvedValue({ + foregroundProcess: String.raw`C:\Users\dev\AppData\Roaming\npm\claude.exe`, + hasChildProcesses: false + }) + + await expect(waitForAgentReady('tab-1', 'claude', { timeoutMs: 1 })).resolves.toEqual({ + ready: true, + reason: 'foreground-match' + }) + }) +}) diff --git a/src/renderer/src/lib/agent-ready-wait.ts b/src/renderer/src/lib/agent-ready-wait.ts index 84b75c721..a5b7909ef 100644 --- a/src/renderer/src/lib/agent-ready-wait.ts +++ b/src/renderer/src/lib/agent-ready-wait.ts @@ -1,4 +1,5 @@ import { detectAgentStatusFromTitle } from '../../../shared/agent-detection' +import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition' import { isShellProcess } from '@/lib/tui-agent-startup' import { useAppStore } from '@/store' import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspection' @@ -92,11 +93,7 @@ export async function waitForAgentReady( try { const process = await inspectRuntimeTerminalProcess(useAppStore.getState().settings, ptyId) const foreground = process.foregroundProcess?.toLowerCase() ?? '' - if ( - foreground === expectedProcess || - foreground.startsWith(`${expectedProcess}.`) || - foreground.endsWith(`/${expectedProcess}`) - ) { + if (isExpectedAgentProcess(foreground, expectedProcess)) { return { ready: true, reason: 'foreground-match' } } diff --git a/src/renderer/src/lib/new-workspace.ts b/src/renderer/src/lib/new-workspace.ts index 06a534ab9..e592e353e 100644 --- a/src/renderer/src/lib/new-workspace.ts +++ b/src/renderer/src/lib/new-workspace.ts @@ -8,6 +8,7 @@ import type { AgentStartupPlan } from '@/lib/tui-agent-startup' import { isShellProcess } from '@/lib/tui-agent-startup' import type { OrcaHooks, TaskViewPresetId } from '../../../shared/types' import { normalizeHookCommandSourcePolicy } from '../../../shared/hook-command-source-policy' +import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition' /** * Why: the TaskPage's preset buttons and the openTaskPage prefetcher both need @@ -294,11 +295,7 @@ async function waitForAgentForeground(ptyId: string, expectedProcess: string): P try { const process = await inspectRuntimeTerminalProcess(useAppStore.getState().settings, ptyId) const foreground = process.foregroundProcess?.toLowerCase() ?? '' - const owns = - foreground === expectedProcess || - foreground.startsWith(`${expectedProcess}.`) || - foreground.endsWith(`/${expectedProcess}`) - if (owns) { + if (isExpectedAgentProcess(foreground, expectedProcess)) { return } if (attempt >= 4 && !isShellProcess(foreground)) { diff --git a/src/shared/agent-process-recognition.test.ts b/src/shared/agent-process-recognition.test.ts index 5b9fb5dda..2c78ae44b 100644 --- a/src/shared/agent-process-recognition.test.ts +++ b/src/shared/agent-process-recognition.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { isRecognizedAgentType, recognizeAgentProcess } from './agent-process-recognition' +import { + isExpectedAgentProcess, + isRecognizedAgentType, + recognizeAgentProcess +} from './agent-process-recognition' describe('agent process recognition', () => { it('recognizes packaged Codex foreground process names', () => { @@ -9,4 +13,12 @@ describe('agent process recognition', () => { }) expect(isRecognizedAgentType('codex-aarch64-ap')).toBe(true) }) + + it('matches expected agents from platform-specific foreground process paths', () => { + expect( + isExpectedAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\claude.exe`, 'claude') + ).toBe(true) + expect(isExpectedAgentProcess('/usr/local/bin/claude', 'claude')).toBe(true) + expect(isExpectedAgentProcess('powershell.exe', 'claude')).toBe(false) + }) }) diff --git a/src/shared/agent-process-recognition.ts b/src/shared/agent-process-recognition.ts index a423b327c..8fe1f17ea 100644 --- a/src/shared/agent-process-recognition.ts +++ b/src/shared/agent-process-recognition.ts @@ -55,6 +55,21 @@ function agentForNormalizedProcess(normalized: string): TuiAgent | undefined { return undefined } +export function isExpectedAgentProcess( + processName: string | null | undefined, + expectedProcess: string +): boolean { + const normalizedProcess = normalizeProcessName(processName) + const normalizedExpected = normalizeProcessName(expectedProcess) + if (!normalizedProcess || !normalizedExpected) { + return false + } + return ( + normalizedProcess === normalizedExpected || + normalizedProcess.startsWith(`${normalizedExpected}.`) + ) +} + export function recognizeAgentProcess( processName: string | null | undefined ): RecognizedAgentProcess | null {