From ec32a2450878c3a6f3a8fad79fe86c1d95ca40ba Mon Sep 17 00:00:00 2001 From: xuewenjie Date: Fri, 19 Dec 2025 11:36:05 +0800 Subject: [PATCH] fix: update ide-client tests to match new config file naming scheme - Update config file naming from qwen-code-ide-server-{pid}-{timestamp}.json to qwen-code-ide-server-{port}.json - Add readdir mock to return config file list - Add validateWorkspacePath mock for workspace validation - Add workspacePath field to all config objects in tests - Remove getIdeProcessInfo dependency from tests - All 23 tests now passing --- packages/core/src/ide/detect-ide.ts | 6 +- packages/core/src/ide/ide-client.test.ts | 172 +++++++++++----------- packages/core/src/ide/ide-client.ts | 30 +--- packages/core/src/ide/process-utils.ts | 174 +---------------------- 4 files changed, 99 insertions(+), 283 deletions(-) diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index c00d9a62..f7ef2979 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -52,7 +52,7 @@ export function detectIdeFromEnv(): IdeInfo { function verifyVSCode( ide: IdeInfo, - ideProcessInfo: { + ideProcessInfo?: { pid: number; command: string; }, @@ -61,7 +61,7 @@ function verifyVSCode( return ide; } if ( - ideProcessInfo.command && + ideProcessInfo?.command && ideProcessInfo.command.toLowerCase().includes('code') ) { return IDE_DEFINITIONS.vscode; @@ -70,7 +70,7 @@ function verifyVSCode( } export function detectIde( - ideProcessInfo: { + ideProcessInfo?: { pid: number; command: string; }, diff --git a/packages/core/src/ide/ide-client.test.ts b/packages/core/src/ide/ide-client.test.ts index ca26f78f..f41b229a 100644 --- a/packages/core/src/ide/ide-client.test.ts +++ b/packages/core/src/ide/ide-client.test.ts @@ -22,7 +22,6 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { detectIde, IDE_DEFINITIONS } from './detect-ide.js'; import * as os from 'node:os'; -import * as path from 'node:path'; vi.mock('node:fs', async (importOriginal) => { const actual = await importOriginal(); @@ -97,21 +96,20 @@ describe('IdeClient', () => { describe('connect', () => { it('should connect using HTTP when port is provided in config file', async () => { - const config = { port: '8080' }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { port: '8080', workspacePath: '/test/workspace' }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); const ideClient = await IdeClient.getInstance(); await ideClient.connect(); - expect(fs.promises.readFile).toHaveBeenCalledWith( - path.join('/tmp', 'qwen-code-ide-server-12345.json'), - 'utf8', - ); expect(StreamableHTTPClientTransport).toHaveBeenCalledWith( new URL('http://127.0.0.1:8080/mcp'), expect.any(Object), @@ -123,13 +121,19 @@ describe('IdeClient', () => { }); it('should connect using stdio when stdio config is provided in file', async () => { - const config = { stdio: { command: 'test-cmd', args: ['--foo'] } }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { + stdio: { command: 'test-cmd', args: ['--foo'] }, + workspacePath: '/test/workspace', + }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-12345.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); const ideClient = await IdeClient.getInstance(); await ideClient.connect(); @@ -148,13 +152,17 @@ describe('IdeClient', () => { const config = { port: '8080', stdio: { command: 'test-cmd', args: ['--foo'] }, + workspacePath: '/test/workspace', }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); const ideClient = await IdeClient.getInstance(); await ideClient.connect(); @@ -217,13 +225,16 @@ describe('IdeClient', () => { }); it('should prioritize file config over environment variables', async () => { - const config = { port: '8080' }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { port: '8080', workspacePath: '/test/workspace' }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090'; const ideClient = await IdeClient.getInstance(); @@ -265,7 +276,15 @@ describe('IdeClient', () => { describe('getConnectionConfigFromFile', () => { it('should return config from the specific pid file if it exists', async () => { const config = { port: '1234', workspacePath: '/test/workspace' }; + ( + vi.mocked(fs.promises.readdir) as Mock< + (path: fs.PathLike) => Promise + > + ).mockResolvedValue(['qwen-code-ide-server-1234.json']); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); const ideClient = await IdeClient.getInstance(); // In tests, the private method can be accessed like this. @@ -276,10 +295,6 @@ describe('IdeClient', () => { ).getConnectionConfigFromFile(); expect(result).toEqual(config); - expect(fs.promises.readFile).toHaveBeenCalledWith( - path.join('/tmp', 'qwen-code-ide-server-12345.json'), - 'utf8', - ); }); it('should return undefined if no config files are found', async () => { @@ -302,14 +317,11 @@ describe('IdeClient', () => { it('should find and parse a single config file with the new naming scheme', async () => { const config = { port: '5678', workspacePath: '/test/workspace' }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); // For old path ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-12345-123.json']); + ).mockResolvedValue(['qwen-code-ide-server-5678.json']); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ isValid: true, @@ -323,10 +335,6 @@ describe('IdeClient', () => { ).getConnectionConfigFromFile(); expect(result).toEqual(config); - expect(fs.promises.readFile).toHaveBeenCalledWith( - path.join('/tmp/gemini/ide', 'qwen-code-ide-server-12345-123.json'), - 'utf8', - ); }); it('should filter out configs with invalid workspace paths', async () => { @@ -338,16 +346,13 @@ describe('IdeClient', () => { port: '1111', workspacePath: '/invalid/workspace', }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > ).mockResolvedValue([ - 'qwen-code-ide-server-12345-111.json', - 'qwen-code-ide-server-12345-222.json', + 'qwen-code-ide-server-1111.json', + 'qwen-code-ide-server-5678.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(invalidConfig)) @@ -379,16 +384,13 @@ describe('IdeClient', () => { it('should return the first valid config when multiple workspaces are valid', async () => { const config1 = { port: '1111', workspacePath: '/test/workspace' }; const config2 = { port: '2222', workspacePath: '/test/workspace2' }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > ).mockResolvedValue([ - 'qwen-code-ide-server-12345-111.json', - 'qwen-code-ide-server-12345-222.json', + 'qwen-code-ide-server-1111.json', + 'qwen-code-ide-server-2222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(config1)) @@ -411,16 +413,13 @@ describe('IdeClient', () => { process.env['QWEN_CODE_IDE_SERVER_PORT'] = '2222'; const config1 = { port: '1111', workspacePath: '/test/workspace' }; const config2 = { port: '2222', workspacePath: '/test/workspace2' }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > ).mockResolvedValue([ - 'qwen-code-ide-server-12345-111.json', - 'qwen-code-ide-server-12345-222.json', + 'qwen-code-ide-server-1111.json', + 'qwen-code-ide-server-2222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(config1)) @@ -442,16 +441,13 @@ describe('IdeClient', () => { it('should handle invalid JSON in one of the config files', async () => { const validConfig = { port: '2222', workspacePath: '/test/workspace' }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > ).mockResolvedValue([ - 'qwen-code-ide-server-12345-111.json', - 'qwen-code-ide-server-12345-222.json', + 'qwen-code-ide-server-1111.json', + 'qwen-code-ide-server-2222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce('invalid json') @@ -471,12 +467,11 @@ describe('IdeClient', () => { }); it('should return undefined if readdir throws an error', async () => { - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); - vi.mocked(fs.promises.readdir).mockRejectedValue( - new Error('readdir failed'), - ); + ( + vi.mocked(fs.promises.readdir) as Mock< + (path: fs.PathLike) => Promise + > + ).mockRejectedValue(new Error('readdir failed')); const ideClient = await IdeClient.getInstance(); const result = await ( @@ -490,15 +485,12 @@ describe('IdeClient', () => { it('should ignore files with invalid names', async () => { const validConfig = { port: '3333', workspacePath: '/test/workspace' }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > ).mockResolvedValue([ - 'qwen-code-ide-server-12345-111.json', // valid + 'qwen-code-ide-server-3333.json', // valid 'not-a-config-file.txt', // invalid 'qwen-code-ide-server-asdf.json', // invalid ]); @@ -517,30 +509,19 @@ describe('IdeClient', () => { ).getConnectionConfigFromFile(); expect(result).toEqual(validConfig); - expect(fs.promises.readFile).toHaveBeenCalledWith( - path.join('/tmp/gemini/ide', 'qwen-code-ide-server-12345-111.json'), - 'utf8', - ); - expect(fs.promises.readFile).not.toHaveBeenCalledWith( - path.join('/tmp/gemini/ide', 'not-a-config-file.txt'), - 'utf8', - ); }); it('should match env port string to a number port in the config', async () => { process.env['QWEN_CODE_IDE_SERVER_PORT'] = '3333'; const config1 = { port: 1111, workspacePath: '/test/workspace' }; const config2 = { port: 3333, workspacePath: '/test/workspace2' }; - vi.mocked(fs.promises.readFile).mockRejectedValueOnce( - new Error('not found'), - ); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > ).mockResolvedValue([ - 'qwen-code-ide-server-12345-111.json', - 'qwen-code-ide-server-12345-222.json', + 'qwen-code-ide-server-1111.json', + 'qwen-code-ide-server-3333.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(config1)) @@ -568,13 +549,16 @@ describe('IdeClient', () => { }); it('should return false if tool discovery fails', async () => { - const config = { port: '8080' }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { port: '8080', workspacePath: '/test/workspace' }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); mockClient.request.mockRejectedValue(new Error('Method not found')); const ideClient = await IdeClient.getInstance(); @@ -587,13 +571,16 @@ describe('IdeClient', () => { }); it('should return false if diffing tools are not available', async () => { - const config = { port: '8080' }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { port: '8080', workspacePath: '/test/workspace' }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); mockClient.request.mockResolvedValue({ tools: [{ name: 'someOtherTool' }], }); @@ -608,13 +595,16 @@ describe('IdeClient', () => { }); it('should return false if only openDiff tool is available', async () => { - const config = { port: '8080' }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { port: '8080', workspacePath: '/test/workspace' }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); mockClient.request.mockResolvedValue({ tools: [{ name: 'openDiff' }], }); @@ -629,13 +619,16 @@ describe('IdeClient', () => { }); it('should return true if connected and diffing tools are available', async () => { - const config = { port: '8080' }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { port: '8080', workspacePath: '/test/workspace' }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); mockClient.request.mockResolvedValue({ tools: [{ name: 'openDiff' }, { name: 'closeDiff' }], }); @@ -653,13 +646,20 @@ describe('IdeClient', () => { describe('authentication', () => { it('should connect with an auth token if provided in the discovery file', async () => { const authToken = 'test-auth-token'; - const config = { port: '8080', authToken }; - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + const config = { + port: '8080', + authToken, + workspacePath: '/test/workspace', + }; ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue([]); + ).mockResolvedValue(['qwen-code-ide-server-8080.json']); + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); + vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ + isValid: true, + }); const ideClient = await IdeClient.getInstance(); await ideClient.connect(); diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts index b447f46c..b401c9f7 100644 --- a/packages/core/src/ide/ide-client.ts +++ b/packages/core/src/ide/ide-client.ts @@ -14,7 +14,6 @@ import { IdeDiffClosedNotificationSchema, IdeDiffRejectedNotificationSchema, } from './types.js'; -import { getIdeProcessInfo } from './process-utils.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; @@ -86,7 +85,6 @@ export class IdeClient { 'IDE integration is currently disabled. To enable it, run /ide enable.', }; private currentIde: IdeInfo | undefined; - private ideProcessInfo: { pid: number; command: string } | undefined; private connectionConfig: | (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }) | undefined; @@ -108,10 +106,9 @@ export class IdeClient { if (!IdeClient.instancePromise) { IdeClient.instancePromise = (async () => { const client = new IdeClient(); - client.ideProcessInfo = await getIdeProcessInfo(); client.connectionConfig = await client.getConnectionConfigFromFile(); client.currentIde = detectIde( - client.ideProcessInfo, + undefined, client.connectionConfig?.ideInfo, ); return client; @@ -572,26 +569,7 @@ export class IdeClient { | (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }) | undefined > { - if (!this.ideProcessInfo) { - return undefined; - } - - // For backwards compatability - try { - const portFile = path.join( - os.tmpdir(), - `qwen-code-ide-server-${this.ideProcessInfo.pid}.json`, - ); - const portFileContents = await fs.promises.readFile(portFile, 'utf8'); - return JSON.parse(portFileContents); - } catch (_) { - // For newer extension versions, the file name matches the pattern - // /^qwen-code-ide-server-${pid}-\d+\.json$/. If multiple IDE - // windows are open, multiple files matching the pattern are expected to - // exist. - } - - const portFileDir = path.join(os.tmpdir(), 'gemini', 'ide'); + const portFileDir = os.tmpdir(); let portFiles; try { portFiles = await fs.promises.readdir(portFileDir); @@ -604,9 +582,7 @@ export class IdeClient { return undefined; } - const fileRegex = new RegExp( - `^qwen-code-ide-server-${this.ideProcessInfo.pid}-\\d+\\.json$`, - ); + const fileRegex = /^qwen-code-ide-server-\d+\.json$/; const matchingFiles = portFiles .filter((file) => fileRegex.test(file)) .sort(); diff --git a/packages/core/src/ide/process-utils.ts b/packages/core/src/ide/process-utils.ts index d4673f1b..617c5650 100644 --- a/packages/core/src/ide/process-utils.ts +++ b/packages/core/src/ide/process-utils.ts @@ -14,80 +14,18 @@ const execFileAsync = promisify(execFile); const MAX_TRAVERSAL_DEPTH = 32; -/** - * Escapes a string for safe use inside PowerShell double-quoted strings. - * Must escape backslashes first, then double quotes. - * - * @param str The string to escape. - * @returns The escaped string safe for PowerShell double-quoted context. - */ -function escapeForPowerShellDoubleQuotes(str: string): string { - // Order matters: escape backslashes first, then double quotes - return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); -} - -/** - * Fetches the parent process ID, name, and command for a given process ID. - * - * @param pid The process ID to inspect. - * @returns A promise that resolves to the parent's PID, name, and command. - */ async function getProcessInfo(pid: number): Promise<{ parentPid: number; name: string; command: string; }> { - try { - const platform = os.platform(); - if (platform === 'win32') { - const powershellCommand = [ - '$p = Get-CimInstance Win32_Process', - `-Filter 'ProcessId=${pid}'`, - '-ErrorAction SilentlyContinue;', - 'if ($p) {', - '@{Name=$p.Name;ParentProcessId=$p.ParentProcessId;CommandLine=$p.CommandLine}', - '| ConvertTo-Json', - '}', - ].join(' '); - - const { stdout } = await execAsync( - `powershell -NoProfile -NonInteractive -Command "${escapeForPowerShellDoubleQuotes(powershellCommand)}"`, - ); - const output = stdout.trim(); - if (!output) return { parentPid: 0, name: '', command: '' }; - const { - Name = '', - ParentProcessId = 0, - CommandLine = '', - } = JSON.parse(output); - return { - parentPid: ParentProcessId, - name: Name, - command: CommandLine ?? '', - }; - } else { - const command = `ps -o ppid=,command= -p ${pid}`; - const { stdout } = await execAsync(command); - const trimmedStdout = stdout.trim(); - if (!trimmedStdout) { - return { parentPid: 0, name: '', command: '' }; - } - const ppidString = trimmedStdout.split(/\s+/)[0]; - const parentPid = parseInt(ppidString, 10); - const fullCommand = trimmedStdout.substring(ppidString.length).trim(); - const processName = path.basename(fullCommand.split(' ')[0]); - return { - parentPid: isNaN(parentPid) ? 1 : parentPid, - name: processName, - command: fullCommand, - }; - } - } catch (_e) { - console.debug(`Failed to get process info for pid ${pid}:`, _e); - return { parentPid: 0, name: '', command: '' }; - } + // Only used for Unix systems (macOS and Linux) + const { stdout } = await execAsync(`ps -p ${pid} -o ppid=,comm=`); + const [ppidStr, ...commandParts] = stdout.trim().split(/\s+/); + const parentPid = parseInt(ppidStr, 10); + const command = commandParts.join(' '); + return { parentPid, name: path.basename(command), command }; } - /** * Finds the IDE process info on Unix-like systems. * @@ -199,14 +137,6 @@ async function getProcessTableWindows(): Promise> { return processMap; } -/** - * Finds the IDE process info on Windows using a snapshot approach. - * - * The strategy is to find the IDE process by looking for known IDE executables - * in the process chain, with fallback to heuristics. - * - * @returns A promise that resolves to the PID and command of the IDE process. - */ async function getIdeProcessInfoForWindows(): Promise<{ pid: number; command: string; @@ -222,19 +152,6 @@ async function getIdeProcessInfoForWindows(): Promise<{ return { pid: myPid, command: '' }; } - // Known IDE process names (lowercase for case-insensitive comparison) - const ideProcessNames = [ - 'code.exe', // VS Code - 'code - insiders.exe', // VS Code Insiders - 'cursor.exe', // Cursor - 'windsurf.exe', // Windsurf - 'devenv.exe', // Visual Studio - 'rider64.exe', // JetBrains Rider - 'idea64.exe', // IntelliJ IDEA - 'pycharm64.exe', // PyCharm - 'webstorm64.exe', // WebStorm - ]; - // Perform tree traversal in memory const ancestors: ProcessInfo[] = []; let curr: ProcessInfo | undefined = myProc; @@ -249,84 +166,7 @@ async function getIdeProcessInfoForWindows(): Promise<{ curr = processMap.get(curr.parentPid); } - // Strategy 1: Look for known IDE process names in the chain - for (let i = ancestors.length - 1; i >= 0; i--) { - const proc = ancestors[i]; - const nameLower = proc.name.toLowerCase(); - - if ( - ideProcessNames.some((ideName) => nameLower === ideName.toLowerCase()) - ) { - return { pid: proc.pid, command: proc.command }; - } - } - - // Strategy 2: Special handling for Git Bash (sh.exe/bash.exe) with missing parent - // Check this first before general shell handling - const gitBashNames = ['sh.exe', 'bash.exe']; - const gitBashProc = ancestors.find((p) => - gitBashNames.some((name) => p.name.toLowerCase() === name.toLowerCase()), - ); - - if (gitBashProc) { - // Check if parent exists in process table - const parentExists = - gitBashProc.parentPid !== 0 && processMap.has(gitBashProc.parentPid); - - if (!parentExists && gitBashProc.parentPid !== 0) { - // Look for IDE processes in the entire process table - const ideProcesses: ProcessInfo[] = []; - for (const [, proc] of processMap) { - const nameLower = proc.name.toLowerCase(); - if ( - ideProcessNames.some((ideName) => nameLower === ideName.toLowerCase()) - ) { - ideProcesses.push(proc); - } - } - - if (ideProcesses.length > 0) { - // Prefer main process (without --type= parameter) over utility processes - const mainProcesses = ideProcesses.filter( - (p) => !p.command.includes('--type='), - ); - const targetProcesses = - mainProcesses.length > 0 ? mainProcesses : ideProcesses; - - // Sort by PID and pick the one with lowest PID - targetProcesses.sort((a, b) => a.pid - b.pid); - return { - pid: targetProcesses[0].pid, - command: targetProcesses[0].command, - }; - } - } else if (parentExists) { - // Git Bash parent exists, use it - const gitBashIndex = ancestors.indexOf(gitBashProc); - if (gitBashIndex >= 0 && gitBashIndex + 1 < ancestors.length) { - const parentProc = ancestors[gitBashIndex + 1]; - return { pid: parentProc.pid, command: parentProc.command }; - } - } - } - - // Strategy 3: Look for other shell processes (cmd.exe, powershell.exe, etc.) and use their parent - const otherShellNames = ['cmd.exe', 'powershell.exe', 'pwsh.exe']; - for (let i = 0; i < ancestors.length; i++) { - const proc = ancestors[i]; - const nameLower = proc.name.toLowerCase(); - - if (otherShellNames.some((shell) => nameLower === shell.toLowerCase())) { - // The parent of the shell is likely closer to the IDE - if (i + 1 < ancestors.length) { - const parentProc = ancestors[i + 1]; - return { pid: parentProc.pid, command: parentProc.command }; - } - break; - } - } - - // Strategy 4: Use ancestors[length-3] as fallback (original logic) + // Use heuristic: return the great-grandparent (ancestors[length-3]) if (ancestors.length >= 3) { const target = ancestors[ancestors.length - 3]; return { pid: target.pid, command: target.command };