From 1386fba27886ce7bd1161b61c80f39157c9bf152 Mon Sep 17 00:00:00 2001 From: xuewenjie Date: Fri, 19 Dec 2025 15:01:26 +0800 Subject: [PATCH] Revert other files to main, keep only process-utils changes --- packages/core/src/ide/detect-ide.test.ts | 43 +++--- 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 +++- 4 files changed, 143 insertions(+), 108 deletions(-) diff --git a/packages/core/src/ide/detect-ide.test.ts b/packages/core/src/ide/detect-ide.test.ts index 81ccdce5..9bf1eb12 100644 --- a/packages/core/src/ide/detect-ide.test.ts +++ b/packages/core/src/ide/detect-ide.test.ts @@ -8,6 +8,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { detectIde, IDE_DEFINITIONS } from './detect-ide.js'; describe('detectIde', () => { + const ideProcessInfo = { pid: 123, command: 'some/path/to/code' }; + const ideProcessInfoNoCode = { pid: 123, command: 'some/path/to/fork' }; + // Clear all IDE-related environment variables before each test beforeEach(() => { vi.stubEnv('__COG_BASHRC_SOURCED', ''); @@ -26,65 +29,73 @@ describe('detectIde', () => { it('should return undefined if TERM_PROGRAM is not vscode', () => { vi.stubEnv('TERM_PROGRAM', ''); - expect(detectIde()).toBeUndefined(); + expect(detectIde(ideProcessInfo)).toBeUndefined(); }); it('should detect Devin', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('__COG_BASHRC_SOURCED', '1'); - expect(detectIde()).toBe(IDE_DEFINITIONS.devin); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.devin); }); it('should detect Replit', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('REPLIT_USER', 'testuser'); - expect(detectIde()).toBe(IDE_DEFINITIONS.replit); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.replit); }); it('should detect Cursor', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CURSOR_TRACE_ID', 'some-id'); - expect(detectIde()).toBe(IDE_DEFINITIONS.cursor); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cursor); }); it('should detect Codespaces', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CODESPACES', 'true'); - expect(detectIde()).toBe(IDE_DEFINITIONS.codespaces); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.codespaces); }); it('should detect Cloud Shell via EDITOR_IN_CLOUD_SHELL', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('EDITOR_IN_CLOUD_SHELL', 'true'); - expect(detectIde()).toBe(IDE_DEFINITIONS.cloudshell); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell); }); it('should detect Cloud Shell via CLOUD_SHELL', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CLOUD_SHELL', 'true'); - expect(detectIde()).toBe(IDE_DEFINITIONS.cloudshell); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell); }); it('should detect Trae', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('TERM_PRODUCT', 'Trae'); - expect(detectIde()).toBe(IDE_DEFINITIONS.trae); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.trae); }); it('should detect Firebase Studio via MONOSPACE_ENV', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', 'true'); - expect(detectIde()).toBe(IDE_DEFINITIONS.firebasestudio); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.firebasestudio); }); - it('should detect VSCodeFork when no other IDE is detected and no process info provided', () => { + it('should detect VSCode when no other IDE is detected and command includes "code"', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); - expect(detectIde()).toBe(IDE_DEFINITIONS.vscodefork); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.vscode); + }); + + it('should detect VSCodeFork when no other IDE is detected and command does not include "code"', () => { + vi.stubEnv('TERM_PROGRAM', 'vscode'); + vi.stubEnv('MONOSPACE_ENV', ''); + expect(detectIde(ideProcessInfoNoCode)).toBe(IDE_DEFINITIONS.vscodefork); }); }); describe('detectIde with ideInfoFromFile', () => { + const ideProcessInfo = { pid: 123, command: 'some/path/to/code' }; + beforeEach(() => { vi.stubEnv('__COG_BASHRC_SOURCED', ''); vi.stubEnv('REPLIT_USER', ''); @@ -105,22 +116,22 @@ describe('detectIde with ideInfoFromFile', () => { name: 'custom-ide', displayName: 'Custom IDE', }; - expect(detectIde(undefined, ideInfoFromFile)).toEqual(ideInfoFromFile); + expect(detectIde(ideProcessInfo, ideInfoFromFile)).toEqual(ideInfoFromFile); }); it('should fall back to env detection if name is missing', () => { const ideInfoFromFile = { displayName: 'Custom IDE' }; vi.stubEnv('TERM_PROGRAM', 'vscode'); - expect(detectIde(undefined, ideInfoFromFile)).toBe( - IDE_DEFINITIONS.vscodefork, + expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe( + IDE_DEFINITIONS.vscode, ); }); it('should fall back to env detection if displayName is missing', () => { const ideInfoFromFile = { name: 'custom-ide' }; vi.stubEnv('TERM_PROGRAM', 'vscode'); - expect(detectIde(undefined, ideInfoFromFile)).toBe( - IDE_DEFINITIONS.vscodefork, + expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe( + IDE_DEFINITIONS.vscode, ); }); }); diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index f7ef2979..c00d9a62 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 f41b229a..ca26f78f 100644 --- a/packages/core/src/ide/ide-client.test.ts +++ b/packages/core/src/ide/ide-client.test.ts @@ -22,6 +22,7 @@ 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(); @@ -96,20 +97,21 @@ describe('IdeClient', () => { describe('connect', () => { it('should connect using HTTP when port is provided in config file', async () => { - const config = { port: '8080', workspacePath: '/test/workspace' }; + const config = { port: '8080' }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); 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), @@ -121,19 +123,13 @@ describe('IdeClient', () => { }); it('should connect using stdio when stdio config is provided in file', async () => { - const config = { - stdio: { command: 'test-cmd', args: ['--foo'] }, - workspacePath: '/test/workspace', - }; + const config = { stdio: { command: 'test-cmd', args: ['--foo'] } }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-12345.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); const ideClient = await IdeClient.getInstance(); await ideClient.connect(); @@ -152,17 +148,13 @@ 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(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); const ideClient = await IdeClient.getInstance(); await ideClient.connect(); @@ -225,16 +217,13 @@ describe('IdeClient', () => { }); it('should prioritize file config over environment variables', async () => { - const config = { port: '8080', workspacePath: '/test/workspace' }; + const config = { port: '8080' }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090'; const ideClient = await IdeClient.getInstance(); @@ -276,15 +265,7 @@ 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. @@ -295,6 +276,10 @@ 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 () => { @@ -317,11 +302,14 @@ 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-5678.json']); + ).mockResolvedValue(['qwen-code-ide-server-12345-123.json']); vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ isValid: true, @@ -335,6 +323,10 @@ 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 () => { @@ -346,13 +338,16 @@ 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-1111.json', - 'qwen-code-ide-server-5678.json', + 'qwen-code-ide-server-12345-111.json', + 'qwen-code-ide-server-12345-222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(invalidConfig)) @@ -384,13 +379,16 @@ 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-1111.json', - 'qwen-code-ide-server-2222.json', + 'qwen-code-ide-server-12345-111.json', + 'qwen-code-ide-server-12345-222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(config1)) @@ -413,13 +411,16 @@ 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-1111.json', - 'qwen-code-ide-server-2222.json', + 'qwen-code-ide-server-12345-111.json', + 'qwen-code-ide-server-12345-222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(config1)) @@ -441,13 +442,16 @@ 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-1111.json', - 'qwen-code-ide-server-2222.json', + 'qwen-code-ide-server-12345-111.json', + 'qwen-code-ide-server-12345-222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce('invalid json') @@ -467,11 +471,12 @@ describe('IdeClient', () => { }); it('should return undefined if readdir throws an error', async () => { - ( - vi.mocked(fs.promises.readdir) as Mock< - (path: fs.PathLike) => Promise - > - ).mockRejectedValue(new Error('readdir failed')); + vi.mocked(fs.promises.readFile).mockRejectedValueOnce( + new Error('not found'), + ); + vi.mocked(fs.promises.readdir).mockRejectedValue( + new Error('readdir failed'), + ); const ideClient = await IdeClient.getInstance(); const result = await ( @@ -485,12 +490,15 @@ 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-3333.json', // valid + 'qwen-code-ide-server-12345-111.json', // valid 'not-a-config-file.txt', // invalid 'qwen-code-ide-server-asdf.json', // invalid ]); @@ -509,19 +517,30 @@ 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-1111.json', - 'qwen-code-ide-server-3333.json', + 'qwen-code-ide-server-12345-111.json', + 'qwen-code-ide-server-12345-222.json', ]); vi.mocked(fs.promises.readFile) .mockResolvedValueOnce(JSON.stringify(config1)) @@ -549,16 +568,13 @@ describe('IdeClient', () => { }); it('should return false if tool discovery fails', async () => { - const config = { port: '8080', workspacePath: '/test/workspace' }; + const config = { port: '8080' }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); mockClient.request.mockRejectedValue(new Error('Method not found')); const ideClient = await IdeClient.getInstance(); @@ -571,16 +587,13 @@ describe('IdeClient', () => { }); it('should return false if diffing tools are not available', async () => { - const config = { port: '8080', workspacePath: '/test/workspace' }; + const config = { port: '8080' }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); mockClient.request.mockResolvedValue({ tools: [{ name: 'someOtherTool' }], }); @@ -595,16 +608,13 @@ describe('IdeClient', () => { }); it('should return false if only openDiff tool is available', async () => { - const config = { port: '8080', workspacePath: '/test/workspace' }; + const config = { port: '8080' }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); mockClient.request.mockResolvedValue({ tools: [{ name: 'openDiff' }], }); @@ -619,16 +629,13 @@ describe('IdeClient', () => { }); it('should return true if connected and diffing tools are available', async () => { - const config = { port: '8080', workspacePath: '/test/workspace' }; + const config = { port: '8080' }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); mockClient.request.mockResolvedValue({ tools: [{ name: 'openDiff' }, { name: 'closeDiff' }], }); @@ -646,20 +653,13 @@ 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, - workspacePath: '/test/workspace', - }; + const config = { port: '8080', authToken }; + vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); ( vi.mocked(fs.promises.readdir) as Mock< (path: fs.PathLike) => Promise > - ).mockResolvedValue(['qwen-code-ide-server-8080.json']); - vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config)); - vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({ - isValid: true, - }); + ).mockResolvedValue([]); 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 b401c9f7..b447f46c 100644 --- a/packages/core/src/ide/ide-client.ts +++ b/packages/core/src/ide/ide-client.ts @@ -14,6 +14,7 @@ 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'; @@ -85,6 +86,7 @@ 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; @@ -106,9 +108,10 @@ 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( - undefined, + client.ideProcessInfo, client.connectionConfig?.ideInfo, ); return client; @@ -569,7 +572,26 @@ export class IdeClient { | (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }) | undefined > { - const portFileDir = os.tmpdir(); + 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'); let portFiles; try { portFiles = await fs.promises.readdir(portFileDir); @@ -582,7 +604,9 @@ export class IdeClient { return undefined; } - const fileRegex = /^qwen-code-ide-server-\d+\.json$/; + const fileRegex = new RegExp( + `^qwen-code-ide-server-${this.ideProcessInfo.pid}-\\d+\\.json$`, + ); const matchingFiles = portFiles .filter((file) => fileRegex.test(file)) .sort();