Merge branch 'main' into connor4312/issue244029

This commit is contained in:
Connor Peet
2025-03-19 21:58:22 -07:00
committed by GitHub
25 changed files with 221 additions and 194 deletions
+4 -29
View File
@@ -9,7 +9,6 @@ import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js'
import { hostname, release } from 'os';
import { VSBuffer } from '../../base/common/buffer.js';
import { toErrorMessage } from '../../base/common/errorMessage.js';
import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from '../../base/common/errors.js';
import { Event } from '../../base/common/event.js';
import { parse } from '../../base/common/jsonc.js';
import { getPathLabel } from '../../base/common/labels.js';
@@ -122,6 +121,7 @@ import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName }
import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeMcpDiscoveryHelperService.js';
import { IWebContentExtractorService } from '../../platform/webContentExtractor/common/webContentExtractor.js';
import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js';
import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js';
/**
* The main VS Code application. There will only ever be one instance,
@@ -373,15 +373,6 @@ export class CodeApplication extends Disposable {
private registerListeners(): void {
// We handle uncaught exceptions here to prevent electron from opening a dialog to the user
setUnexpectedErrorHandler(error => this.onUnexpectedError(error));
process.on('uncaughtException', error => {
if (!isSigPipeError(error)) {
onUnexpectedError(error);
}
});
process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason));
// Dispose on shutdown
Event.once(this.lifecycleMainService.onWillShutdown)(() => this.dispose());
@@ -528,25 +519,6 @@ export class CodeApplication extends Disposable {
//#endregion
}
private onUnexpectedError(error: Error): void {
if (error) {
// take only the message and stack property
const friendlyError = {
message: `[uncaught exception in main]: ${error.message}`,
stack: error.stack
};
// handle on client side
this.windowsMainService?.sendToFocused('vscode:reportError', JSON.stringify(friendlyError));
}
this.logService.error(`[uncaught exception in main]: ${error}`);
if (error.stack) {
this.logService.error(error.stack);
}
}
async startup(): Promise<void> {
this.logService.debug('Starting VS Code');
this.logService.debug(`from: ${this.environmentMainService.appRoot}`);
@@ -603,6 +575,9 @@ export class CodeApplication extends Disposable {
// Services
const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady);
// Error telemetry
appInstantiationService.invokeFunction(accessor => this._register(new ErrorTelemetry(accessor.get(ILogService), accessor.get(ITelemetryService))));
// Auth Handler
appInstantiationService.invokeFunction(accessor => accessor.get(IProxyAuthService));
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from '../../../base/common/errors.js';
import BaseErrorTelemetry from '../common/errorTelemetry.js';
import { ITelemetryService } from '../common/telemetry.js';
import { ILogService } from '../../../platform/log/common/log.js';
export default class ErrorTelemetry extends BaseErrorTelemetry {
constructor(
private readonly logService: ILogService,
@ITelemetryService telemetryService: ITelemetryService
) {
super(telemetryService);
}
protected override installErrorListeners(): void {
// We handle uncaught exceptions here to prevent electron from opening a dialog to the user
setUnexpectedErrorHandler(error => this.onUnexpectedError(error));
process.on('uncaughtException', error => {
if (!isSigPipeError(error)) {
onUnexpectedError(error);
}
});
process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason));
}
private onUnexpectedError(error: Error): void {
this.logService.error(`[uncaught exception in main]: ${error}`);
if (error.stack) {
this.logService.error(error.stack);
}
}
}
@@ -91,7 +91,10 @@ export function registerNewChatActions() {
},
menu: [{
id: MenuId.ChatContext,
group: 'z_clear'
group: 'z_clear',
when: ContextKeyExpr.and(
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),
ChatContextKeys.inUnifiedChat.negate()),
},
{
id: MenuId.ViewTitle,
@@ -244,14 +247,14 @@ export function registerNewChatActions() {
constructor() {
super({
id: 'workbench.action.chat.undoEdit',
title: localize2('chat.undoEdit.label', "Undo Last Edit"),
title: localize2('chat.undoEdit.label', "Undo Last Request"),
category: CHAT_CATEGORY,
icon: Codicon.discard,
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanUndo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
f1: true,
menu: [{
id: MenuId.ViewTitle,
when: ChatContextKeyExprs.inEditingMode,
when: ChatContextKeyExprs.inEditsOrUnified,
group: 'navigation',
order: -3
}]
@@ -267,14 +270,14 @@ export function registerNewChatActions() {
constructor() {
super({
id: 'workbench.action.chat.redoEdit',
title: localize2('chat.redoEdit.label', "Redo Last Edit"),
title: localize2('chat.redoEdit.label', "Redo Last Request"),
category: CHAT_CATEGORY,
icon: Codicon.redo,
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
f1: true,
menu: [{
id: MenuId.ViewTitle,
when: ChatContextKeyExprs.inEditingMode,
when: ChatContextKeyExprs.inEditsOrUnified,
group: 'navigation',
order: -2
}]
@@ -358,7 +358,7 @@ export function registerChatTitleActions() {
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.x,
precondition: ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask),
precondition: ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ChatContextKeyExprs.unifiedChatEnabled.negate()),
keybinding: {
primary: KeyCode.Delete,
mac: {
@@ -371,7 +371,7 @@ export function registerChatTitleActions() {
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ChatContextKeys.isRequest)
when: ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ChatContextKeys.isRequest, ChatContextKeyExprs.unifiedChatEnabled.negate())
}
});
}
@@ -439,7 +439,7 @@ registerAction2(class RemoveAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.undoEdits',
title: localize2('chat.undoEdits.label', "Undo Edits"),
title: localize2('chat.undoEdits.label', "Undo Requests"),
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.x,
@@ -448,7 +448,7 @@ registerAction2(class RemoveAction extends Action2 {
mac: {
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
},
when: ContextKeyExpr.and(ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask), ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
weight: KeybindingWeight.WorkbenchContrib,
},
menu: [
@@ -456,7 +456,7 @@ registerAction2(class RemoveAction extends Action2 {
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask), ChatContextKeys.isRequest)
when: ChatContextKeys.isRequest
}
]
});
@@ -479,7 +479,7 @@ registerAction2(class RemoveAction extends Action2 {
const chatEditingService = accessor.get(IChatEditingService);
const chatService = accessor.get(IChatService);
const chatModel = chatService.getSession(item.sessionId);
if (chatModel?.initialLocation !== ChatAgentLocation.EditingSession) {
if (!chatModel) {
return;
}
@@ -7,11 +7,13 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
import { IDisposable } from '../../../../../base/common/lifecycle.js';
import { autorun } from '../../../../../base/common/observable.js';
import { isEqual } from '../../../../../base/common/resources.js';
import { URI, UriComponents } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { localize } from '../../../../../nls.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { SaveReason } from '../../../../common/editor.js';
import { GroupsOrder, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
import { CellUri } from '../../../notebook/common/notebookCommon.js';
import { INotebookService } from '../../../notebook/common/notebookService.js';
@@ -79,6 +81,7 @@ export class EditTool implements IToolImpl {
@ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService,
@ITextFileService private readonly textFileService: ITextFileService,
@INotebookService private readonly notebookService: INotebookService,
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
) { }
async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {
@@ -88,8 +91,18 @@ export class EditTool implements IToolImpl {
const parameters = invocation.parameters as EditToolParams;
const uri = URI.revive(parameters.file); // TODO@roblourens do revive in MainThreadLanguageModelTools
if (!this.workspaceContextService.isInsideWorkspace(uri)) {
throw new Error(`File ${uri.fsPath} can't be edited because it's not inside the current workspace`);
const groupsByLastActive = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
const uriIsOpenInSomeEditor = groupsByLastActive.some((group) => {
return group.editors.some((editor) => {
return isEqual(editor.resource, uri);
});
});
if (!uriIsOpenInSomeEditor) {
throw new Error(`File ${uri.fsPath} can't be edited because it's not inside the current workspace`);
}
}
if (await this.ignoredFilesService.fileIsIgnored(uri, token)) {
@@ -59,14 +59,14 @@ export class FetchWebPageTool implements IToolImpl {
const contents = await this._readerModeService.extract(validUris);
// Make an array that contains either the content or undefined for invalid URLs
const contentsWithUndefined = new Map<string, string | undefined>();
const contentsWithUndefined: (string | undefined)[] = [];
let indexInContents = 0;
parsedUriResults.forEach((uri, url) => {
parsedUriResults.forEach((uri) => {
if (uri) {
contentsWithUndefined.set(url, contents[indexInContents]);
contentsWithUndefined.push(contents[indexInContents]);
indexInContents++;
} else {
contentsWithUndefined.set(url, undefined);
contentsWithUndefined.push(undefined);
}
});
@@ -154,21 +154,10 @@ export class FetchWebPageTool implements IToolImpl {
return results;
}
private _getPromptPartsForResults(results: Map<string, string | undefined>): IToolResultTextPart[] {
const arr = new Array<IToolResultTextPart>();
for (const [url, content] of results.entries()) {
if (content) {
arr.push({
kind: 'text',
value: `<!-- ${url} -->\n\n` + content
});
} else {
arr.push({
kind: 'text',
value: `<!-- ${url} -->\n\n` + localize('fetchWebPage.invalidUrl', 'Invalid URL')
});
}
}
return arr;
private _getPromptPartsForResults(results: (string | undefined)[]): IToolResultTextPart[] {
return results.map(value => ({
kind: 'text',
value: value || localize('fetchWebPage.invalidUrl', 'Invalid URL')
}));
}
}
@@ -21,7 +21,7 @@ import { McpRegistry } from '../common/mcpRegistry.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { McpService } from '../common/mcpService.js';
import { IMcpService } from '../common/mcpTypes.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { McpDiscovery } from './mcpDiscovery.js';
import { McpLanguageFeatures } from './mcpLanguageFeatures.js';
import { McpUrlHandler } from './mcpUrlHandler.js';
@@ -50,6 +50,7 @@ registerAction2(StartServer);
registerAction2(StopServer);
registerAction2(ShowOutput);
registerAction2(InstallFromActivation);
registerAction2(RestartServer);
registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore);
@@ -471,6 +471,26 @@ export class ShowOutput extends Action2 {
}
}
export class RestartServer extends Action2 {
static readonly ID = 'workbench.mcp.restartServer';
constructor() {
super({
id: RestartServer.ID,
title: localize2('mcp.command.restartServer', "Restart Server"),
category,
f1: false,
});
}
async run(accessor: ServicesAccessor, serverId: string) {
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
s?.showOutput();
await s?.stop();
await s?.start();
}
}
export class StartServer extends Action2 {
static readonly ID = 'workbench.mcp.startServer';
@@ -485,7 +505,6 @@ export class StartServer extends Action2 {
async run(accessor: ServicesAccessor, serverId: string) {
const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId);
await s?.stop();
await s?.start();
}
}
@@ -19,7 +19,7 @@ import { ConfigurationResolverExpression, IResolvedValue } from '../../../servic
import { IMcpConfigPathsService } from '../common/mcpConfigPathsService.js';
import { IMcpRegistry } from '../common/mcpRegistryTypes.js';
import { IMcpService, McpConnectionState } from '../common/mcpTypes.js';
import { EditStoredInput, RemoveStoredInput, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
import { EditStoredInput, RemoveStoredInput, RestartServer, ShowOutput, StartServer, StopServer } from './mcpCommands.js';
export class McpLanguageFeatures extends Disposable implements IWorkbenchContribution {
private readonly _cachedMcpSection = this._register(new MutableDisposable<{ model: ITextModel; node: Node } & IDisposable>());
@@ -113,7 +113,7 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
}, {
range,
command: {
id: StartServer.ID,
id: RestartServer.ID,
title: localize('mcp.restart', "Restart"),
arguments: [server.definition.id],
},
@@ -147,19 +147,19 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
}, {
range,
command: {
id: StartServer.ID,
id: RestartServer.ID,
title: localize('mcp.restart', "Restart"),
arguments: [server.definition.id],
},
}, {
range,
command: {
id: 'workbench.action.chat.attachTools',
id: '',
title: localize('server.toolCount', '{0} tools', read(server.tools).length),
},
});
break;
case McpConnectionState.Kind.Stopped:
case McpConnectionState.Kind.Stopped: {
lenses.lenses.push({
range,
command: {
@@ -168,6 +168,17 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib
arguments: [server.definition.id],
},
});
const toolCount = read(server.tools).length;
if (toolCount) {
lenses.lenses.push({
range,
command: {
id: '',
title: localize('server.toolCountCached', '{0} cached tools', toolCount),
}
});
}
}
}
}
@@ -293,7 +293,7 @@ export class McpRegistry extends Disposable implements IMcpRegistry {
return await this._configurationResolverService.resolveAsync(folder, expr);
}
public async resolveConnection({ collectionRef, definitionRef, forceTrust }: IMcpResolveConnectionOptions): Promise<IMcpServerConnection | undefined> {
public async resolveConnection({ collectionRef, definitionRef, forceTrust, logger }: IMcpResolveConnectionOptions): Promise<IMcpServerConnection | undefined> {
const collection = this._collections.get().find(c => c.id === collectionRef.id);
const definition = collection?.serverDefinitions.get().find(s => s.id === definitionRef.id);
if (!collection || !definition) {
@@ -356,6 +356,7 @@ export class McpRegistry extends Disposable implements IMcpRegistry {
definition,
delegate,
launch,
logger,
);
}
}
@@ -8,6 +8,7 @@ import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable } from '../../../../base/common/observable.js';
import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogger } from '../../../../platform/log/common/log.js';
import { StorageScope } from '../../../../platform/storage/common/storage.js';
import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
import { IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js';
@@ -32,6 +33,7 @@ export interface IMcpHostDelegate {
}
export interface IMcpResolveConnectionOptions {
logger: ILogger;
collectionRef: McpCollectionReference;
definitionRef: McpDefinitionReference;
/** If set, the user will be asked to trust the collection even if they untrusted it previously */
@@ -10,9 +10,11 @@ import { LRUCache } from '../../../../base/common/map.js';
import { autorun, autorunWithStore, derived, disposableObservableValue, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js';
import { basename } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IOutputService } from '../../../services/output/common/output.js';
import { mcpActivationEvent } from './mcpConfiguration.js';
import { IMcpRegistry } from './mcpRegistryTypes.js';
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
@@ -130,6 +132,9 @@ export class McpServer extends Disposable implements IMcpServer {
return fromServerResult.error ? (this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown) : McpServerToolsState.Live;
});
private readonly _loggerId: string;
private readonly _logger: ILogger;
public get trusted() {
return this._mcpRegistry.getTrust(this.collection);
}
@@ -143,9 +148,17 @@ export class McpServer extends Disposable implements IMcpServer {
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
@IWorkspaceContextService workspacesService: IWorkspaceContextService,
@IExtensionService private readonly _extensionService: IExtensionService,
@ILoggerService private readonly _loggerService: ILoggerService,
@IOutputService private readonly _outputService: IOutputService,
) {
super();
this._loggerId = `mcpServer/${definition.id}`;
this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));
// If the logger is disposed but not deregistered, then the disposed instance
// is reused and no-ops. todo@sandy081 this seems like a bug.
this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));
// 1. Reflect workspaces into the MCP roots
const workspaces = explicitRoots
? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) })))
@@ -196,7 +209,8 @@ export class McpServer extends Disposable implements IMcpServer {
}
public showOutput(): void {
this._connection.get()?.showOutput();
this._loggerService.setVisibility(this._loggerId, true);
this._outputService.showChannel(this._loggerId);
}
public start(isFromInteraction?: boolean): Promise<McpConnectionState> {
@@ -222,6 +236,7 @@ export class McpServer extends Disposable implements IMcpServer {
if (!connection) {
connection = await this._mcpRegistry.resolveConnection({
logger: this._logger,
collectionRef: this.collection,
definitionRef: this.definition,
forceTrust: isFromInteraction,
@@ -8,11 +8,10 @@ import { Disposable, DisposableStore, IReference, MutableDisposable, toDisposabl
import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js';
import { localize } from '../../../../nls.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';
import { IOutputService } from '../../../services/output/common/output.js';
import { ILogger } from '../../../../platform/log/common/log.js';
import { IMcpHostDelegate, IMcpMessageTransport } from './mcpRegistryTypes.js';
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
import { McpCollectionDefinition, IMcpServerConnection, McpServerDefinition, McpConnectionState, McpServerLaunch } from './mcpTypes.js';
import { IMcpServerConnection, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from './mcpTypes.js';
export class McpServerConnection extends Disposable implements IMcpServerConnection {
private readonly _launch = this._register(new MutableDisposable<IReference<IMcpMessageTransport>>());
@@ -22,8 +21,6 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect
public readonly state: IObservable<McpConnectionState> = this._state;
public readonly handler: IObservable<McpServerRequestHandler | undefined> = this._requestHandler;
private readonly _loggerId: string;
private readonly _logger: ILogger;
private _launchId = 0;
constructor(
@@ -31,22 +28,10 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect
public readonly definition: McpServerDefinition,
private readonly _delegate: IMcpHostDelegate,
public readonly launchDefinition: McpServerLaunch,
@ILoggerService private readonly _loggerService: ILoggerService,
@IOutputService private readonly _outputService: IOutputService,
private readonly _logger: ILogger,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this._loggerId = `mcpServer/${definition.id}`;
this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));
// If the logger is disposed but not deregistered, then the disposed instance
// is reused and no-ops. todo@sandy081 this seems like a bug.
this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));
}
/** @inheritdoc */
public showOutput(): void {
this._loggerService.setVisibility(this._loggerId, true);
this._outputService.showChannel(this._loggerId);
}
/** @inheritdoc */
@@ -320,11 +320,6 @@ export interface IMcpServerConnection extends IDisposable {
readonly state: IObservable<McpConnectionState>;
readonly handler: IObservable<McpServerRequestHandler | undefined>;
/**
* Shows the current server output.
*/
showOutput(): void;
/**
* Starts the server if it's stopped. Returns a promise that resolves once
* server exits a 'starting' state.
@@ -14,7 +14,7 @@ import { ConfigurationTarget } from '../../../../../platform/configuration/commo
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ILoggerService } from '../../../../../platform/log/common/log.js';
import { ILogger, ILoggerService, NullLogger } from '../../../../../platform/log/common/log.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
import { ISecretStorageService } from '../../../../../platform/secrets/common/secrets.js';
import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js';
@@ -120,6 +120,7 @@ suite('Workbench - MCP - Registry', () => {
let testDialogService: TestDialogService;
let testCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable<McpServerDefinition[]> };
let baseDefinition: McpServerDefinition;
let logger: ILogger;
setup(() => {
testConfigResolverService = new TestConfigurationResolverService();
@@ -136,6 +137,8 @@ suite('Workbench - MCP - Registry', () => {
[IProductService, {}],
);
logger = new NullLogger();
const instaService = store.add(new TestInstantiationService(services));
registry = store.add(instaService.createInstance(McpRegistry));
@@ -211,7 +214,7 @@ suite('Workbench - MCP - Registry', () => {
testCollection.serverDefinitions.set([definition], undefined);
store.add(registry.registerCollection(testCollection));
const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }) as McpServerConnection;
const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }) as McpServerConnection;
assert.ok(connection);
assert.strictEqual(connection.definition, definition);
@@ -219,7 +222,7 @@ suite('Workbench - MCP - Registry', () => {
assert.strictEqual((connection.launchDefinition as any).env.PATH, 'interactiveValue0');
connection.dispose();
const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }) as McpServerConnection;
const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }) as McpServerConnection;
assert.ok(connection2);
assert.strictEqual((connection2.launchDefinition as any).env.PATH, 'interactiveValue0');
@@ -227,7 +230,7 @@ suite('Workbench - MCP - Registry', () => {
registry.clearSavedInputs(StorageScope.WORKSPACE);
const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }) as McpServerConnection;
const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }) as McpServerConnection;
assert.ok(connection3);
assert.strictEqual((connection3.launchDefinition as any).env.PATH, 'interactiveValue4');
@@ -245,7 +248,7 @@ suite('Workbench - MCP - Registry', () => {
store.add(registry.registerCollection(testCollection));
testCollection.serverDefinitions.set([definition], undefined);
const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition });
const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger });
assert.ok(connection);
assert.strictEqual(testDialogService.promptSpy.called, false);
@@ -265,6 +268,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.setPromptResult(true);
const connection = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition
});
@@ -275,6 +279,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.promptSpy.resetHistory();
const connection2 = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition
});
@@ -297,6 +302,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.setPromptResult(false);
const connection = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition
});
@@ -306,6 +312,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.promptSpy.resetHistory();
const connection2 = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition
});
@@ -327,6 +334,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.setPromptResult(false);
const connection1 = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition
});
@@ -337,6 +345,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.setPromptResult(true);
const connection2 = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition,
forceTrust: true
@@ -348,6 +357,7 @@ suite('Workbench - MCP - Registry', () => {
testDialogService.promptSpy.resetHistory();
const connection3 = await registry.resolveConnection({
logger,
collectionRef: untrustedCollection,
definitionRef: definition
});
@@ -12,7 +12,7 @@ import { URI } from '../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ILogger, ILoggerService } from '../../../../../platform/log/common/log.js';
import { ILogger, ILoggerService, NullLogger } from '../../../../../platform/log/common/log.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
import { IOutputService } from '../../../../services/output/common/output.js';
@@ -127,7 +127,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -154,7 +155,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -172,7 +174,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -197,7 +200,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -220,7 +224,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -251,7 +256,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
// Start the connection
@@ -265,81 +271,24 @@ suite('Workbench - MCP - ServerConnection', () => {
assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);
});
test('showOutput should call logger and output services', () => {
let channelShown = false;
const outputService = {
showChannel: (id: string) => {
assert.strictEqual(id, `mcpServer/${serverDefinition.id}`);
channelShown = true;
}
};
let loggerVisible = false;
const loggerService = new class extends TestLoggerService {
override setVisibility(id: string, visible: boolean): void {
assert.strictEqual(id, `mcpServer/${serverDefinition.id}`);
assert.strictEqual(visible, true);
loggerVisible = true;
}
};
// Override services
const services = new ServiceCollection(
[ILoggerService, store.add(loggerService)],
[IOutputService, upcast(outputService)],
[IStorageService, store.add(new TestStorageService())]
);
const localInstantiationService = store.add(new TestInstantiationService(services));
// Create server connection
const connection = localInstantiationService.createInstance(
McpServerConnection,
collection,
serverDefinition,
delegate,
serverDefinition.launch
);
store.add(connection);
// Show output
connection.showOutput();
assert.strictEqual(channelShown, true);
assert.strictEqual(loggerVisible, true);
});
test('should log transport messages', async () => {
// Track logged messages
const loggedMessages: string[] = [];
const loggerService = new class extends TestLoggerService {
override createLogger(id: string) {
return {
info: (message: string) => {
loggedMessages.push(message);
},
error: () => { },
dispose: () => { }
} as Partial<ILogger> as ILogger;
}
};
// Override services
const services = new ServiceCollection(
[ILoggerService, store.add(loggerService)],
[IOutputService, upcast({ showChannel: () => { } })],
[IStorageService, store.add(new TestStorageService())]
);
const localInstantiationService = store.add(new TestInstantiationService(services));
// Create server connection
const connection = localInstantiationService.createInstance(
const connection = instantiationService.createInstance(
McpServerConnection,
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
{
info: (message: string) => {
loggedMessages.push(message);
},
error: () => { },
dispose: () => { }
} as Partial<ILogger> as ILogger,
);
store.add(connection);
@@ -355,6 +304,9 @@ suite('Workbench - MCP - ServerConnection', () => {
// Check that the message was logged
assert.ok(loggedMessages.some(msg => msg === 'Test log message'));
connection.dispose();
await timeout(10);
});
test('should correctly handle transitions to and from error state', async () => {
@@ -364,7 +316,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -401,7 +354,8 @@ suite('Workbench - MCP - ServerConnection', () => {
collection,
serverDefinition,
delegate,
serverDefinition.launch
serverDefinition.launch,
new NullLogger(),
);
store.add(connection);
@@ -929,9 +929,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD
deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]) {
if (diffSide === DiffSide.Original) {
this._originalWebview?.deltaCellContainerClassNames(cellId, added, removed);
this._originalWebview?.deltaCellOutputContainerClassNames(cellId, added, removed);
} else {
this._modifiedWebview?.deltaCellContainerClassNames(cellId, added, removed);
this._modifiedWebview?.deltaCellOutputContainerClassNames(cellId, added, removed);
}
}
@@ -807,7 +807,7 @@ export interface INotebookEditorDelegate extends INotebookEditor {
* Hide the inset in the webview layer without removing it
*/
hideInset(output: IDisplayOutputViewModel): void;
deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]): void;
deltaCellContainerClassNames(cellId: string, added: string[], removed: string[], cellKind: CellKind): void;
}
export interface IActiveNotebookEditorDelegate extends INotebookEditorDelegate {
@@ -1617,21 +1617,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
store.add(cell.onCellDecorationsChanged(e => {
e.added.forEach(options => {
if (options.className) {
this.deltaCellContainerClassNames(cell.id, [options.className], []);
this.deltaCellContainerClassNames(cell.id, [options.className], [], cell.cellKind);
}
if (options.outputClassName) {
this.deltaCellContainerClassNames(cell.id, [options.outputClassName], []);
this.deltaCellContainerClassNames(cell.id, [options.outputClassName], [], cell.cellKind);
}
});
e.removed.forEach(options => {
if (options.className) {
this.deltaCellContainerClassNames(cell.id, [], [options.className]);
this.deltaCellContainerClassNames(cell.id, [], [options.className], cell.cellKind);
}
if (options.outputClassName) {
this.deltaCellContainerClassNames(cell.id, [], [options.outputClassName]);
this.deltaCellContainerClassNames(cell.id, [], [options.outputClassName], cell.cellKind);
}
});
}));
@@ -2285,8 +2285,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
return ret;
}
deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]) {
this._webview?.deltaCellContainerClassNames(cellId, added, removed);
deltaCellContainerClassNames(cellId: string, added: string[], removed: string[], cellkind: CellKind): void {
if (cellkind === CellKind.Markup) {
this._webview?.deltaMarkupPreviewClassNames(cellId, added, removed);
} else {
this._webview?.deltaCellOutputContainerClassNames(cellId, added, removed);
}
}
changeModelDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null {
@@ -72,11 +72,11 @@ export class CellDecorations extends CellContentPart {
this.currentCell.getCellDecorations().forEach(options => {
if (options.className && this.currentCell) {
this.rootContainer.classList.add(options.className);
this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.className], []);
this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.className], [], this.currentCell.cellKind);
}
if (options.outputClassName && this.currentCell) {
this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.outputClassName], []);
this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.outputClassName], [], this.currentCell.cellKind);
}
});
}
@@ -1855,14 +1855,24 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
}
deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]) {
deltaCellOutputContainerClassNames(cellId: string, added: string[], removed: string[]) {
this._sendMessageToWebview({
type: 'decorations',
cellId,
addedClassNames: added,
removedClassNames: removed
});
}
deltaMarkupPreviewClassNames(cellId: string, added: string[], removed: string[]) {
if (this.markupPreviewMapping.get(cellId)) {
this._sendMessageToWebview({
type: 'markupDecorations',
cellId,
addedClassNames: added,
removedClassNames: removed
});
}
}
updateOutputRenderers() {
@@ -305,7 +305,7 @@ export interface IUpdateRenderersMessage {
}
export interface IUpdateDecorationsMessage {
readonly type: 'decorations';
readonly type: 'decorations' | 'markupDecorations';
readonly cellId: string;
readonly addedClassNames: readonly string[];
readonly removedClassNames: readonly string[];
@@ -1780,6 +1780,16 @@ async function webviewPreloads(ctx: PreloadContext) {
outputContainer?.classList.remove(...event.data.removedClassNames);
break;
}
case 'markupDecorations': {
const markupCell = window.document.getElementById(event.data.cellId);
// The cell may not have been added yet if it is out of view.
// Decorations will be added when the cell is shown.
if (markupCell) {
markupCell?.classList.add(...event.data.addedClassNames);
markupCell?.classList.remove(...event.data.removedClassNames);
}
break;
}
case 'customKernelMessage':
onDidReceiveKernelMessage.fire(event.data.message);
break;
@@ -6,7 +6,6 @@
import './media/window.css';
import { localize } from '../../nls.js';
import { URI } from '../../base/common/uri.js';
import { onUnexpectedError } from '../../base/common/errors.js';
import { equals } from '../../base/common/objects.js';
import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindowById, getWindows, $ } from '../../base/browser/dom.js';
import { Action, Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../base/common/actions.js';
@@ -187,13 +186,6 @@ export class NativeWindow extends BaseWindow {
}
});
// Error reporting from main
ipcRenderer.on('vscode:reportError', (event: unknown, error: string) => {
if (error) {
onUnexpectedError(JSON.parse(error));
}
});
// Shared Process crash reported from main
ipcRenderer.on('vscode:reportSharedProcessCrash', (event: unknown, error: string) => {
this.notificationService.prompt(