mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
@@ -6,22 +6,24 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { isSubpath } from '../utils/paths.js';
|
||||
import { detectIde, type DetectedIde, getIdeInfo } from '../ide/detect-ide.js';
|
||||
import type { DiffUpdateResult } from '../ide/ideContext.js';
|
||||
import { detectIde, type IdeInfo } from '../ide/detect-ide.js';
|
||||
import { ideContextStore } from './ideContext.js';
|
||||
import {
|
||||
ideContext,
|
||||
IdeContextNotificationSchema,
|
||||
IdeDiffAcceptedNotificationSchema,
|
||||
IdeDiffClosedNotificationSchema,
|
||||
CloseDiffResponseSchema,
|
||||
} from '../ide/ideContext.js';
|
||||
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';
|
||||
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { EnvHttpProxyAgent } from 'undici';
|
||||
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { IDE_REQUEST_TIMEOUT_MS } from './constants.js';
|
||||
|
||||
const logger = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -30,6 +32,16 @@ const logger = {
|
||||
error: (...args: any[]) => console.error('[ERROR] [IDEClient]', ...args),
|
||||
};
|
||||
|
||||
export type DiffUpdateResult =
|
||||
| {
|
||||
status: 'accepted';
|
||||
content?: string;
|
||||
}
|
||||
| {
|
||||
status: 'rejected';
|
||||
content: undefined;
|
||||
};
|
||||
|
||||
export type IDEConnectionState = {
|
||||
status: IDEConnectionStatus;
|
||||
details?: string; // User-facing
|
||||
@@ -48,6 +60,7 @@ type StdioConfig = {
|
||||
|
||||
type ConnectionConfig = {
|
||||
port?: string;
|
||||
authToken?: string;
|
||||
stdio?: StdioConfig;
|
||||
};
|
||||
|
||||
@@ -65,34 +78,46 @@ function getRealPath(path: string): string {
|
||||
* Manages the connection to and interaction with the IDE server.
|
||||
*/
|
||||
export class IdeClient {
|
||||
private static instance: IdeClient;
|
||||
private static instancePromise: Promise<IdeClient> | null = null;
|
||||
private client: Client | undefined = undefined;
|
||||
private state: IDEConnectionState = {
|
||||
status: IDEConnectionStatus.Disconnected,
|
||||
details:
|
||||
'IDE integration is currently disabled. To enable it, run /ide enable.',
|
||||
};
|
||||
private currentIde: DetectedIde | undefined;
|
||||
private currentIdeDisplayName: string | undefined;
|
||||
private currentIde: IdeInfo | undefined;
|
||||
private ideProcessInfo: { pid: number; command: string } | undefined;
|
||||
private connectionConfig:
|
||||
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
||||
| undefined;
|
||||
private authToken: string | undefined;
|
||||
private diffResponses = new Map<string, (result: DiffUpdateResult) => void>();
|
||||
private statusListeners = new Set<(state: IDEConnectionState) => void>();
|
||||
private trustChangeListeners = new Set<(isTrusted: boolean) => void>();
|
||||
private availableTools: string[] = [];
|
||||
/**
|
||||
* A mutex to ensure that only one diff view is open in the IDE at a time.
|
||||
* This prevents race conditions and UI issues in IDEs like VSCode that
|
||||
* can't handle multiple diff views being opened simultaneously.
|
||||
*/
|
||||
private diffMutex = Promise.resolve();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static async getInstance(): Promise<IdeClient> {
|
||||
if (!IdeClient.instance) {
|
||||
const client = new IdeClient();
|
||||
client.ideProcessInfo = await getIdeProcessInfo();
|
||||
client.currentIde = detectIde(client.ideProcessInfo);
|
||||
if (client.currentIde) {
|
||||
client.currentIdeDisplayName = getIdeInfo(
|
||||
client.currentIde,
|
||||
).displayName;
|
||||
}
|
||||
IdeClient.instance = client;
|
||||
static getInstance(): Promise<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,
|
||||
client.connectionConfig?.ideInfo,
|
||||
);
|
||||
return client;
|
||||
})();
|
||||
}
|
||||
return IdeClient.instance;
|
||||
return IdeClient.instancePromise;
|
||||
}
|
||||
|
||||
addStatusChangeListener(listener: (state: IDEConnectionState) => void) {
|
||||
@@ -103,8 +128,16 @@ export class IdeClient {
|
||||
this.statusListeners.delete(listener);
|
||||
}
|
||||
|
||||
addTrustChangeListener(listener: (isTrusted: boolean) => void) {
|
||||
this.trustChangeListeners.add(listener);
|
||||
}
|
||||
|
||||
removeTrustChangeListener(listener: (isTrusted: boolean) => void) {
|
||||
this.trustChangeListeners.delete(listener);
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (!this.currentIde || !this.currentIdeDisplayName) {
|
||||
if (!this.currentIde) {
|
||||
this.setState(
|
||||
IDEConnectionStatus.Disconnected,
|
||||
`IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks`,
|
||||
@@ -115,14 +148,16 @@ export class IdeClient {
|
||||
|
||||
this.setState(IDEConnectionStatus.Connecting);
|
||||
|
||||
const configFromFile = await this.getConnectionConfigFromFile();
|
||||
this.connectionConfig = await this.getConnectionConfigFromFile();
|
||||
if (this.connectionConfig?.authToken) {
|
||||
this.authToken = this.connectionConfig.authToken;
|
||||
}
|
||||
const workspacePath =
|
||||
configFromFile?.workspacePath ??
|
||||
this.connectionConfig?.workspacePath ??
|
||||
process.env['QWEN_CODE_IDE_WORKSPACE_PATH'];
|
||||
|
||||
const { isValid, error } = IdeClient.validateWorkspacePath(
|
||||
workspacePath,
|
||||
this.currentIdeDisplayName,
|
||||
process.cwd(),
|
||||
);
|
||||
|
||||
@@ -131,18 +166,18 @@ export class IdeClient {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configFromFile) {
|
||||
if (configFromFile.port) {
|
||||
if (this.connectionConfig) {
|
||||
if (this.connectionConfig.port) {
|
||||
const connected = await this.establishHttpConnection(
|
||||
configFromFile.port,
|
||||
this.connectionConfig.port,
|
||||
);
|
||||
if (connected) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (configFromFile.stdio) {
|
||||
if (this.connectionConfig.stdio) {
|
||||
const connected = await this.establishStdioConnection(
|
||||
configFromFile.stdio,
|
||||
this.connectionConfig.stdio,
|
||||
);
|
||||
if (connected) {
|
||||
return;
|
||||
@@ -168,70 +203,185 @@ export class IdeClient {
|
||||
|
||||
this.setState(
|
||||
IDEConnectionStatus.Disconnected,
|
||||
`Failed to connect to IDE companion extension in ${this.currentIdeDisplayName}. Please ensure the extension is running. To install the extension, run /ide install.`,
|
||||
`Failed to connect to IDE companion extension in ${this.currentIde.displayName}. Please ensure the extension is running. To install the extension, run /ide install.`,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A diff is accepted with any modifications if the user performs one of the
|
||||
* following actions:
|
||||
* - Clicks the checkbox icon in the IDE to accept
|
||||
* - Runs `command+shift+p` > "Gemini CLI: Accept Diff in IDE" to accept
|
||||
* - Selects "accept" in the CLI UI
|
||||
* - Saves the file via `ctrl/command+s`
|
||||
* Opens a diff view in the IDE, allowing the user to review and accept or
|
||||
* reject changes.
|
||||
*
|
||||
* A diff is rejected if the user performs one of the following actions:
|
||||
* - Clicks the "x" icon in the IDE
|
||||
* - Runs "Gemini CLI: Close Diff in IDE"
|
||||
* - Selects "no" in the CLI UI
|
||||
* - Closes the file
|
||||
* This method sends a request to the IDE to display a diff between the
|
||||
* current content of a file and the new content provided. It then waits for
|
||||
* a notification from the IDE indicating that the user has either accepted
|
||||
* (potentially with manual edits) or rejected the diff.
|
||||
*
|
||||
* A mutex ensures that only one diff view can be open at a time to prevent
|
||||
* race conditions.
|
||||
*
|
||||
* @param filePath The absolute path to the file to be diffed.
|
||||
* @param newContent The proposed new content for the file.
|
||||
* @returns A promise that resolves with a `DiffUpdateResult`, indicating
|
||||
* whether the diff was 'accepted' or 'rejected' and including the final
|
||||
* content if accepted.
|
||||
*/
|
||||
async openDiff(
|
||||
filePath: string,
|
||||
newContent?: string,
|
||||
newContent: string,
|
||||
): Promise<DiffUpdateResult> {
|
||||
return new Promise<DiffUpdateResult>((resolve, reject) => {
|
||||
const release = await this.acquireMutex();
|
||||
|
||||
const promise = new Promise<DiffUpdateResult>((resolve, reject) => {
|
||||
if (!this.client) {
|
||||
// The promise will be rejected, and the finally block below will release the mutex.
|
||||
return reject(new Error('IDE client is not connected.'));
|
||||
}
|
||||
this.diffResponses.set(filePath, resolve);
|
||||
this.client
|
||||
?.callTool({
|
||||
name: `openDiff`,
|
||||
arguments: {
|
||||
filePath,
|
||||
newContent,
|
||||
.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: `openDiff`,
|
||||
arguments: {
|
||||
filePath,
|
||||
newContent,
|
||||
},
|
||||
},
|
||||
},
|
||||
CallToolResultSchema,
|
||||
{ timeout: IDE_REQUEST_TIMEOUT_MS },
|
||||
)
|
||||
.then((parsedResultData) => {
|
||||
if (parsedResultData.isError) {
|
||||
const textPart = parsedResultData.content.find(
|
||||
(part) => part.type === 'text',
|
||||
);
|
||||
const errorMessage =
|
||||
textPart?.text ?? `Tool 'openDiff' reported an error.`;
|
||||
logger.debug(
|
||||
`Request for openDiff ${filePath} failed with isError:`,
|
||||
errorMessage,
|
||||
);
|
||||
this.diffResponses.delete(filePath);
|
||||
reject(new Error(errorMessage));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.debug(`callTool for ${filePath} failed:`, err);
|
||||
logger.debug(`Request for openDiff ${filePath} failed:`, err);
|
||||
this.diffResponses.delete(filePath);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Ensure the mutex is released only after the diff interaction is complete.
|
||||
promise.finally(release);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async closeDiff(filePath: string): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await this.client?.callTool({
|
||||
name: `closeDiff`,
|
||||
arguments: {
|
||||
filePath,
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Acquires a lock to ensure sequential execution of critical sections.
|
||||
*
|
||||
* This method implements a promise-based mutex. It works by chaining promises.
|
||||
* Each call to `acquireMutex` gets the current `diffMutex` promise. It then
|
||||
* creates a *new* promise (`newMutex`) that will be resolved when the caller
|
||||
* invokes the returned `release` function. The `diffMutex` is immediately
|
||||
* updated to this `newMutex`.
|
||||
*
|
||||
* The method returns a promise that resolves with the `release` function only
|
||||
* *after* the *previous* `diffMutex` promise has resolved. This creates a
|
||||
* queue where each subsequent operation must wait for the previous one to release
|
||||
* the lock.
|
||||
*
|
||||
* @returns A promise that resolves to a function that must be called to
|
||||
* release the lock.
|
||||
*/
|
||||
private acquireMutex(): Promise<() => void> {
|
||||
let release: () => void;
|
||||
const newMutex = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const oldMutex = this.diffMutex;
|
||||
this.diffMutex = newMutex;
|
||||
return oldMutex.then(() => release);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const parsed = CloseDiffResponseSchema.parse(result);
|
||||
return parsed.content;
|
||||
async closeDiff(
|
||||
filePath: string,
|
||||
options?: { suppressNotification?: boolean },
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
if (!this.client) {
|
||||
return undefined;
|
||||
}
|
||||
const resultData = await this.client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: `closeDiff`,
|
||||
arguments: {
|
||||
filePath,
|
||||
suppressNotification: options?.suppressNotification,
|
||||
},
|
||||
},
|
||||
},
|
||||
CallToolResultSchema,
|
||||
{ timeout: IDE_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
if (!resultData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (resultData.isError) {
|
||||
const textPart = resultData.content.find(
|
||||
(part) => part.type === 'text',
|
||||
);
|
||||
const errorMessage =
|
||||
textPart?.text ?? `Tool 'closeDiff' reported an error.`;
|
||||
logger.debug(
|
||||
`Request for closeDiff ${filePath} failed with isError:`,
|
||||
errorMessage,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const textPart = resultData.content.find((part) => part.type === 'text');
|
||||
|
||||
if (textPart?.text) {
|
||||
try {
|
||||
const parsedJson = JSON.parse(textPart.text);
|
||||
if (parsedJson && typeof parsedJson.content === 'string') {
|
||||
return parsedJson.content;
|
||||
}
|
||||
if (parsedJson && parsedJson.content === null) {
|
||||
return undefined;
|
||||
}
|
||||
} catch (_e) {
|
||||
logger.debug(
|
||||
`Invalid JSON in closeDiff response for ${filePath}:`,
|
||||
textPart.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.debug(`callTool for ${filePath} failed:`, err);
|
||||
logger.debug(`Request for closeDiff ${filePath} failed:`, err);
|
||||
}
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Closes the diff. Instead of waiting for a notification,
|
||||
// manually resolves the diff resolver as the desired outcome.
|
||||
async resolveDiffFromCli(filePath: string, outcome: 'accepted' | 'rejected') {
|
||||
const content = await this.closeDiff(filePath);
|
||||
const resolver = this.diffResponses.get(filePath);
|
||||
const content = await this.closeDiff(filePath, {
|
||||
// Suppress notification to avoid race where closing the diff rejects the
|
||||
// request.
|
||||
suppressNotification: true,
|
||||
});
|
||||
|
||||
if (resolver) {
|
||||
if (outcome === 'accepted') {
|
||||
resolver({ status: 'accepted', content });
|
||||
@@ -257,7 +407,7 @@ export class IdeClient {
|
||||
this.client?.close();
|
||||
}
|
||||
|
||||
getCurrentIde(): DetectedIde | undefined {
|
||||
getCurrentIde(): IdeInfo | undefined {
|
||||
return this.currentIde;
|
||||
}
|
||||
|
||||
@@ -266,7 +416,54 @@ export class IdeClient {
|
||||
}
|
||||
|
||||
getDetectedIdeDisplayName(): string | undefined {
|
||||
return this.currentIdeDisplayName;
|
||||
return this.currentIde?.displayName;
|
||||
}
|
||||
|
||||
isDiffingEnabled(): boolean {
|
||||
return (
|
||||
!!this.client &&
|
||||
this.state.status === IDEConnectionStatus.Connected &&
|
||||
this.availableTools.includes('openDiff') &&
|
||||
this.availableTools.includes('closeDiff')
|
||||
);
|
||||
}
|
||||
|
||||
private async discoverTools(): Promise<void> {
|
||||
if (!this.client) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
logger.debug('Discovering tools from IDE...');
|
||||
const response = await this.client.request(
|
||||
{ method: 'tools/list', params: {} },
|
||||
ListToolsResultSchema,
|
||||
);
|
||||
|
||||
// Map the array of tool objects to an array of tool names (strings)
|
||||
this.availableTools = response.tools.map((tool) => tool.name);
|
||||
|
||||
if (this.availableTools.length > 0) {
|
||||
logger.debug(
|
||||
`Discovered ${this.availableTools.length} tools from IDE: ${this.availableTools.join(', ')}`,
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
'IDE supports tool discovery, but no tools are available.',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// It's okay if this fails, the IDE might not support it.
|
||||
// Don't log an error if the method is not found, which is a common case.
|
||||
if (
|
||||
error instanceof Error &&
|
||||
!error.message?.includes('Method not found')
|
||||
) {
|
||||
logger.error(`Error discovering tools from IDE: ${error.message}`);
|
||||
} else {
|
||||
logger.debug('IDE does not support tool discovery.');
|
||||
}
|
||||
this.availableTools = [];
|
||||
}
|
||||
}
|
||||
|
||||
private setState(
|
||||
@@ -297,26 +494,25 @@ export class IdeClient {
|
||||
}
|
||||
|
||||
if (status === IDEConnectionStatus.Disconnected) {
|
||||
ideContext.clearIdeContext();
|
||||
ideContextStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static validateWorkspacePath(
|
||||
ideWorkspacePath: string | undefined,
|
||||
currentIdeDisplayName: string | undefined,
|
||||
cwd: string,
|
||||
): { isValid: boolean; error?: string } {
|
||||
if (ideWorkspacePath === undefined) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Failed to connect to IDE companion extension in ${currentIdeDisplayName}. Please ensure the extension is running. To install the extension, run /ide install.`,
|
||||
error: `Failed to connect to IDE companion extension. Please ensure the extension is running. To install the extension, run /ide install.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (ideWorkspacePath === '') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `To use this feature, please open a workspace folder in ${currentIdeDisplayName} and try again.`,
|
||||
error: `To use this feature, please open a workspace folder in your IDE and try again.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,7 +526,7 @@ export class IdeClient {
|
||||
if (!isWithinWorkspace) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Directory mismatch. Qwen Code is running in a different location than the open workspace in ${currentIdeDisplayName}. Please run the CLI from one of the following directories: ${ideWorkspacePaths.join(
|
||||
error: `Directory mismatch. Qwen Code is running in a different location than the open workspace in the IDE. Please run the CLI from one of the following directories: ${ideWorkspacePaths.join(
|
||||
', ',
|
||||
)}`,
|
||||
};
|
||||
@@ -373,11 +569,14 @@ export class IdeClient {
|
||||
}
|
||||
|
||||
private async getConnectionConfigFromFile(): Promise<
|
||||
(ConnectionConfig & { workspacePath?: string }) | undefined
|
||||
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
||||
| undefined
|
||||
> {
|
||||
if (!this.ideProcessInfo) {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For backwards compatability
|
||||
try {
|
||||
const portFile = path.join(
|
||||
os.tmpdir(),
|
||||
@@ -386,8 +585,85 @@ export class IdeClient {
|
||||
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);
|
||||
} catch (e) {
|
||||
logger.debug('Failed to read IDE connection directory:', e);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!portFiles) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fileRegex = new RegExp(
|
||||
`^qwen-code-ide-server-${this.ideProcessInfo.pid}-\\d+\\.json$`,
|
||||
);
|
||||
const matchingFiles = portFiles
|
||||
.filter((file) => fileRegex.test(file))
|
||||
.sort();
|
||||
if (matchingFiles.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let fileContents: string[];
|
||||
try {
|
||||
fileContents = await Promise.all(
|
||||
matchingFiles.map((file) =>
|
||||
fs.promises.readFile(path.join(portFileDir, file), 'utf8'),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
logger.debug('Failed to read IDE connection config file(s):', e);
|
||||
return undefined;
|
||||
}
|
||||
const parsedContents = fileContents.map((content) => {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
logger.debug('Failed to parse JSON from config file: ', e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const validWorkspaces = parsedContents.filter((content) => {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
const { isValid } = IdeClient.validateWorkspacePath(
|
||||
content.workspacePath,
|
||||
process.cwd(),
|
||||
);
|
||||
return isValid;
|
||||
});
|
||||
|
||||
if (validWorkspaces.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (validWorkspaces.length === 1) {
|
||||
return validWorkspaces[0];
|
||||
}
|
||||
|
||||
const portFromEnv = this.getPortFromEnv();
|
||||
if (portFromEnv) {
|
||||
const matchingPort = validWorkspaces.find(
|
||||
(content) => String(content.port) === portFromEnv,
|
||||
);
|
||||
if (matchingPort) {
|
||||
return matchingPort;
|
||||
}
|
||||
}
|
||||
|
||||
return validWorkspaces[0];
|
||||
}
|
||||
|
||||
private createProxyAwareFetch() {
|
||||
@@ -414,7 +690,7 @@ export class IdeClient {
|
||||
return new Response(response.body as ReadableStream<unknown> | null, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: standardHeaders,
|
||||
headers: [...response.headers.entries()],
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -427,20 +703,27 @@ export class IdeClient {
|
||||
this.client.setNotificationHandler(
|
||||
IdeContextNotificationSchema,
|
||||
(notification) => {
|
||||
ideContext.setIdeContext(notification.params);
|
||||
ideContextStore.set(notification.params);
|
||||
const isTrusted = notification.params.workspaceState?.isTrusted;
|
||||
if (isTrusted !== undefined) {
|
||||
for (const listener of this.trustChangeListeners) {
|
||||
listener(isTrusted);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
this.client.onerror = (_error) => {
|
||||
const errorMessage = _error instanceof Error ? _error.message : `_error`;
|
||||
this.setState(
|
||||
IDEConnectionStatus.Disconnected,
|
||||
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`,
|
||||
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable\n${errorMessage}`,
|
||||
true,
|
||||
);
|
||||
};
|
||||
this.client.onclose = () => {
|
||||
this.setState(
|
||||
IDEConnectionStatus.Disconnected,
|
||||
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`,
|
||||
`IDE connection closed. To reconnect, run /ide enable.`,
|
||||
true,
|
||||
);
|
||||
};
|
||||
@@ -458,6 +741,22 @@ export class IdeClient {
|
||||
},
|
||||
);
|
||||
|
||||
this.client.setNotificationHandler(
|
||||
IdeDiffRejectedNotificationSchema,
|
||||
(notification) => {
|
||||
const { filePath } = notification.params;
|
||||
const resolver = this.diffResponses.get(filePath);
|
||||
if (resolver) {
|
||||
resolver({ status: 'rejected', content: undefined });
|
||||
this.diffResponses.delete(filePath);
|
||||
} else {
|
||||
logger.debug(`No resolver found for ${filePath}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// For backwards compatability. Newer extension versions will only send
|
||||
// IdeDiffRejectedNotificationSchema.
|
||||
this.client.setNotificationHandler(
|
||||
IdeDiffClosedNotificationSchema,
|
||||
(notification) => {
|
||||
@@ -487,6 +786,11 @@ export class IdeClient {
|
||||
new URL(`http://${getIdeServerHost()}:${port}/mcp`),
|
||||
{
|
||||
fetch: this.createProxyAwareFetch(),
|
||||
requestInit: {
|
||||
headers: this.authToken
|
||||
? { Authorization: `Bearer ${this.authToken}` }
|
||||
: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -494,6 +798,7 @@ export class IdeClient {
|
||||
|
||||
await this.client.connect(transport);
|
||||
this.registerClientHandlers();
|
||||
await this.discoverTools();
|
||||
this.setState(IDEConnectionStatus.Connected);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
@@ -527,6 +832,7 @@ export class IdeClient {
|
||||
});
|
||||
await this.client.connect(transport);
|
||||
this.registerClientHandlers();
|
||||
await this.discoverTools();
|
||||
this.setState(IDEConnectionStatus.Connected);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
|
||||
Reference in New Issue
Block a user