diff --git a/src/main/providers/agent-foreground-process.test.ts b/src/main/providers/agent-foreground-process.test.ts index aff948c61..c9f32dd04 100644 --- a/src/main/providers/agent-foreground-process.test.ts +++ b/src/main/providers/agent-foreground-process.test.ts @@ -289,6 +289,55 @@ describe('resolveAgentForegroundProcess', () => { await expect(resolveAgentForegroundProcess(100, 'powershell.exe')).resolves.toBe('codex') }) + it('recognizes the native Windows Cursor launcher process tree', async () => { + Object.defineProperty(process, 'platform', { value: 'win32' }) + mockPs( + windowsProcessJsonRows([ + { + CommandLine: 'powershell.exe', + Name: 'powershell.exe', + ParentProcessId: 99, + ProcessId: 100 + }, + { + CommandLine: 'cmd.exe /c cursor-agent.cmd', + Name: 'cmd.exe', + ParentProcessId: 100, + ProcessId: 101 + }, + { + CommandLine: + 'powershell.exe -File C:\\Users\\dev\\AppData\\Local\\cursor-agent\\cursor-agent.ps1', + Name: 'powershell.exe', + ParentProcessId: 101, + ProcessId: 102 + }, + { + CommandLine: + 'node.exe C:\\Users\\dev\\AppData\\Local\\cursor-agent\\versions\\2026.07.09-a3815c0\\index.js', + Name: 'node.exe', + ParentProcessId: 102, + ProcessId: 103 + }, + { + CommandLine: + 'node.exe C:\\Users\\dev\\AppData\\Local\\cursor-agent\\versions\\2026.07.09-a3815c0\\index.js worker-server', + Name: 'node.exe', + ParentProcessId: 103, + ProcessId: 104 + }, + { + CommandLine: 'C:\\Users\\dev\\.grok\\bin\\agent.exe', + Name: 'agent.exe', + ParentProcessId: 100, + ProcessId: 105 + } + ]) + ) + + await expect(resolveAgentForegroundProcess(100, 'powershell.exe')).resolves.toBe('cursor-agent') + }) + it('recognizes Windows Git Bash shell-rooted agent launches', async () => { Object.defineProperty(process, 'platform', { value: 'win32' }) execFileMock.mockImplementation( diff --git a/src/shared/agent-process-recognition.test.ts b/src/shared/agent-process-recognition.test.ts index c64e9c3e3..b6000f332 100644 --- a/src/shared/agent-process-recognition.test.ts +++ b/src/shared/agent-process-recognition.test.ts @@ -212,6 +212,24 @@ describe('agent process recognition', () => { expect(recognizeAgentProcessFromCommandLine('node /usr/local/bin/orca status')).toBeNull() }) + it('recognizes the versioned Cursor Node wrapper without accepting generic agent processes', () => { + const cursorEntrypoint = String.raw`C:\Users\dev\AppData\Local\cursor-agent\versions\2026.07.09-a3815c0\index.js` + + expect(recognizeAgentProcessFromCommandLine(`node.exe ${cursorEntrypoint}`)).toEqual({ + agent: 'cursor', + processName: 'cursor-agent' + }) + expect( + recognizeAgentProcessFromCommandLine(`node.exe ${cursorEntrypoint} worker-server`) + ).toEqual({ agent: 'cursor', processName: 'cursor-agent' }) + expect( + recognizeAgentProcessFromCommandLine(String.raw`node.exe C:\repo\cursor-agent\index.js`) + ).toBeNull() + expect( + recognizeAgentProcessFromCommandLine(String.raw`C:\Users\dev\.grok\bin\agent.exe`) + ).toBeNull() + }) + it('does not classify prompt text as a wrapped agent command', () => { expect( recognizeAgentProcessFromCommandLine( diff --git a/src/shared/agent-process-recognition.ts b/src/shared/agent-process-recognition.ts index cbeaca6f7..e68031863 100644 --- a/src/shared/agent-process-recognition.ts +++ b/src/shared/agent-process-recognition.ts @@ -52,6 +52,7 @@ const NODE_PACKAGE_SCRIPT_ENTRYPOINTS: Record = { codex: ['node_modules/@openai/codex/'], gemini: ['node_modules/@google/gemini-cli/'] } +const CURSOR_AGENT_NODE_ENTRYPOINT_RE = /(?:^|\/)cursor-agent\/versions\/[^/]+\/index\.js$/ const PYTHON_SCRIPT_ENTRYPOINT_DIRECTORIES = ['/bin/', '/scripts/', '/site-packages/'] const PROCESS_TO_AGENT = new Map() @@ -95,6 +96,11 @@ function agentForNormalizedProcess(normalized: string): TuiAgent | undefined { return undefined } +function recognizedAgentForProcess(normalized: string): RecognizedAgentProcess | null { + const agent = agentForNormalizedProcess(normalized) + return agent ? { agent, processName: normalized } : null +} + function tokenizeCommandLine(commandLine: string): string[] { const tokens: string[] = [] let current = '' @@ -197,20 +203,21 @@ function comparablePath(token: string): string { } function recognizeNodeScriptEntrypoint(token: string): RecognizedAgentProcess | null { + const path = comparablePath(token) + // Why: Cursor's native Windows launcher runs a generic versioned index.js, + // so its install path is the only stable identity that avoids ordinary Node apps. + if (CURSOR_AGENT_NODE_ENTRYPOINT_RE.test(path)) { + return { agent: 'cursor', processName: 'cursor-agent' } + } const normalized = normalizeProcessName(token, { stripInterpreterScriptExtension: true }) const markers = NODE_PACKAGE_SCRIPT_ENTRYPOINTS[normalized] if (!markers) { return null } - const path = comparablePath(token) if (!markers.some((marker) => path.includes(marker))) { return null } - const agent = agentForNormalizedProcess(normalized) - if (!agent) { - return null - } - return { agent, processName: normalized } + return recognizedAgentForProcess(normalized) } function recognizePythonModule( @@ -220,11 +227,7 @@ function recognizePythonModule( return null } const normalized = moduleName.split('.', 1)[0]?.toLowerCase() ?? '' - const agent = agentForNormalizedProcess(normalized) - if (!agent) { - return null - } - return { agent, processName: normalized } + return recognizedAgentForProcess(normalized) } function recognizePythonScriptEntrypoint(token: string): RecognizedAgentProcess | null { @@ -237,11 +240,7 @@ function recognizePythonScriptEntrypoint(token: string): RecognizedAgentProcess } const basename = path.split('/').pop() ?? '' const normalized = basename.replace(PYTHON_SCRIPT_EXTENSION_RE, '') - const agent = agentForNormalizedProcess(normalized) - if (!agent) { - return null - } - return { agent, processName: normalized } + return recognizedAgentForProcess(normalized) } function recognizePythonEntrypoint( @@ -274,11 +273,7 @@ export function recognizeAgentProcess( processName: string | null | undefined ): RecognizedAgentProcess | null { const normalized = normalizeProcessName(processName) - const agent = agentForNormalizedProcess(normalized) - if (!agent) { - return null - } - return { agent, processName: normalized } + return recognizedAgentForProcess(normalized) } export function recognizeAgentProcessFromCommandLine(