mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
[ide-mode] Create an IDE manager class to handle connecting to and exposing methods from the IDE server (#4797)
This commit is contained in:
@@ -1011,100 +1011,4 @@ describe('loadCliConfig ideMode', () => {
|
|||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getIdeMode()).toBe(false);
|
expect(config.getIdeMode()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add _ide_server when ideMode is true', async () => {
|
|
||||||
process.argv = ['node', 'script.js', '--ide-mode'];
|
|
||||||
const argv = await parseArguments();
|
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
|
||||||
process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000';
|
|
||||||
const settings: Settings = {};
|
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
|
||||||
expect(config.getIdeMode()).toBe(true);
|
|
||||||
const mcpServers = config.getMcpServers();
|
|
||||||
expect(mcpServers['_ide_server']).toBeDefined();
|
|
||||||
expect(mcpServers['_ide_server'].httpUrl).toBe('http://localhost:3000/mcp');
|
|
||||||
expect(mcpServers['_ide_server'].description).toBe('IDE connection');
|
|
||||||
expect(mcpServers['_ide_server'].trust).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should warn if ideMode is true and no port is set', async () => {
|
|
||||||
const consoleWarnSpy = vi
|
|
||||||
.spyOn(console, 'warn')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
process.argv = ['node', 'script.js', '--ide-mode'];
|
|
||||||
const argv = await parseArguments();
|
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
|
||||||
const settings: Settings = {};
|
|
||||||
await loadCliConfig(settings, [], 'test-session', argv);
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
||||||
'[WARN]',
|
|
||||||
'Could not connect to IDE. Make sure you have the companion VS Code extension installed from the marketplace or via /ide install.',
|
|
||||||
);
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should warn and overwrite if settings contain the reserved _ide_server name and ideMode is active', async () => {
|
|
||||||
const consoleWarnSpy = vi
|
|
||||||
.spyOn(console, 'warn')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
process.argv = ['node', 'script.js', '--ide-mode'];
|
|
||||||
const argv = await parseArguments();
|
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
|
||||||
process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000';
|
|
||||||
const settings: Settings = {
|
|
||||||
mcpServers: {
|
|
||||||
_ide_server: new ServerConfig.MCPServerConfig(
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'http://malicious:1234',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
|
||||||
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
||||||
'[WARN]',
|
|
||||||
'Ignoring user-defined MCP server config for "_ide_server" as it is a reserved name.',
|
|
||||||
);
|
|
||||||
|
|
||||||
const mcpServers = config.getMcpServers();
|
|
||||||
expect(mcpServers['_ide_server']).toBeDefined();
|
|
||||||
expect(mcpServers['_ide_server'].httpUrl).toBe('http://localhost:3000/mcp');
|
|
||||||
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should NOT warn if settings contain the reserved _ide_server name and ideMode is NOT active', async () => {
|
|
||||||
const consoleWarnSpy = vi
|
|
||||||
.spyOn(console, 'warn')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
process.argv = ['node', 'script.js'];
|
|
||||||
const argv = await parseArguments();
|
|
||||||
const settings: Settings = {
|
|
||||||
mcpServers: {
|
|
||||||
_ide_server: new ServerConfig.MCPServerConfig(
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'http://malicious:1234',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
|
||||||
|
|
||||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
const mcpServers = config.getMcpServers();
|
|
||||||
expect(mcpServers['_ide_server']).toBeDefined();
|
|
||||||
expect(mcpServers['_ide_server'].url).toBe('http://malicious:1234');
|
|
||||||
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import {
|
|||||||
FileDiscoveryService,
|
FileDiscoveryService,
|
||||||
TelemetryTarget,
|
TelemetryTarget,
|
||||||
FileFilteringOptions,
|
FileFilteringOptions,
|
||||||
MCPServerConfig,
|
IdeClient,
|
||||||
IDE_SERVER_NAME,
|
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { Settings } from './settings.js';
|
import { Settings } from './settings.js';
|
||||||
|
|
||||||
@@ -264,6 +263,11 @@ export async function loadCliConfig(
|
|||||||
process.env.TERM_PROGRAM === 'vscode' &&
|
process.env.TERM_PROGRAM === 'vscode' &&
|
||||||
!process.env.SANDBOX;
|
!process.env.SANDBOX;
|
||||||
|
|
||||||
|
let ideClient: IdeClient | undefined;
|
||||||
|
if (ideMode) {
|
||||||
|
ideClient = new IdeClient();
|
||||||
|
}
|
||||||
|
|
||||||
const allExtensions = annotateActiveExtensions(
|
const allExtensions = annotateActiveExtensions(
|
||||||
extensions,
|
extensions,
|
||||||
argv.extensions || [],
|
argv.extensions || [],
|
||||||
@@ -355,37 +359,6 @@ export async function loadCliConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ideMode) {
|
|
||||||
if (mcpServers[IDE_SERVER_NAME]) {
|
|
||||||
logger.warn(
|
|
||||||
`Ignoring user-defined MCP server config for "${IDE_SERVER_NAME}" as it is a reserved name.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const companionPort = process.env.GEMINI_CLI_IDE_SERVER_PORT;
|
|
||||||
if (companionPort) {
|
|
||||||
const httpUrl = `http://localhost:${companionPort}/mcp`;
|
|
||||||
mcpServers[IDE_SERVER_NAME] = new MCPServerConfig(
|
|
||||||
undefined, // command
|
|
||||||
undefined, // args
|
|
||||||
undefined, // env
|
|
||||||
undefined, // cwd
|
|
||||||
undefined, // url
|
|
||||||
httpUrl, // httpUrl
|
|
||||||
undefined, // headers
|
|
||||||
undefined, // tcp
|
|
||||||
undefined, // timeout
|
|
||||||
false, // trust
|
|
||||||
'IDE connection', // description
|
|
||||||
undefined, // includeTools
|
|
||||||
undefined, // excludeTools
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
'Could not connect to IDE. Make sure you have the companion VS Code extension installed from the marketplace or via /ide install.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
||||||
|
|
||||||
return new Config({
|
return new Config({
|
||||||
@@ -450,6 +423,7 @@ export async function loadCliConfig(
|
|||||||
noBrowser: !!process.env.NO_BROWSER,
|
noBrowser: !!process.env.NO_BROWSER,
|
||||||
summarizeToolOutput: settings.summarizeToolOutput,
|
summarizeToolOutput: settings.summarizeToolOutput,
|
||||||
ideMode,
|
ideMode,
|
||||||
|
ideClient,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,25 +19,10 @@ import { type Config } from '@google/gemini-cli-core';
|
|||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import { glob } from 'glob';
|
import { glob } from 'glob';
|
||||||
|
|
||||||
import {
|
import { IDEConnectionStatus } from '@google/gemini-cli-core/index.js';
|
||||||
getMCPDiscoveryState,
|
|
||||||
getMCPServerStatus,
|
|
||||||
IDE_SERVER_NAME,
|
|
||||||
MCPDiscoveryState,
|
|
||||||
MCPServerStatus,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
|
|
||||||
vi.mock('child_process');
|
vi.mock('child_process');
|
||||||
vi.mock('glob');
|
vi.mock('glob');
|
||||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
||||||
const original =
|
|
||||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
|
||||||
return {
|
|
||||||
...original,
|
|
||||||
getMCPServerStatus: vi.fn(),
|
|
||||||
getMCPDiscoveryState: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function regexEscape(value: string) {
|
function regexEscape(value: string) {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
@@ -49,8 +34,6 @@ describe('ideCommand', () => {
|
|||||||
let execSyncSpy: MockInstance;
|
let execSyncSpy: MockInstance;
|
||||||
let globSyncSpy: MockInstance;
|
let globSyncSpy: MockInstance;
|
||||||
let platformSpy: MockInstance;
|
let platformSpy: MockInstance;
|
||||||
let getMCPServerStatusSpy: MockInstance;
|
|
||||||
let getMCPDiscoveryStateSpy: MockInstance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = {
|
mockContext = {
|
||||||
@@ -61,13 +44,12 @@ describe('ideCommand', () => {
|
|||||||
|
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
getIdeMode: vi.fn(),
|
getIdeMode: vi.fn(),
|
||||||
|
getIdeClient: vi.fn(),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
execSyncSpy = vi.spyOn(child_process, 'execSync');
|
execSyncSpy = vi.spyOn(child_process, 'execSync');
|
||||||
globSyncSpy = vi.spyOn(glob, 'sync');
|
globSyncSpy = vi.spyOn(glob, 'sync');
|
||||||
platformSpy = vi.spyOn(process, 'platform', 'get');
|
platformSpy = vi.spyOn(process, 'platform', 'get');
|
||||||
getMCPServerStatusSpy = vi.mocked(getMCPServerStatus);
|
|
||||||
getMCPDiscoveryStateSpy = vi.mocked(getMCPDiscoveryState);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -91,15 +73,21 @@ describe('ideCommand', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('status subcommand', () => {
|
describe('status subcommand', () => {
|
||||||
|
const mockGetConnectionStatus = vi.fn();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
||||||
|
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
|
||||||
|
getConnectionStatus: mockGetConnectionStatus,
|
||||||
|
} as ReturnType<Config['getIdeClient']>);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show connected status', () => {
|
it('should show connected status', () => {
|
||||||
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.CONNECTED);
|
mockGetConnectionStatus.mockReturnValue({
|
||||||
|
status: IDEConnectionStatus.Connected,
|
||||||
|
});
|
||||||
const command = ideCommand(mockConfig);
|
const command = ideCommand(mockConfig);
|
||||||
const result = command!.subCommands![0].action!(mockContext, '');
|
const result = command!.subCommands![0].action!(mockContext, '');
|
||||||
expect(getMCPServerStatusSpy).toHaveBeenCalledWith(IDE_SERVER_NAME);
|
expect(mockGetConnectionStatus).toHaveBeenCalled();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'info',
|
messageType: 'info',
|
||||||
@@ -108,37 +96,45 @@ describe('ideCommand', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show connecting status', () => {
|
it('should show connecting status', () => {
|
||||||
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.CONNECTING);
|
mockGetConnectionStatus.mockReturnValue({
|
||||||
|
status: IDEConnectionStatus.Connecting,
|
||||||
|
});
|
||||||
const command = ideCommand(mockConfig);
|
const command = ideCommand(mockConfig);
|
||||||
const result = command!.subCommands![0].action!(mockContext, '');
|
const result = command!.subCommands![0].action!(mockContext, '');
|
||||||
|
expect(mockGetConnectionStatus).toHaveBeenCalled();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'info',
|
messageType: 'info',
|
||||||
content: '🔄 Initializing...',
|
content: `🟡 Connecting...`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show discovery in progress status', () => {
|
|
||||||
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.DISCONNECTED);
|
|
||||||
getMCPDiscoveryStateSpy.mockReturnValue(MCPDiscoveryState.IN_PROGRESS);
|
|
||||||
const command = ideCommand(mockConfig);
|
|
||||||
const result = command!.subCommands![0].action!(mockContext, '');
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'message',
|
|
||||||
messageType: 'info',
|
|
||||||
content: '🔄 Initializing...',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show disconnected status', () => {
|
it('should show disconnected status', () => {
|
||||||
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.DISCONNECTED);
|
mockGetConnectionStatus.mockReturnValue({
|
||||||
getMCPDiscoveryStateSpy.mockReturnValue(MCPDiscoveryState.COMPLETED);
|
status: IDEConnectionStatus.Disconnected,
|
||||||
|
});
|
||||||
const command = ideCommand(mockConfig);
|
const command = ideCommand(mockConfig);
|
||||||
const result = command!.subCommands![0].action!(mockContext, '');
|
const result = command!.subCommands![0].action!(mockContext, '');
|
||||||
|
expect(mockGetConnectionStatus).toHaveBeenCalled();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'error',
|
messageType: 'error',
|
||||||
content: '🔴 Disconnected',
|
content: `🔴 Disconnected`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show disconnected status with details', () => {
|
||||||
|
const details = 'Something went wrong';
|
||||||
|
mockGetConnectionStatus.mockReturnValue({
|
||||||
|
status: IDEConnectionStatus.Disconnected,
|
||||||
|
details,
|
||||||
|
});
|
||||||
|
const command = ideCommand(mockConfig);
|
||||||
|
const result = command!.subCommands![0].action!(mockContext, '');
|
||||||
|
expect(mockGetConnectionStatus).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `🔴 Disconnected: ${details}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import {
|
import { Config, IDEConnectionStatus } from '@google/gemini-cli-core';
|
||||||
Config,
|
|
||||||
getMCPDiscoveryState,
|
|
||||||
getMCPServerStatus,
|
|
||||||
IDE_SERVER_NAME,
|
|
||||||
MCPDiscoveryState,
|
|
||||||
MCPServerStatus,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import {
|
import {
|
||||||
CommandContext,
|
CommandContext,
|
||||||
SlashCommand,
|
SlashCommand,
|
||||||
@@ -56,35 +49,30 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
|||||||
description: 'check status of IDE integration',
|
description: 'check status of IDE integration',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
action: (_context: CommandContext): SlashCommandActionReturn => {
|
action: (_context: CommandContext): SlashCommandActionReturn => {
|
||||||
const status = getMCPServerStatus(IDE_SERVER_NAME);
|
const connection = config.getIdeClient()?.getConnectionStatus();
|
||||||
const discoveryState = getMCPDiscoveryState();
|
switch (connection?.status) {
|
||||||
switch (status) {
|
case IDEConnectionStatus.Connected:
|
||||||
case MCPServerStatus.CONNECTED:
|
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'info',
|
messageType: 'info',
|
||||||
content: `🟢 Connected`,
|
content: `🟢 Connected`,
|
||||||
};
|
} as const;
|
||||||
case MCPServerStatus.CONNECTING:
|
case IDEConnectionStatus.Connecting:
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'info',
|
messageType: 'info',
|
||||||
content: `🔄 Initializing...`,
|
content: `🟡 Connecting...`,
|
||||||
};
|
} as const;
|
||||||
case MCPServerStatus.DISCONNECTED:
|
default: {
|
||||||
default:
|
let content = `🔴 Disconnected`;
|
||||||
if (discoveryState === MCPDiscoveryState.IN_PROGRESS) {
|
if (connection?.details) {
|
||||||
return {
|
content += `: ${connection.details}`;
|
||||||
type: 'message',
|
}
|
||||||
messageType: 'info',
|
|
||||||
content: `🔄 Initializing...`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
messageType: 'error',
|
messageType: 'error',
|
||||||
content: `🔴 Disconnected`,
|
content,
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"@types/glob": "^8.1.0",
|
"@types/glob": "^8.1.0",
|
||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "^9.0.4",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
|
"chardet": "^2.1.0",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"dotenv": "^17.1.0",
|
"dotenv": "^17.1.0",
|
||||||
"glob": "^10.4.5",
|
"glob": "^10.4.5",
|
||||||
@@ -44,8 +45,7 @@
|
|||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"undici": "^7.10.0",
|
"undici": "^7.10.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0"
|
||||||
"chardet": "^2.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||||
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
|
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
|
||||||
import { MCPOAuthConfig } from '../mcp/oauth-provider.js';
|
import { MCPOAuthConfig } from '../mcp/oauth-provider.js';
|
||||||
|
import { IdeClient } from '../ide/ide-client.js';
|
||||||
|
|
||||||
// Re-export OAuth config type
|
// Re-export OAuth config type
|
||||||
export type { MCPOAuthConfig };
|
export type { MCPOAuthConfig };
|
||||||
@@ -180,6 +181,7 @@ export interface ConfigParameters {
|
|||||||
noBrowser?: boolean;
|
noBrowser?: boolean;
|
||||||
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
|
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
|
||||||
ideMode?: boolean;
|
ideMode?: boolean;
|
||||||
|
ideClient?: IdeClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
@@ -221,6 +223,7 @@ export class Config {
|
|||||||
private readonly extensionContextFilePaths: string[];
|
private readonly extensionContextFilePaths: string[];
|
||||||
private readonly noBrowser: boolean;
|
private readonly noBrowser: boolean;
|
||||||
private readonly ideMode: boolean;
|
private readonly ideMode: boolean;
|
||||||
|
private readonly ideClient: IdeClient | undefined;
|
||||||
private modelSwitchedDuringSession: boolean = false;
|
private modelSwitchedDuringSession: boolean = false;
|
||||||
private readonly maxSessionTurns: number;
|
private readonly maxSessionTurns: number;
|
||||||
private readonly listExtensions: boolean;
|
private readonly listExtensions: boolean;
|
||||||
@@ -286,6 +289,7 @@ export class Config {
|
|||||||
this.noBrowser = params.noBrowser ?? false;
|
this.noBrowser = params.noBrowser ?? false;
|
||||||
this.summarizeToolOutput = params.summarizeToolOutput;
|
this.summarizeToolOutput = params.summarizeToolOutput;
|
||||||
this.ideMode = params.ideMode ?? false;
|
this.ideMode = params.ideMode ?? false;
|
||||||
|
this.ideClient = params.ideClient;
|
||||||
|
|
||||||
if (params.contextFileName) {
|
if (params.contextFileName) {
|
||||||
setGeminiMdFilename(params.contextFileName);
|
setGeminiMdFilename(params.contextFileName);
|
||||||
@@ -574,6 +578,10 @@ export class Config {
|
|||||||
return this.ideMode;
|
return this.ideMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIdeClient(): IdeClient | undefined {
|
||||||
|
return this.ideClient;
|
||||||
|
}
|
||||||
|
|
||||||
async getGitService(): Promise<GitService> {
|
async getGitService(): Promise<GitService> {
|
||||||
if (!this.gitService) {
|
if (!this.gitService) {
|
||||||
this.gitService = new GitService(this.targetDir);
|
this.gitService = new GitService(this.targetDir);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
|||||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
import { setSimulate429 } from '../utils/testUtils.js';
|
import { setSimulate429 } from '../utils/testUtils.js';
|
||||||
import { tokenLimit } from './tokenLimits.js';
|
import { tokenLimit } from './tokenLimits.js';
|
||||||
import { ideContext } from '../services/ideContext.js';
|
import { ideContext } from '../ide/ideContext.js';
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
const mockChatCreateFn = vi.fn();
|
const mockChatCreateFn = vi.fn();
|
||||||
@@ -72,7 +72,7 @@ vi.mock('../telemetry/index.js', () => ({
|
|||||||
logApiResponse: vi.fn(),
|
logApiResponse: vi.fn(),
|
||||||
logApiError: vi.fn(),
|
logApiError: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock('../services/ideContext.js');
|
vi.mock('../ide/ideContext.js');
|
||||||
|
|
||||||
describe('findIndexAfterFraction', () => {
|
describe('findIndexAfterFraction', () => {
|
||||||
const history: Content[] = [
|
const history: Content[] = [
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||||
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
||||||
import { LoopDetectionService } from '../services/loopDetectionService.js';
|
import { LoopDetectionService } from '../services/loopDetectionService.js';
|
||||||
import { ideContext } from '../services/ideContext.js';
|
import { ideContext } from '../ide/ideContext.js';
|
||||||
import { logFlashDecidedToContinue } from '../telemetry/loggers.js';
|
import { logFlashDecidedToContinue } from '../telemetry/loggers.js';
|
||||||
import { FlashDecidedToContinueEvent } from '../telemetry/types.js';
|
import { FlashDecidedToContinueEvent } from '../telemetry/types.js';
|
||||||
|
|
||||||
|
|||||||
100
packages/core/src/ide/ide-client.ts
Normal file
100
packages/core/src/ide/ide-client.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ideContext, OpenFilesNotificationSchema } from '../ide/ideContext.js';
|
||||||
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
|
|
||||||
|
const logger = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
debug: (...args: any[]) =>
|
||||||
|
console.debug('[DEBUG] [ImportProcessor]', ...args),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IDEConnectionState = {
|
||||||
|
status: IDEConnectionStatus;
|
||||||
|
details?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum IDEConnectionStatus {
|
||||||
|
Connected = 'connected',
|
||||||
|
Disconnected = 'disconnected',
|
||||||
|
Connecting = 'connecting',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the connection to and interaction with the IDE server.
|
||||||
|
*/
|
||||||
|
export class IdeClient {
|
||||||
|
client: Client | undefined = undefined;
|
||||||
|
connectionStatus: IDEConnectionStatus = IDEConnectionStatus.Disconnected;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.connectToMcpServer().catch((err) => {
|
||||||
|
logger.debug('Failed to initialize IdeClient:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getConnectionStatus(): {
|
||||||
|
status: IDEConnectionStatus;
|
||||||
|
details?: string;
|
||||||
|
} {
|
||||||
|
let details: string | undefined;
|
||||||
|
if (this.connectionStatus === IDEConnectionStatus.Disconnected) {
|
||||||
|
if (!process.env['GEMINI_CLI_IDE_SERVER_PORT']) {
|
||||||
|
details = 'GEMINI_CLI_IDE_SERVER_PORT environment variable is not set.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: this.connectionStatus,
|
||||||
|
details,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectToMcpServer(): Promise<void> {
|
||||||
|
this.connectionStatus = IDEConnectionStatus.Connecting;
|
||||||
|
const idePort = process.env['GEMINI_CLI_IDE_SERVER_PORT'];
|
||||||
|
if (!idePort) {
|
||||||
|
logger.debug(
|
||||||
|
'Unable to connect to IDE mode MCP server. GEMINI_CLI_IDE_SERVER_PORT environment variable is not set.',
|
||||||
|
);
|
||||||
|
this.connectionStatus = IDEConnectionStatus.Disconnected;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.client = new Client({
|
||||||
|
name: 'streamable-http-client',
|
||||||
|
// TODO(#3487): use the CLI version here.
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
const transport = new StreamableHTTPClientTransport(
|
||||||
|
new URL(`http://localhost:${idePort}/mcp`),
|
||||||
|
);
|
||||||
|
await this.client.connect(transport);
|
||||||
|
this.client.setNotificationHandler(
|
||||||
|
OpenFilesNotificationSchema,
|
||||||
|
(notification) => {
|
||||||
|
ideContext.setOpenFilesContext(notification.params);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.client.onerror = (error) => {
|
||||||
|
logger.debug('IDE MCP client error:', error);
|
||||||
|
this.connectionStatus = IDEConnectionStatus.Disconnected;
|
||||||
|
ideContext.clearOpenFilesContext();
|
||||||
|
};
|
||||||
|
this.client.onclose = () => {
|
||||||
|
logger.debug('IDE MCP client connection closed.');
|
||||||
|
this.connectionStatus = IDEConnectionStatus.Disconnected;
|
||||||
|
ideContext.clearOpenFilesContext();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.connectionStatus = IDEConnectionStatus.Connected;
|
||||||
|
} catch (error) {
|
||||||
|
this.connectionStatus = IDEConnectionStatus.Disconnected;
|
||||||
|
logger.debug('Failed to connect to MCP server:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,6 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
|
||||||
* The reserved server name for the IDE's MCP server.
|
|
||||||
*/
|
|
||||||
export const IDE_SERVER_NAME = '_ide_server';
|
|
||||||
/**
|
/**
|
||||||
* Zod schema for validating a cursor position.
|
* Zod schema for validating a cursor position.
|
||||||
*/
|
*/
|
||||||
@@ -40,7 +40,10 @@ export * from './utils/systemEncoding.js';
|
|||||||
// Export services
|
// Export services
|
||||||
export * from './services/fileDiscoveryService.js';
|
export * from './services/fileDiscoveryService.js';
|
||||||
export * from './services/gitService.js';
|
export * from './services/gitService.js';
|
||||||
export * from './services/ideContext.js';
|
|
||||||
|
// Export IDE specific logic
|
||||||
|
export * from './ide/ide-client.js';
|
||||||
|
export * from './ide/ideContext.js';
|
||||||
|
|
||||||
// Export base tool definitions
|
// Export base tool definitions
|
||||||
export * from './tools/tools.js';
|
export * from './tools/tools.js';
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ import { ToolRegistry } from './tool-registry.js';
|
|||||||
import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
|
import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
|
||||||
import { OAuthUtils } from '../mcp/oauth-utils.js';
|
import { OAuthUtils } from '../mcp/oauth-utils.js';
|
||||||
import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
|
import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
|
||||||
import {
|
|
||||||
OpenFilesNotificationSchema,
|
|
||||||
IDE_SERVER_NAME,
|
|
||||||
ideContext,
|
|
||||||
} from '../services/ideContext.js';
|
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
|
|
||||||
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
|
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
|
||||||
@@ -379,24 +374,11 @@ export async function connectAndDiscover(
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTED);
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTED);
|
||||||
|
|
||||||
mcpClient.onerror = (error) => {
|
mcpClient.onerror = (error) => {
|
||||||
console.error(`MCP ERROR (${mcpServerName}):`, error.toString());
|
console.error(`MCP ERROR (${mcpServerName}):`, error.toString());
|
||||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
||||||
if (mcpServerName === IDE_SERVER_NAME) {
|
|
||||||
ideContext.clearOpenFilesContext();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mcpServerName === IDE_SERVER_NAME) {
|
|
||||||
mcpClient.setNotificationHandler(
|
|
||||||
OpenFilesNotificationSchema,
|
|
||||||
(notification) => {
|
|
||||||
ideContext.setOpenFilesContext(notification.params);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tools = await discoverTools(
|
const tools = await discoverTools(
|
||||||
mcpServerName,
|
mcpServerName,
|
||||||
mcpServerConfig,
|
mcpServerConfig,
|
||||||
|
|||||||
@@ -244,34 +244,5 @@ const createMcpServer = () => {
|
|||||||
},
|
},
|
||||||
{ capabilities: { logging: {} } },
|
{ capabilities: { logging: {} } },
|
||||||
);
|
);
|
||||||
server.registerTool(
|
|
||||||
'getOpenFiles',
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
'(IDE Tool) Get the path of the file currently active in VS Code.',
|
|
||||||
inputSchema: {},
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
const editor = vscode.window.activeTextEditor;
|
|
||||||
const filePath =
|
|
||||||
editor && editor.document.uri.scheme === 'file'
|
|
||||||
? editor.document.uri.fsPath
|
|
||||||
: '';
|
|
||||||
if (filePath) {
|
|
||||||
return {
|
|
||||||
content: [{ type: 'text', text: `Active file: ${filePath}` }],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: 'No file is currently active in the editor.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user