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:
@@ -31,6 +31,7 @@
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"ajv": "^8.17.1",
|
||||
"chardet": "^2.1.0",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^17.1.0",
|
||||
"glob": "^10.4.5",
|
||||
@@ -44,8 +45,7 @@
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"undici": "^7.10.0",
|
||||
"ws": "^8.18.0",
|
||||
"chardet": "^2.1.0"
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^7.0.2",
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
|
||||
import { MCPOAuthConfig } from '../mcp/oauth-provider.js';
|
||||
import { IdeClient } from '../ide/ide-client.js';
|
||||
|
||||
// Re-export OAuth config type
|
||||
export type { MCPOAuthConfig };
|
||||
@@ -180,6 +181,7 @@ export interface ConfigParameters {
|
||||
noBrowser?: boolean;
|
||||
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
|
||||
ideMode?: boolean;
|
||||
ideClient?: IdeClient;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
@@ -221,6 +223,7 @@ export class Config {
|
||||
private readonly extensionContextFilePaths: string[];
|
||||
private readonly noBrowser: boolean;
|
||||
private readonly ideMode: boolean;
|
||||
private readonly ideClient: IdeClient | undefined;
|
||||
private modelSwitchedDuringSession: boolean = false;
|
||||
private readonly maxSessionTurns: number;
|
||||
private readonly listExtensions: boolean;
|
||||
@@ -286,6 +289,7 @@ export class Config {
|
||||
this.noBrowser = params.noBrowser ?? false;
|
||||
this.summarizeToolOutput = params.summarizeToolOutput;
|
||||
this.ideMode = params.ideMode ?? false;
|
||||
this.ideClient = params.ideClient;
|
||||
|
||||
if (params.contextFileName) {
|
||||
setGeminiMdFilename(params.contextFileName);
|
||||
@@ -574,6 +578,10 @@ export class Config {
|
||||
return this.ideMode;
|
||||
}
|
||||
|
||||
getIdeClient(): IdeClient | undefined {
|
||||
return this.ideClient;
|
||||
}
|
||||
|
||||
async getGitService(): Promise<GitService> {
|
||||
if (!this.gitService) {
|
||||
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 { setSimulate429 } from '../utils/testUtils.js';
|
||||
import { tokenLimit } from './tokenLimits.js';
|
||||
import { ideContext } from '../services/ideContext.js';
|
||||
import { ideContext } from '../ide/ideContext.js';
|
||||
|
||||
// --- Mocks ---
|
||||
const mockChatCreateFn = vi.fn();
|
||||
@@ -72,7 +72,7 @@ vi.mock('../telemetry/index.js', () => ({
|
||||
logApiResponse: vi.fn(),
|
||||
logApiError: vi.fn(),
|
||||
}));
|
||||
vi.mock('../services/ideContext.js');
|
||||
vi.mock('../ide/ideContext.js');
|
||||
|
||||
describe('findIndexAfterFraction', () => {
|
||||
const history: Content[] = [
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.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 { 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';
|
||||
|
||||
/**
|
||||
* The reserved server name for the IDE's MCP server.
|
||||
*/
|
||||
export const IDE_SERVER_NAME = '_ide_server';
|
||||
/**
|
||||
* Zod schema for validating a cursor position.
|
||||
*/
|
||||
@@ -40,7 +40,10 @@ export * from './utils/systemEncoding.js';
|
||||
// Export services
|
||||
export * from './services/fileDiscoveryService.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 * from './tools/tools.js';
|
||||
|
||||
@@ -24,11 +24,6 @@ import { ToolRegistry } from './tool-registry.js';
|
||||
import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
|
||||
import { OAuthUtils } from '../mcp/oauth-utils.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';
|
||||
|
||||
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
|
||||
@@ -379,24 +374,11 @@ export async function connectAndDiscover(
|
||||
);
|
||||
try {
|
||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTED);
|
||||
|
||||
mcpClient.onerror = (error) => {
|
||||
console.error(`MCP ERROR (${mcpServerName}):`, error.toString());
|
||||
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(
|
||||
mcpServerName,
|
||||
mcpServerConfig,
|
||||
|
||||
Reference in New Issue
Block a user