From 554340042e6632879d438cfa44d071190e97d8f8 Mon Sep 17 00:00:00 2001 From: Isidor Date: Thu, 19 Feb 2026 16:35:54 +0100 Subject: [PATCH 1/9] chat: remove restore to last checkpoint action --- .../browser/actions/chatAccessibilityHelp.ts | 1 - .../browser/chatEditing/chatEditingActions.ts | 58 ------------------- .../contrib/chat/browser/chatTipService.ts | 2 +- 3 files changed, 1 insertion(+), 60 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index bfff1a5f0309f..177661c8f6ff4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -113,7 +113,6 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age } content.push(localize('chatEditing.helpfulCommands', 'Some helpful commands include:')); content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '')); - content.push(localize('workbench.action.chat.restoreLastCheckpoint', '- Restore to Last Checkpoint{0}.', '')); content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '')); content.push(localize('chatEditing.removeFileFromWorkingSet', '- Remove File from Working Set{0}.', '')); content.push(localize('chatEditing.acceptFile', '- Keep{0} and Undo File{1}.', '', '')); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 388edd3f00c23..488358e0c902e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -6,7 +6,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { basename } from '../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -599,63 +598,6 @@ registerAction2(class RestoreCheckpointAction extends Action2 { } }); -registerAction2(class RestoreLastCheckpoint extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.restoreLastCheckpoint', - title: localize2('chat.restoreLastCheckpoint.label', "Restore to Last Checkpoint"), - f1: true, - category: CHAT_CATEGORY, - icon: Codicon.discard, - precondition: ContextKeyExpr.and( - ChatContextKeys.inChatSession, - ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), - ChatContextKeys.lockedToCodingAgent.negate() - ), - menu: [ - { - id: MenuId.ChatMessageFooter, - group: 'navigation', - order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), ChatContextKeys.lockedToCodingAgent.negate()), - } - ] - }); - } - - async run(accessor: ServicesAccessor, ...args: unknown[]) { - let item = args[0] as ChatTreeItem | undefined; - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); - const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget; - if (!isResponseVM(item) && !isRequestVM(item)) { - item = widget?.getFocus(); - } - - const sessionResource = widget?.viewModel?.sessionResource ?? (isChatTreeItem(item) ? item.sessionResource : undefined); - if (!sessionResource) { - return; - } - - const chatModel = chatService.getSession(sessionResource); - if (!chatModel?.editingSession) { - return; - } - - const checkpointRequest = chatModel.checkpoint; - if (!checkpointRequest) { - alert(localize('chat.restoreCheckpoint.none', 'There is no checkpoint to restore.')); - return; - } - - widget?.viewModel?.model.setCheckpoint(checkpointRequest.id); - widget?.focusInput(); - widget?.input.setValue(checkpointRequest.message.text, false); - - await restoreSnapshotWithConfirmationByRequestId(accessor, sessionResource, checkpointRequest.id); - } -}); - registerAction2(class EditAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 2e6c540a88fcb..d98b288922843 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -210,7 +210,7 @@ const TIP_CATALOG: ITipDefinition[] = [ ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), ), ), - excludeWhenCommandsExecuted: ['workbench.action.chat.restoreCheckpoint', 'workbench.action.chat.restoreLastCheckpoint'], + excludeWhenCommandsExecuted: ['workbench.action.chat.restoreCheckpoint'], }, { id: 'tip.customInstructions', From 4a952339f1256955bee60596804bb348877c926f Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:58:00 -0800 Subject: [PATCH 2/9] Enabling sandboxing for local MCP servers at workspace level. (#295704) * Enabling MCP sandboxing * Added debug logs for troubleshooting * changes for confirmation window during MCP server run * refactoring changes * refactoring changes * code review comments * code review comments * code review comments * code review comments * fixing tests * Code review comments * code review * Code review comments --- .../platform/mcp/common/mcpPlatformTypes.ts | 14 + .../mcp/common/mcpResourceScannerService.ts | 21 +- .../contrib/mcp/browser/mcp.contribution.ts | 2 + .../discovery/installedMcpServersDiscovery.ts | 2 + .../contrib/mcp/common/mcpConfiguration.ts | 49 ++++ .../mcpLanguageModelToolContribution.ts | 27 +- .../contrib/mcp/common/mcpRegistry.ts | 4 + .../contrib/mcp/common/mcpSandboxService.ts | 240 ++++++++++++++++++ .../workbench/contrib/mcp/common/mcpServer.ts | 4 +- .../workbench/contrib/mcp/common/mcpTypes.ts | 14 +- .../mcp/test/common/mcpRegistry.test.ts | 78 ++++++ 11 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index b5ea52d7b88ca..985d17f1dc7ac 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -12,6 +12,18 @@ export interface IMcpDevModeConfig { debug?: { type: 'node' } | { type: 'debugpy'; debugpyPath?: string }; } +export interface IMcpSandboxConfiguration { + network?: { + allowedDomains?: string[]; + deniedDomains?: string[]; + }; + filesystem?: { + denyRead?: string[]; + allowWrite?: string[]; + denyWrite?: string[]; + }; +} + export const enum McpServerVariableType { PROMPT = 'promptString', PICK = 'pickString', @@ -45,6 +57,8 @@ export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfigurat readonly env?: Record; readonly envFile?: string; readonly cwd?: string; + readonly sandboxEnabled?: boolean; + readonly sandbox?: IMcpSandboxConfiguration; readonly dev?: IMcpDevModeConfig; } diff --git a/src/vs/platform/mcp/common/mcpResourceScannerService.ts b/src/vs/platform/mcp/common/mcpResourceScannerService.ts index 69a0f04f4998e..cd8e0a9f0eb88 100644 --- a/src/vs/platform/mcp/common/mcpResourceScannerService.ts +++ b/src/vs/platform/mcp/common/mcpResourceScannerService.ts @@ -18,11 +18,12 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IInstallableMcpServer } from './mcpManagement.js'; -import { ICommonMcpServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from './mcpPlatformTypes.js'; +import { ICommonMcpServerConfiguration, IMcpSandboxConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from './mcpPlatformTypes.js'; interface IScannedMcpServers { servers?: IStringDictionary>; inputs?: IMcpServerVariable[]; + sandbox?: IMcpSandboxConfiguration; } interface IOldScannedMcpServer { @@ -77,7 +78,7 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc updatedInputs = [...updatedInputs, ...newInputs]; } } - return { servers: existingServers, inputs: updatedInputs }; + return { servers: existingServers, inputs: updatedInputs, sandbox: scannedMcpServers.sandbox }; }); } @@ -173,13 +174,14 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc private fromUserMcpServers(scannedMcpServers: IScannedMcpServers): IScannedMcpServers { const userMcpServers: IScannedMcpServers = { - inputs: scannedMcpServers.inputs + inputs: scannedMcpServers.inputs, + sandbox: scannedMcpServers.sandbox }; const servers = Object.entries(scannedMcpServers.servers ?? {}); if (servers.length > 0) { userMcpServers.servers = {}; for (const [serverName, server] of servers) { - userMcpServers.servers[serverName] = this.sanitizeServer(server); + userMcpServers.servers[serverName] = this.sanitizeServer(server, scannedMcpServers.sandbox); } } return userMcpServers; @@ -187,19 +189,20 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc private fromWorkspaceFolderMcpServers(scannedWorkspaceFolderMcpServers: IScannedMcpServers): IScannedMcpServers { const scannedMcpServers: IScannedMcpServers = { - inputs: scannedWorkspaceFolderMcpServers.inputs + inputs: scannedWorkspaceFolderMcpServers.inputs, + sandbox: scannedWorkspaceFolderMcpServers.sandbox }; const servers = Object.entries(scannedWorkspaceFolderMcpServers.servers ?? {}); if (servers.length > 0) { scannedMcpServers.servers = {}; for (const [serverName, config] of servers) { - scannedMcpServers.servers[serverName] = this.sanitizeServer(config); + scannedMcpServers.servers[serverName] = this.sanitizeServer(config, scannedWorkspaceFolderMcpServers.sandbox); } } return scannedMcpServers; } - private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable): IMcpServerConfiguration { + private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable, sandbox?: IMcpSandboxConfiguration): IMcpServerConfiguration { let server: IMcpServerConfiguration; if ((serverOrConfig).config) { const oldScannedMcpServer = serverOrConfig; @@ -216,6 +219,10 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc (>server).type = (server).command ? McpServerType.LOCAL : McpServerType.REMOTE; } + if (sandbox && server.type === McpServerType.LOCAL && !(server as IMcpStdioServerConfiguration).sandbox && server.sandboxEnabled) { + (>server).sandbox = sandbox; + } + return server; } diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 9bdf3329e64ee..d1c8e7ea22b28 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -30,6 +30,7 @@ import { IMcpDevModeDebugging, McpDevModeDebugging } from '../common/mcpDevMode. import { McpLanguageModelToolContribution } from '../common/mcpLanguageModelToolContribution.js'; import { McpRegistry } from '../common/mcpRegistry.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { IMcpSandboxService, McpSandboxService } from '../common/mcpSandboxService.js'; import { McpResourceFilesystem } from '../common/mcpResourceFilesystem.js'; import { McpSamplingService } from '../common/mcpSamplingService.js'; import { McpService } from '../common/mcpService.js'; @@ -50,6 +51,7 @@ import { McpServersViewsContribution } from './mcpServersView.js'; import { MCPContextsInitialisation, McpWorkbenchService } from './mcpWorkbenchService.js'; registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed); +registerSingleton(IMcpSandboxService, McpSandboxService, InstantiationType.Delayed); registerSingleton(IMcpService, McpService, InstantiationType.Delayed); registerSingleton(IMcpWorkbenchService, McpWorkbenchService, InstantiationType.Eager); registerSingleton(IMcpDevModeDebugging, McpDevModeDebugging, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts index 990d135833d48..601ce1eff09bb 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts @@ -102,6 +102,8 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc id: `${collectionId}.${server.name}`, label: server.name, launch, + sandboxEnabled: config.type === 'http' ? undefined : config.sandboxEnabled, + sandbox: config.type === 'http' || !config.sandboxEnabled ? undefined : config.sandbox, cacheNonce: await McpServerLaunch.hash(launch), roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined, variableReplacement: { diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index c15e39910e340..e74c8948c3cc2 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -136,6 +136,11 @@ export const mcpStdioServerSchema: IJSONSchema = { enum: ['stdio'], description: localize('app.mcp.json.type', "The type of the server.") }, + sandboxEnabled: { + type: 'boolean', + default: false, + description: localize('app.mcp.json.sandboxEnabled', "Whether to run the server in a sandboxed environment.") + }, command: { type: 'string', description: localize('app.mcp.json.command', "The command to run the server.") @@ -179,6 +184,50 @@ export const mcpServerSchema: IJSONSchema = { allowComments: true, additionalProperties: false, properties: { + sandbox: { + description: localize('app.mcp.json.sandbox', "Default sandbox settings for running servers."), + type: 'object', + additionalProperties: false, + properties: { + network: { + type: 'object', + additionalProperties: false, + properties: { + allowedDomains: { + type: 'array', + items: { type: 'string' }, + default: [] + }, + deniedDomains: { + type: 'array', + items: { type: 'string' }, + default: [] + } + } + }, + filesystem: { + type: 'object', + additionalProperties: false, + properties: { + denyRead: { + type: 'array', + items: { type: 'string' }, + default: [] + }, + allowWrite: { + type: 'array', + items: { type: 'string' }, + default: [] + }, + denyWrite: { + type: 'array', + items: { type: 'string' }, + default: [] + } + } + } + } + }, servers: { examples: [ mcpSchemaExampleServers, diff --git a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts index 10224c53daa34..9ef7cf37e5f6e 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts @@ -30,6 +30,7 @@ import { IMcpRegistry } from './mcpRegistryTypes.js'; import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType, McpToolVisibility } from './mcpTypes.js'; import { mcpServerToSourceData } from './mcpTypesUtils.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { McpServer } from './mcpServer.js'; interface ISyncedToolData { toolData: IToolData; @@ -205,6 +206,11 @@ class McpToolImplementation implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext): Promise { const tool = this._tool; const server = this._server; + // ToDO: need to be revisited as the first tool invocation doesnt have sandbox info and we are optimistically assuming it is not sandboxed. We should ideally have the sandbox info. + const sandboxEnabled = await McpServer.callOn(server, async (_handler, connection) => { + return connection.definition.sandboxEnabled; + }); + const isSandboxedServer = sandboxEnabled === true; const mcpToolWarning = localize( 'mcp.tool.warning', @@ -215,15 +221,18 @@ class McpToolImplementation implements IToolImpl { // duplicative: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/813 const title = tool.definition.annotations?.title || tool.definition.title || ('`' + tool.definition.name + '`'); - const confirm: IToolConfirmationMessages = {}; - if (!tool.definition.annotations?.readOnlyHint) { - confirm.title = new MarkdownString(localize('msg.title', "Run {0}", title)); - confirm.message = new MarkdownString(tool.definition.description, { supportThemeIcons: true }); - confirm.disclaimer = mcpToolWarning; - confirm.allowAutoConfirm = true; - } - if (tool.definition.annotations?.openWorldHint) { - confirm.confirmResults = true; + let confirm: IToolConfirmationMessages | undefined; + if (!isSandboxedServer) { + confirm = {}; + if (!tool.definition.annotations?.readOnlyHint) { + confirm.title = new MarkdownString(localize('msg.title', "Run {0}", title)); + confirm.message = new MarkdownString(tool.definition.description, { supportThemeIcons: true }); + confirm.disclaimer = mcpToolWarning; + confirm.allowAutoConfirm = true; + } + if (tool.definition.annotations?.openWorldHint) { + confirm.confirmResults = true; + } } const mcpUiEnabled = this._configurationService.getValue(mcpAppsEnabledConfig); diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index f650edaec320a..20500e68f87a8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -32,6 +32,7 @@ import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/commo import { IMcpDevModeDebugging } from './mcpDevMode.js'; import { McpRegistryInputStorage } from './mcpRegistryInputStorage.js'; import { IMcpHostDelegate, IMcpRegistry, IMcpResolveConnectionOptions } from './mcpRegistryTypes.js'; +import { IMcpSandboxService } from './mcpSandboxService.js'; import { McpServerConnection } from './mcpServerConnection.js'; import { IMcpServerConnection, LazyCollectionState, McpCollectionDefinition, McpDefinitionReference, McpServerDefinition, McpServerLaunch, McpServerTrust, McpStartServerInteraction, UserInteractionRequiredError } from './mcpTypes.js'; @@ -85,6 +86,7 @@ export class McpRegistry extends Disposable implements IMcpRegistry { @IQuickInputService private readonly _quickInputService: IQuickInputService, @ILabelService private readonly _labelService: ILabelService, @ILogService private readonly _logService: ILogService, + @IMcpSandboxService private readonly _mcpSandboxService: IMcpSandboxService, ) { super(); this._mcpAccessValue = observableConfigValue(mcpAccessConfig, McpAccessValue.All, configurationService); @@ -509,6 +511,8 @@ export class McpRegistry extends Disposable implements IMcpRegistry { if (definition.devMode && debug) { launch = await this._instantiationService.invokeFunction(accessor => accessor.get(IMcpDevModeDebugging).transform(definition, launch!)); } + // If sandbox is enabled for this server, attempt to launch in sandbox + launch = await this._mcpSandboxService.launchInSandboxIfEnabled(definition, launch, collection.remoteAuthority ?? undefined, collection.configTarget); } catch (e) { if (e instanceof UserInteractionRequiredError) { throw e; diff --git a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts new file mode 100644 index 0000000000000..f4cf13707d20c --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts @@ -0,0 +1,240 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { FileAccess } from '../../../../base/common/network.js'; +import { dirname, posix, win32 } from '../../../../base/common/path.js'; +import { OperatingSystem, OS } from '../../../../base/common/platform.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { ConfigurationTarget, ConfigurationTargetToString } from '../../../../platform/configuration/common/configuration.js'; +import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js'; +import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { IMcpSandboxConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { McpServerDefinition, McpServerLaunch, McpServerTransportType } from './mcpTypes.js'; + +export const IMcpSandboxService = createDecorator('mcpSandboxService'); + +export interface IMcpSandboxService { + readonly _serviceBrand: undefined; + launchInSandboxIfEnabled(serverDef: McpServerDefinition, launch: McpServerLaunch, remoteAuthority: string | undefined, configTarget: ConfigurationTarget): Promise; + isEnabled(serverDef: McpServerDefinition, serverLabel?: string): Promise; +} + +type SandboxLaunchDetails = { + execPath: string | undefined; + srtPath: string | undefined; + sandboxConfigPath: string | undefined; + tempDir: URI | undefined; +}; + +export class McpSandboxService extends Disposable implements IMcpSandboxService { + readonly _serviceBrand: undefined; + + private _sandboxSettingsId: string | undefined; + private _remoteEnvDetailsPromise: Promise; + private readonly _defaultAllowedDomains: readonly string[] = ['*.npmjs.org']; + private _sandboxConfigPerConfigurationTarget: Map = new Map(); + + constructor( + @IFileService private readonly _fileService: IFileService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @ILogService private readonly _logService: ILogService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + ) { + super(); + this._sandboxSettingsId = generateUuid(); + this._remoteEnvDetailsPromise = this._remoteAgentService.getEnvironment(); + + } + + public async isEnabled(serverDef: McpServerDefinition, remoteAuthority?: string): Promise { + const os = await this._getOperatingSystem(remoteAuthority); + if (os === OperatingSystem.Windows) { + return false; + } + return !!serverDef.sandboxEnabled; + } + + public async launchInSandboxIfEnabled(serverDef: McpServerDefinition, launch: McpServerLaunch, remoteAuthority: string | undefined, configTarget: ConfigurationTarget): Promise { + if (launch.type !== McpServerTransportType.Stdio) { + return launch; + } + if (await this.isEnabled(serverDef, remoteAuthority)) { + this._logService.trace(`McpSandboxService: Launching with config target ${configTarget}`); + const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, serverDef.sandbox); + const sandboxArgs = this._getSandboxCommandArgs(launch.command, launch.args, launchDetails.sandboxConfigPath); + const sandboxEnv = this._getSandboxEnvVariables(launchDetails.tempDir, remoteAuthority); + if (launchDetails.srtPath) { + const envWithSandbox = sandboxEnv ? { ...launch.env, ...sandboxEnv } : launch.env; + if (launchDetails.execPath) { + return { + ...launch, + command: launchDetails.execPath, + args: [launchDetails.srtPath, ...sandboxArgs], + env: envWithSandbox, + type: McpServerTransportType.Stdio, + }; + } else { + return { + ...launch, + command: launchDetails.srtPath, + args: sandboxArgs, + env: envWithSandbox, + type: McpServerTransportType.Stdio, + }; + } + } + if (!launchDetails.execPath) { + this._logService.warn('McpSandboxService: execPath is unavailable, launching without sandbox runtime wrapper'); + } + this._logService.debug(`McpSandboxService: launch details for server ${serverDef.label} - command: ${launch.command}, args: ${launch.args.join(' ')}`); + } + return launch; + } + + private async _resolveSandboxLaunchDetails(configTarget: ConfigurationTarget, remoteAuthority?: string, sandboxConfig?: IMcpSandboxConfiguration): Promise { + const os = await this._getOperatingSystem(remoteAuthority); + if (os === OperatingSystem.Windows) { + return { execPath: undefined, srtPath: undefined, sandboxConfigPath: undefined, tempDir: undefined }; + } + + const appRoot = await this._getAppRoot(remoteAuthority); + const execPath = await this._getExecPath(os, appRoot, remoteAuthority); + const tempDir = await this._getTempDir(remoteAuthority); + const srtPath = this._pathJoin(os, appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js'); + const sandboxConfigPath = tempDir ? await this._updateSandboxConfig(tempDir, configTarget, sandboxConfig) : undefined; + this._logService.debug(`McpSandboxService: Updated sandbox config path: ${sandboxConfigPath}`); + return { execPath, srtPath, sandboxConfigPath, tempDir }; + } + + private async _getExecPath(os: OperatingSystem, appRoot: string, remoteAuthority?: string): Promise { + if (remoteAuthority) { + return this._pathJoin(os, appRoot, 'node'); + } + return undefined; // Use Electron executable as the default exec path for local development, which will run the sandbox runtime wrapper with Electron in node mode. For remote, we need to specify the node executable to ensure it runs with Node.js. + } + + private _getSandboxEnvVariables(tempDir: URI | undefined, remoteAuthority?: string): Record | undefined { + let env: Record = {}; + if (tempDir) { + env = { TMPDIR: tempDir.path, SRT_DEBUG: 'true' }; + } + if (!remoteAuthority) { + // Add any remote-specific environment variables here + env = { ...env, ELECTRON_RUN_AS_NODE: '1' }; + } + // Ensure VSCODE_INSPECTOR_OPTIONS is not inherited by the sandboxed process, as it can cause issues with sandboxing. + env['VSCODE_INSPECTOR_OPTIONS'] = null; + return env; + } + + private _getSandboxCommandArgs(command: string, args: readonly string[], sandboxConfigPath: string | undefined): string[] { + const result: string[] = []; + if (sandboxConfigPath) { + result.push('--settings', sandboxConfigPath); + } + result.push(command, ...args); + return result; + } + + private async _getRemoteEnv(remoteAuthority?: string): Promise { + if (!remoteAuthority) { + return null; + } + return this._remoteEnvDetailsPromise; + } + + private async _getOperatingSystem(remoteAuthority?: string): Promise { + const remoteEnv = await this._getRemoteEnv(remoteAuthority); + if (remoteEnv) { + return remoteEnv.os; + } + return OS; + } + + private async _getAppRoot(remoteAuthority?: string): Promise { + const remoteEnv = await this._getRemoteEnv(remoteAuthority); + if (remoteEnv) { + return remoteEnv.appRoot.path; + } + return dirname(FileAccess.asFileUri('').path); + } + + private async _getTempDir(remoteAuthority?: string): Promise { + const remoteEnv = await this._getRemoteEnv(remoteAuthority); + if (remoteEnv) { + return remoteEnv.tmpDir; + } + const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI }; + const tempDir = environmentService.tmpDir; + if (!tempDir) { + this._logService.warn('McpSandboxService: Cannot create sandbox settings file because no tmpDir is available in this environment'); + } + return tempDir; + } + + private async _updateSandboxConfig(tempDir: URI, configTarget: ConfigurationTarget, sandboxConfig?: IMcpSandboxConfiguration): Promise { + const normalizedSandboxConfig = this._withDefaultSandboxConfig(sandboxConfig); + let configFileUri: URI; + const configTargetKey = ConfigurationTargetToString(configTarget); + if (this._sandboxConfigPerConfigurationTarget.has(configTargetKey)) { + configFileUri = URI.parse(this._sandboxConfigPerConfigurationTarget.get(configTargetKey)!); + } else { + configFileUri = URI.joinPath(tempDir, `vscode-${configTargetKey}-mcp-sandbox-settings-${this._sandboxSettingsId}.json`); + this._sandboxConfigPerConfigurationTarget.set(configTargetKey, configFileUri.toString()); + } + await this._fileService.createFile(configFileUri, VSBuffer.fromString(JSON.stringify(normalizedSandboxConfig, null, '\t')), { overwrite: true }); + return configFileUri.path; + } + + // this method merges the default allowWrite paths and allowedDomains with the ones provided in the sandbox config, to ensure that the default necessary paths and domains are always included in the sandbox config used for launching, + // even if they are not explicitly specified in the config provided by the user or the MCP server config. + private _withDefaultSandboxConfig(sandboxConfig?: IMcpSandboxConfiguration): IMcpSandboxConfiguration { + const mergedAllowWrite = new Set(sandboxConfig?.filesystem?.allowWrite ?? []); + for (const defaultAllowWrite of this._getDefaultAllowWrite()) { + if (defaultAllowWrite) { + mergedAllowWrite.add(defaultAllowWrite); + } + } + + const mergedAllowedDomains = new Set(sandboxConfig?.network?.allowedDomains ?? []); + for (const defaultAllowedDomain of this._defaultAllowedDomains) { + if (defaultAllowedDomain) { + mergedAllowedDomains.add(defaultAllowedDomain); + } + } + + return { + ...sandboxConfig, + network: { + allowedDomains: [...mergedAllowedDomains], + deniedDomains: sandboxConfig?.network?.deniedDomains ?? [], + }, + filesystem: { + allowWrite: [...mergedAllowWrite], + denyRead: sandboxConfig?.filesystem?.denyRead ?? [], + denyWrite: sandboxConfig?.filesystem?.denyWrite ?? [], + }, + }; + } + + private _getDefaultAllowWrite(): readonly string[] { + return [ + '~/.npm' + ]; + } + + private _pathJoin = (os: OperatingSystem, ...segments: string[]) => { + const path = os === OperatingSystem.Windows ? win32 : posix; + return path.join(...segments); + }; + +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 6452f5d62bd83..49743c1f10a06 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -297,7 +297,7 @@ export class McpServer extends Disposable implements IMcpServer { * Helper function to call the function on the handler once it's online. The * connection started if it is not already. */ - public static async callOn(server: IMcpServer, fn: (handler: McpServerRequestHandler) => Promise, token: CancellationToken = CancellationToken.None): Promise { + public static async callOn(server: IMcpServer, fn: (handler: McpServerRequestHandler, connection: IMcpServerConnection) => Promise, token: CancellationToken = CancellationToken.None): Promise { await server.start({ promptType: 'all-untrusted' }); // idempotent let ranOnce = false; @@ -326,7 +326,7 @@ export class McpServer extends Disposable implements IMcpServer { } } - resolve(fn(handler)); + resolve(fn(handler, connection)); ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync }); }); diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 7d35fa80ac796..5b26c5133c29a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -24,7 +24,7 @@ import { ExtensionIdentifier } from '../../../../platform/extensions/common/exte import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { McpGalleryManifestStatus } from '../../../../platform/mcp/common/mcpGalleryManifest.js'; import { IGalleryMcpServer, IInstallableMcpServer, IGalleryMcpServerConfiguration, IQueryOptions } from '../../../../platform/mcp/common/mcpManagement.js'; -import { IMcpDevModeConfig, IMcpServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IMcpDevModeConfig, IMcpSandboxConfiguration, IMcpServerConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolder, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchLocalMcpServer, IWorkbencMcpServerInstallOptions } from '../../../services/mcp/common/mcpWorkbenchManagementService.js'; @@ -132,6 +132,10 @@ export interface McpServerDefinition { readonly devMode?: IMcpDevModeConfig; /** Static description of server tools/data, used to hydrate the cache. */ readonly staticMetadata?: McpServerStaticMetadata; + /** Indicates if the sandbox is enabled for this server. */ + readonly sandboxEnabled?: boolean; + /** Sandbox configuration to apply for this server. */ + readonly sandbox?: IMcpSandboxConfiguration; readonly presentation?: { @@ -164,6 +168,8 @@ export namespace McpServerDefinition { readonly launch: McpServerLaunch.Serialized; readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; readonly staticMetadata?: McpServerStaticMetadata; + readonly sandboxEnabled?: boolean; + readonly sandbox?: IMcpSandboxConfiguration; } export function toSerialized(def: McpServerDefinition): McpServerDefinition.Serialized { @@ -177,6 +183,8 @@ export namespace McpServerDefinition { cacheNonce: def.cacheNonce, staticMetadata: def.staticMetadata, launch: McpServerLaunch.fromSerialized(def.launch), + sandboxEnabled: def.sandboxEnabled, + sandbox: def.sandboxEnabled ? def.sandbox : undefined, variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, }; } @@ -189,7 +197,9 @@ export namespace McpServerDefinition { && objectsEqual(a.launch, b.launch) && objectsEqual(a.presentation, b.presentation) && objectsEqual(a.variableReplacement, b.variableReplacement) - && objectsEqual(a.devMode, b.devMode); + && objectsEqual(a.devMode, b.devMode) + && a.sandboxEnabled === b.sandboxEnabled + && objectsEqual(a.sandbox, b.sandbox); } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index d0c7e9552351d..e48fa86019459 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -17,6 +17,8 @@ import { ServiceCollection } from '../../../../../platform/instantiation/common/ import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { ILogger, ILoggerService, ILogService, NullLogger, NullLogService } from '../../../../../platform/log/common/log.js'; import { mcpAccessConfig, McpAccessValue } from '../../../../../platform/mcp/common/mcpManagement.js'; +import { INotificationService } from '../../../../../platform/notification/common/notification.js'; +import { TestNotificationService } from '../../../../../platform/notification/test/common/testNotificationService.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'; @@ -28,6 +30,7 @@ import { IOutputService } from '../../../../services/output/common/output.js'; import { TestLoggerService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { McpRegistry } from '../../common/mcpRegistry.js'; import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { IMcpSandboxService } from '../../common/mcpSandboxService.js'; import { McpServerConnection } from '../../common/mcpServerConnection.js'; import { McpTaskManager } from '../../common/mcpTaskManager.js'; import { LazyCollectionState, McpCollectionDefinition, McpServerDefinition, McpServerLaunch, McpServerTransportStdio, McpServerTransportType, McpServerTrust, McpStartServerInteraction } from '../../common/mcpTypes.js'; @@ -135,6 +138,31 @@ class TestMcpRegistry extends McpRegistry { } } +class TestMcpSandboxService implements IMcpSandboxService { + declare readonly _serviceBrand: undefined; + public callCount = 0; + public enabled = false; + public lastLaunchCallArgs: { serverDef: McpServerDefinition; launch: McpServerLaunch; remoteAuthority: string | undefined; configTarget: ConfigurationTarget } | undefined; + + launchInSandboxIfEnabled(serverDef: McpServerDefinition, launch: McpServerLaunch, remoteAuthority: string | undefined, configTarget: ConfigurationTarget): Promise { + this.callCount++; + this.lastLaunchCallArgs = { serverDef, launch, remoteAuthority, configTarget }; + + if (this.enabled && launch.type === McpServerTransportType.Stdio) { + return Promise.resolve({ + ...launch, + command: 'sandboxed-command', + }); + } + + return Promise.resolve(launch); + } + + isEnabled(serverDef: McpServerDefinition): Promise { + return Promise.resolve(this.enabled); + } +} + suite('Workbench - MCP - Registry', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -148,6 +176,7 @@ suite('Workbench - MCP - Registry', () => { let logger: ILogger; let trustNonceBearer: { trustedAtNonce: string | undefined }; let taskManager: McpTaskManager; + let testMcpSandboxService: TestMcpSandboxService; setup(() => { testConfigResolverService = new TestConfigurationResolverService(); @@ -155,6 +184,7 @@ suite('Workbench - MCP - Registry', () => { testDialogService = new TestDialogService(); configurationService = new TestConfigurationService({ [mcpAccessConfig]: McpAccessValue.All }); trustNonceBearer = { trustedAtNonce: undefined }; + testMcpSandboxService = new TestMcpSandboxService(); const services = new ServiceCollection( [IConfigurationService, configurationService], @@ -163,8 +193,10 @@ suite('Workbench - MCP - Registry', () => { [ISecretStorageService, new TestSecretStorageService()], [ILoggerService, store.add(new TestLoggerService())], [ILogService, store.add(new NullLogService())], + [INotificationService, new TestNotificationService()], [IOutputService, upcast({ showChannel: () => { } })], [IDialogService, testDialogService], + [IMcpSandboxService, testMcpSandboxService], [IProductService, {}], ); @@ -337,6 +369,52 @@ suite('Workbench - MCP - Registry', () => { connection.dispose(); }); + test('resolveConnection calls launchInSandboxIfEnabled with expected arguments when sandboxing is enabled', async () => { + testMcpSandboxService.enabled = true; + + const sandboxCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable } = { + ...testCollection, + id: 'sandbox-collection', + remoteAuthority: 'ssh-remote+test', + }; + + const definition: McpServerDefinition = { + ...baseDefinition, + id: 'sandbox-server', + launch: { + type: McpServerTransportType.Stdio, + command: 'test-command', + args: ['--flag'], + env: {}, + envFile: undefined, + cwd: '/test', + }, + }; + + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + sandboxCollection.serverDefinitions.set([definition], undefined); + store.add(registry.registerCollection(sandboxCollection)); + + const connection = await registry.resolveConnection({ + collectionRef: sandboxCollection, + definitionRef: definition, + logger, + trustNonceBearer, + taskManager, + }) as McpServerConnection; + + assert.ok(connection); + assert.strictEqual(testMcpSandboxService.callCount, 1); + assert.strictEqual(testMcpSandboxService.lastLaunchCallArgs?.serverDef, definition); + assert.deepStrictEqual(testMcpSandboxService.lastLaunchCallArgs?.launch, definition.launch); + assert.strictEqual(testMcpSandboxService.lastLaunchCallArgs?.remoteAuthority, 'ssh-remote+test'); + assert.strictEqual(testMcpSandboxService.lastLaunchCallArgs?.configTarget, ConfigurationTarget.USER); + assert.strictEqual((connection.launchDefinition as McpServerTransportStdio).command, 'sandboxed-command'); + + connection.dispose(); + }); + suite('Lazy Collections', () => { let lazyCollection: McpCollectionDefinition; let normalCollection: McpCollectionDefinition; From 7e87802f490815eba130ae07b4918247a461c29e Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Sun, 22 Feb 2026 21:19:45 -0800 Subject: [PATCH 3/9] update distro (#296907) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1f42fcaa675b..14eb04131bde8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "9d472fb245bfdeb5eca66384d5bf6a9881fe4965", + "distro": "7314fffcb02a8ca77b33f974f69256250bed190e", "author": { "name": "Microsoft Corporation" }, From 000d29cc4e7c2465395b5b5b5aea153817456a10 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:33:29 -0800 Subject: [PATCH 4/9] Add helper class to work with js/ts unified configs For #292934 --- .../codeLens/implementationsCodeLens.ts | 40 +++--- .../codeLens/referencesCodeLens.ts | 31 ++--- .../src/languageProvider.ts | 20 +-- .../src/tsServer/bufferSyncSupport.ts | 20 +-- .../src/utils/configuration.ts | 126 +++++++++++++++++- 5 files changed, 174 insertions(+), 63 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts index d32cfa129f867..03a1716868abc 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -11,7 +11,7 @@ import type * as Proto from '../../tsServer/protocol/protocol'; import * as PConst from '../../tsServer/protocol/protocol.const'; import * as typeConverters from '../../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService'; -import { readUnifiedConfig, unifiedConfigSection } from '../../utils/configuration'; +import { ResourceUnifiedConfigValue } from '../../utils/configuration'; import { conditionalRegistration, requireHasModifiedUnifiedConfig, requireSomeCapability } from '../util/dependentRegistration'; import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider'; import { ExecutionTarget } from '../../tsServer/server'; @@ -23,31 +23,29 @@ const Config = Object.freeze({ }); export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider { + + private readonly _enabled: ResourceUnifiedConfigValue; + private readonly _showOnInterfaceMethods: ResourceUnifiedConfigValue; + private readonly _showOnAllClassMethods: ResourceUnifiedConfigValue; + public constructor( client: ITypeScriptServiceClient, protected _cachedResponse: CachedResponse, - private readonly language: LanguageDescription ) { super(client, _cachedResponse); - this._register( - vscode.workspace.onDidChangeConfiguration(evt => { - if ( - evt.affectsConfiguration(`${unifiedConfigSection}.${Config.enabled}`) || - evt.affectsConfiguration(`${language.id}.${Config.enabled}`) || - evt.affectsConfiguration(`${unifiedConfigSection}.${Config.showOnInterfaceMethods}`) || - evt.affectsConfiguration(`${language.id}.${Config.showOnInterfaceMethods}`) || - evt.affectsConfiguration(`${unifiedConfigSection}.${Config.showOnAllClassMethods}`) || - evt.affectsConfiguration(`${language.id}.${Config.showOnAllClassMethods}`) - ) { - this.changeEmitter.fire(); - } - }) - ); - } + this._enabled = this._register(new ResourceUnifiedConfigValue(Config.enabled, false)); + this._register(this._enabled.onDidChange(() => this.changeEmitter.fire())); + + this._showOnInterfaceMethods = this._register(new ResourceUnifiedConfigValue(Config.showOnInterfaceMethods, false)); + this._register(this._showOnInterfaceMethods.onDidChange(() => this.changeEmitter.fire())); + + this._showOnAllClassMethods = this._register(new ResourceUnifiedConfigValue(Config.showOnAllClassMethods, false)); + this._register(this._showOnAllClassMethods.onDidChange(() => this.changeEmitter.fire())); + } override async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { - const enabled = readUnifiedConfig(Config.enabled, false, { scope: document, fallbackSection: this.language.id }); + const enabled = this._enabled.getValue(document); if (!enabled) { return []; } @@ -131,7 +129,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip if ( item.kind === PConst.Kind.method && parent?.kind === PConst.Kind.interface && - readUnifiedConfig('implementationsCodeLens.showOnInterfaceMethods', false, { scope: document, fallbackSection: this.language.id }) + this._showOnInterfaceMethods.getValue(document) ) { return getSymbolRange(document, item); } @@ -141,7 +139,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip if ( item.kind === PConst.Kind.method && parent?.kind === PConst.Kind.class && - readUnifiedConfig('implementationsCodeLens.showOnAllClassMethods', false, { scope: document, fallbackSection: this.language.id }) + this._showOnAllClassMethods.getValue(document) ) { // But not private ones as these can never be overridden if (/\bprivate\b/.test(item.kindModifiers ?? '')) { @@ -165,6 +163,6 @@ export function register( requireSomeCapability(client, ClientCapability.Semantic), ], () => { return vscode.languages.registerCodeLensProvider(selector.semantic, - new TypeScriptImplementationsCodeLensProvider(client, cachedResponse, language)); + new TypeScriptImplementationsCodeLensProvider(client, cachedResponse)); }); } diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts index 0942f7f8f3ad7..3ea0850914a36 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts @@ -12,7 +12,7 @@ import * as PConst from '../../tsServer/protocol/protocol.const'; import { ExecutionTarget } from '../../tsServer/server'; import * as typeConverters from '../../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService'; -import { readUnifiedConfig, unifiedConfigSection } from '../../utils/configuration'; +import { ResourceUnifiedConfigValue } from '../../utils/configuration'; import { conditionalRegistration, requireHasModifiedUnifiedConfig, requireSomeCapability } from '../util/dependentRegistration'; import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider'; @@ -22,28 +22,25 @@ const Config = Object.freeze({ }); export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider { + + private readonly _enabled: ResourceUnifiedConfigValue; + private readonly _showOnAllFunctions: ResourceUnifiedConfigValue; + public constructor( client: ITypeScriptServiceClient, protected _cachedResponse: CachedResponse, - private readonly language: LanguageDescription ) { super(client, _cachedResponse); - this._register( - vscode.workspace.onDidChangeConfiguration(evt => { - if ( - evt.affectsConfiguration(`${unifiedConfigSection}.${Config.enabled}`) || - evt.affectsConfiguration(`${language.id}.${Config.enabled}`) || - evt.affectsConfiguration(`${unifiedConfigSection}.${Config.showOnAllFunctions}`) || - evt.affectsConfiguration(`${language.id}.${Config.showOnAllFunctions}`) - ) { - this.changeEmitter.fire(); - } - }) - ); + + this._enabled = this._register(new ResourceUnifiedConfigValue(Config.enabled, false)); + this._register(this._enabled.onDidChange(() => this.changeEmitter.fire())); + + this._showOnAllFunctions = this._register(new ResourceUnifiedConfigValue(Config.showOnAllFunctions, false)); + this._register(this._showOnAllFunctions.onDidChange(() => this.changeEmitter.fire())); } override async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { - const enabled = readUnifiedConfig(Config.enabled, false, { scope: document, fallbackSection: this.language.id }); + const enabled = this._enabled.getValue(document); if (!enabled) { return []; } @@ -95,7 +92,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens switch (item.kind) { case PConst.Kind.function: { - const showOnAllFunctions = readUnifiedConfig(Config.showOnAllFunctions, false, { scope: document, fallbackSection: this.language.id }); + const showOnAllFunctions = this._showOnAllFunctions.getValue(document); if (showOnAllFunctions && item.nameSpan) { return getSymbolRange(document, item); } @@ -160,6 +157,6 @@ export function register( requireSomeCapability(client, ClientCapability.Semantic), ], () => { return vscode.languages.registerCodeLensProvider(selector.semantic, - new TypeScriptReferencesCodeLensProvider(client, cachedResponse, language)); + new TypeScriptReferencesCodeLensProvider(client, cachedResponse)); }); } diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 230e79a6f7fb0..b70b21280d6bc 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -18,7 +18,7 @@ import { ClientCapability } from './typescriptService'; import TypeScriptServiceClient from './typescriptServiceClient'; import TypingsStatus from './ui/typingsStatus'; import { Disposable } from './utils/dispose'; -import { readUnifiedConfig } from './utils/configuration'; +import { UnifiedConfigValue } from './utils/configuration'; import { isWeb, isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; @@ -34,8 +34,16 @@ export default class LanguageProvider extends Disposable { private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void, ) { super(); - vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables); - this.configurationChanged(); + + const scope: vscode.ConfigurationScope = { languageId: this.description.languageIds[0] }; + + const validateConfig = this._register(new UnifiedConfigValue('validate.enabled', true, { scope, fallbackSection: this.id, fallbackSubSectionNameOverride: 'validate.enable' })); + this.updateValidate(validateConfig.getValue()); + this._register(validateConfig.onDidChange(value => this.updateValidate(value))); + + const suggestionsConfig = this._register(new UnifiedConfigValue('suggestionActions.enabled', true, { scope, fallbackSection: this.id })); + this.updateSuggestionDiagnostics(suggestionsConfig.getValue()); + this._register(suggestionsConfig.onDidChange(value => this.updateSuggestionDiagnostics(value))); client.onReady(() => this.registerProviders()); } @@ -91,12 +99,6 @@ export default class LanguageProvider extends Disposable { ]); } - private configurationChanged(): void { - const scope: vscode.ConfigurationScope = { languageId: this.description.languageIds[0] }; - this.updateValidate(readUnifiedConfig('validate.enabled', true, { scope, fallbackSection: this.id, fallbackSubSectionNameOverride: 'validate.enable' })); - this.updateSuggestionDiagnostics(readUnifiedConfig('suggestionActions.enabled', true, { scope, fallbackSection: this.id })); - } - public handlesUri(resource: vscode.Uri): boolean { const ext = extname(resource.path).slice(1).toLowerCase(); return this.description.standardFileExtensions.includes(ext) || this.handlesConfigFile(resource); diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 89ccb66646ba5..0531abc34d8e1 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -10,7 +10,7 @@ import * as typeConverters from '../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import { inMemoryResourcePrefix } from '../typescriptServiceClient'; import { coalesce } from '../utils/arrays'; -import { readUnifiedConfig, unifiedConfigSection } from '../utils/configuration'; +import { ResourceUnifiedConfigValue } from '../utils/configuration'; import { Delayer, setImmediate } from '../utils/async'; import { nulToken } from '../utils/cancellation'; import { Disposable } from '../utils/dispose'; @@ -471,6 +471,8 @@ export default class BufferSyncSupport extends Disposable { private listening: boolean = false; private readonly synchronizer: BufferSynchronizer; + private readonly _validate: ResourceUnifiedConfigValue; + private readonly _tabResources: TabResourceTracker; constructor( @@ -482,6 +484,8 @@ export default class BufferSyncSupport extends Disposable { this.client = client; this.modeIds = new Set(modeIds); + this._validate = this._register(new ResourceUnifiedConfigValue('validate.enabled', true, { fallbackSubSectionNameOverride: 'validate.enable' })); + this.diagnosticDelayer = new Delayer(300); const pathNormalizer = (path: vscode.Uri) => this.client.toTsFilePath(path); @@ -511,14 +515,7 @@ export default class BufferSyncSupport extends Disposable { } })); - this._register(vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration(`${unifiedConfigSection}.validate.enabled`) - || e.affectsConfiguration('typescript.validate.enable') - || e.affectsConfiguration('javascript.validate.enable') - ) { - this.requestAllDiagnostics(); - } - })); + this._register(this._validate.onDidChange(() => this.requestAllDiagnostics())); } private readonly _onDelete = this._register(new vscode.EventEmitter()); @@ -769,9 +766,6 @@ export default class BufferSyncSupport extends Disposable { return false; } - const fallbackSection = (buffer.languageId === languageModeIds.javascript || buffer.languageId === languageModeIds.javascriptreact) - ? 'javascript' - : 'typescript'; - return readUnifiedConfig('validate.enabled', true, { scope: buffer.document, fallbackSection, fallbackSubSectionNameOverride: 'validate.enable' }); + return this._validate.getValue(buffer.document); } } diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index 2b9bca95d6ad9..86d0c3d872214 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { Disposable } from './dispose'; export type UnifiedConfigurationScope = vscode.ConfigurationScope | null | undefined; export const unifiedConfigSection = 'js/ts'; -export type ReadUnifiedConfigOptions = { - readonly scope?: UnifiedConfigurationScope; +export interface ReadUnifiedConfigOptions { + readonly scope?: Scope; readonly fallbackSection: string; readonly fallbackSubSectionNameOverride?: string; -}; +} /** * Gets a configuration value, checking the unified `js/ts` setting first, @@ -75,3 +76,122 @@ export function hasModifiedUnifiedConfig( const languageConfig = vscode.workspace.getConfiguration(options.fallbackSection, options.scope); return hasModifiedValue(languageConfig.inspect(subSectionName)); } + +/** + * A cached, observable unified configuration value. + */ +export class UnifiedConfigValue extends Disposable { + + private _value: T; + + private readonly _onDidChange = this._register(new vscode.EventEmitter()); + public get onDidChange() { return this._onDidChange.event; } + + constructor( + private readonly subSectionName: string, + private readonly defaultValue: T, + private readonly options: ReadUnifiedConfigOptions<{ languageId: string }>, + ) { + super(); + + this._value = this.read(); + + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${unifiedConfigSection}.${subSectionName}`, options.scope ?? undefined) || + e.affectsConfiguration(`${options.fallbackSection}.${options.fallbackSubSectionNameOverride ?? subSectionName}`, options.scope ?? undefined) + ) { + const newValue = this.read(); + if (newValue !== this._value) { + this._value = newValue; + this._onDidChange.fire(newValue); + } + } + })); + } + + private read(): T { + return readUnifiedConfig(this.subSectionName, this.defaultValue, this.options); + } + + public getValue(): T { + return this._value; + } +} + +export interface ResourceUnifiedConfigScope { + readonly uri: vscode.Uri; + readonly languageId: string; +} + +/** + * A cached, observable unified configuration value that varies per workspace folder. + * + * Values are keyed by the workspace folder the resource belongs to, with a separate + * entry for resources outside any workspace folder. + */ +export class ResourceUnifiedConfigValue extends Disposable { + + private readonly _cache = new Map(); + + private readonly _onDidChange = this._register(new vscode.EventEmitter()); + public readonly onDidChange = this._onDidChange.event; + + constructor( + private readonly subSectionName: string, + private readonly defaultValue: T, + private readonly options?: { + readonly fallbackSubSectionNameOverride?: string; + }, + ) { + super(); + + const fallbackName = options?.fallbackSubSectionNameOverride ?? subSectionName; + + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${unifiedConfigSection}.${subSectionName}`) || + e.affectsConfiguration(`javascript.${fallbackName}`) || + e.affectsConfiguration(`typescript.${fallbackName}`) + ) { + this._cache.clear(); + this._onDidChange.fire(); + } + })); + + this._register(vscode.workspace.onDidChangeWorkspaceFolders(() => { + this._cache.clear(); + this._onDidChange.fire(); + })); + } + + public getValue(scope: ResourceUnifiedConfigScope): T { + const key = this.keyFor(scope); + const cached = this._cache.get(key); + if (cached !== undefined) { + return cached; + } + + const fallbackSection = this.fallbackSectionFor(scope.languageId); + const value = readUnifiedConfig(this.subSectionName, this.defaultValue, { + scope: { uri: scope.uri, languageId: scope.languageId }, + fallbackSection, + fallbackSubSectionNameOverride: this.options?.fallbackSubSectionNameOverride, + }); + this._cache.set(key, value); + return value; + } + + private fallbackSectionFor(languageId: string): string { + switch (languageId) { + case 'javascript': + case 'javascriptreact': + return 'javascript'; + default: + return 'typescript'; + } + } + + private keyFor(scope: ResourceUnifiedConfigScope): string { + const folder = vscode.workspace.getWorkspaceFolder(scope.uri); + return folder ? folder.uri.toString() : ''; + } +} From fb0cc9331659d2bce9b28636c605e8e9392f6968 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 23 Feb 2026 09:08:52 +0100 Subject: [PATCH 5/9] fix #296916 (#296919) --- .../contrib/chat/browser/newChatViewPane.ts | 2 +- .../browser/widget/input/chatInputPart.ts | 2 +- .../browser/widget/input/chatModelPicker.ts | 334 +++++++++--------- .../widget/input/modelPickerActionItem.ts | 6 +- .../widget/input/chatModelPicker.test.ts | 4 + 5 files changed, 173 insertions(+), 175 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 26de8dfa68797..40ae533a9268c 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -471,7 +471,7 @@ class NewChatWidget extends Disposable { this._focusEditor(); }, getModels: () => this._getAvailableModels(), - showCuratedModels: () => false, + canManageModels: () => false, }; const pickerOptions: IChatInputPickerOptions = { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 72c86bd684c90..7e52aae50849e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2180,7 +2180,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.renderAttachedContext(); }, getModels: () => this.getModels(), - showCuratedModels: () => { + canManageModels: () => { const sessionType = this.getCurrentSessionType(); return !sessionType || sessionType === localChatSessionType; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index a3ace94a9235a..ce813638d782f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -128,13 +128,11 @@ export function buildModelPickerItems( updateStateType: StateType, onSelect: (model: ILanguageModelChatMetadataAndIdentifier) => void, manageSettingsUrl: string | undefined, + canManageModels: boolean, commandService: ICommandService, chatEntitlementService: IChatEntitlementService, - showCuratedModels: boolean = true, ): IActionListItem[] { - const isPro = isProUser(chatEntitlementService.entitlement); const items: IActionListItem[] = []; - let otherModels: ILanguageModelChatMetadataAndIdentifier[] = []; if (models.length === 0) { items.push(createModelItem({ id: 'auto', @@ -145,186 +143,190 @@ export function buildModelPickerItems( label: localize('chat.modelPicker.auto', "Auto"), run: () => { } })); - } else { - if (!showCuratedModels) { - // Flat list: auto first, then all models sorted alphabetically - const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); - if (autoModel) { - items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel)); + } + + if (!canManageModels) { + // Flat list: auto first, then all models sorted alphabetically + const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); + if (autoModel) { + items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel)); + } + const sortedModels = models + .filter(m => m !== autoModel) + .sort((a, b) => { + const vendorCmp = a.metadata.vendor.localeCompare(b.metadata.vendor); + return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); + }); + for (const model of sortedModels) { + items.push(createModelItem(createModelAction(model, selectedModelId, onSelect), model)); + } + return items; + } + + const isPro = isProUser(chatEntitlementService.entitlement); + let otherModels: ILanguageModelChatMetadataAndIdentifier[] = []; + if (models.length) { + // Collect all available models into lookup maps + const allModelsMap = new Map(); + const modelsByMetadataId = new Map(); + for (const model of models) { + allModelsMap.set(model.identifier, model); + modelsByMetadataId.set(model.metadata.id, model); + } + + const placed = new Set(); + + const markPlaced = (identifierOrId: string, metadataId?: string) => { + placed.add(identifierOrId); + if (metadataId) { + placed.add(metadataId); } - const sortedModels = models - .filter(m => m !== autoModel) - .sort((a, b) => { - const vendorCmp = a.metadata.vendor.localeCompare(b.metadata.vendor); - return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); - }); - for (const model of sortedModels) { - items.push(createModelItem(createModelAction(model, selectedModelId, onSelect), model)); + }; + + const resolveModel = (id: string) => allModelsMap.get(id) ?? modelsByMetadataId.get(id); + + const getUnavailableReason = (entry: IModelControlEntry): 'upgrade' | 'update' | 'admin' => { + if (!isPro) { + return 'upgrade'; } - } else { - - // Collect all available models into lookup maps - const allModelsMap = new Map(); - const modelsByMetadataId = new Map(); - for (const model of models) { - allModelsMap.set(model.identifier, model); - modelsByMetadataId.set(model.metadata.id, model); + if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + return 'update'; } + return 'admin'; + }; - const placed = new Set(); + // --- 1. Auto --- + const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); + if (autoModel) { + markPlaced(autoModel.identifier, autoModel.metadata.id); + items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel)); + } - const markPlaced = (identifierOrId: string, metadataId?: string) => { - placed.add(identifierOrId); - if (metadataId) { - placed.add(metadataId); - } - }; + // --- 2. Promoted section (selected + recently used + featured) --- + type PromotedItem = + | { kind: 'available'; model: ILanguageModelChatMetadataAndIdentifier } + | { kind: 'unavailable'; id: string; entry: IModelControlEntry; reason: 'upgrade' | 'update' | 'admin' }; - const resolveModel = (id: string) => allModelsMap.get(id) ?? modelsByMetadataId.get(id); + const promotedItems: PromotedItem[] = []; - const getUnavailableReason = (entry: IModelControlEntry): 'upgrade' | 'update' | 'admin' => { - if (!isPro) { - return 'upgrade'; + // Try to place a model by id. Returns true if handled. + const tryPlaceModel = (id: string): boolean => { + if (placed.has(id)) { + return false; + } + const model = resolveModel(id); + if (model && !placed.has(model.identifier)) { + markPlaced(model.identifier, model.metadata.id); + const entry = controlModels[model.metadata.id]; + if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + promotedItems.push({ kind: 'unavailable', id: model.metadata.id, entry, reason: 'update' }); + } else { + promotedItems.push({ kind: 'available', model }); } - if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - return 'update'; + return true; + } + if (!model) { + const entry = controlModels[id]; + if (entry && !entry.exists) { + markPlaced(id); + promotedItems.push({ kind: 'unavailable', id, entry, reason: getUnavailableReason(entry) }); + return true; } - return 'admin'; - }; - - // --- 1. Auto --- - const autoModel = models.find(m => m.metadata.id === 'auto' && m.metadata.vendor === 'copilot'); - if (autoModel) { - markPlaced(autoModel.identifier, autoModel.metadata.id); - items.push(createModelItem(createModelAction(autoModel, selectedModelId, onSelect), autoModel)); } + return false; + }; - // --- 2. Promoted section (selected + recently used + featured) --- - type PromotedItem = - | { kind: 'available'; model: ILanguageModelChatMetadataAndIdentifier } - | { kind: 'unavailable'; id: string; entry: IModelControlEntry; reason: 'upgrade' | 'update' | 'admin' }; + // Selected model + if (selectedModelId && selectedModelId !== autoModel?.identifier) { + tryPlaceModel(selectedModelId); + } - const promotedItems: PromotedItem[] = []; + // Recently used models + for (const id of recentModelIds) { + tryPlaceModel(id); + } - // Try to place a model by id. Returns true if handled. - const tryPlaceModel = (id: string): boolean => { - if (placed.has(id)) { - return false; - } - const model = resolveModel(id); - if (model && !placed.has(model.identifier)) { - markPlaced(model.identifier, model.metadata.id); - const entry = controlModels[model.metadata.id]; - if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - promotedItems.push({ kind: 'unavailable', id: model.metadata.id, entry, reason: 'update' }); - } else { - promotedItems.push({ kind: 'available', model }); - } - return true; - } - if (!model) { - const entry = controlModels[id]; - if (entry && !entry.exists) { - markPlaced(id); - promotedItems.push({ kind: 'unavailable', id, entry, reason: getUnavailableReason(entry) }); - return true; - } + // Featured models from control manifest + for (const [entryId, entry] of Object.entries(controlModels)) { + if (!entry.featured || placed.has(entryId)) { + continue; + } + const model = resolveModel(entryId); + if (model && !placed.has(model.identifier)) { + markPlaced(model.identifier, model.metadata.id); + if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: 'update' }); + } else { + promotedItems.push({ kind: 'available', model }); } - return false; - }; - - // Selected model - if (selectedModelId && selectedModelId !== autoModel?.identifier) { - tryPlaceModel(selectedModelId); + } else if (!model && !entry.exists) { + markPlaced(entryId); + promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: getUnavailableReason(entry) }); } + } - // Recently used models - for (const id of recentModelIds) { - tryPlaceModel(id); - } + // Render promoted section: sorted alphabetically by name + let hasShownActionLink = false; + if (promotedItems.length > 0) { + promotedItems.sort((a, b) => { + const aName = a.kind === 'available' ? a.model.metadata.name : a.entry.label; + const bName = b.kind === 'available' ? b.model.metadata.name : b.entry.label; + return aName.localeCompare(bName); + }); - // Featured models from control manifest - for (const [entryId, entry] of Object.entries(controlModels)) { - if (!entry.featured || placed.has(entryId)) { - continue; - } - const model = resolveModel(entryId); - if (model && !placed.has(model.identifier)) { - markPlaced(model.identifier, model.metadata.id); - if (entry.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: 'update' }); - } else { - promotedItems.push({ kind: 'available', model }); + for (const item of promotedItems) { + if (item.kind === 'available') { + items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); + } else { + const showActionLink = item.reason === 'upgrade' ? !hasShownActionLink : true; + if (showActionLink && item.reason === 'upgrade') { + hasShownActionLink = true; } - } else if (!model && !entry.exists) { - markPlaced(entryId); - promotedItems.push({ kind: 'unavailable', id: entryId, entry, reason: getUnavailableReason(entry) }); + items.push(createUnavailableModelItem(item.id, item.entry, item.reason, manageSettingsUrl, updateStateType, undefined, showActionLink)); } } + } - // Render promoted section: sorted alphabetically by name - let hasShownActionLink = false; - if (promotedItems.length > 0) { - promotedItems.sort((a, b) => { - const aName = a.kind === 'available' ? a.model.metadata.name : a.entry.label; - const bName = b.kind === 'available' ? b.model.metadata.name : b.entry.label; - return aName.localeCompare(bName); - }); - - for (const item of promotedItems) { - if (item.kind === 'available') { - items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); - } else { - const showActionLink = item.reason === 'upgrade' ? !hasShownActionLink : true; - if (showActionLink && item.reason === 'upgrade') { - hasShownActionLink = true; - } - items.push(createUnavailableModelItem(item.id, item.entry, item.reason, manageSettingsUrl, updateStateType, undefined, showActionLink)); - } + // --- 3. Other Models (collapsible) --- + otherModels = models + .filter(m => !placed.has(m.identifier) && !placed.has(m.metadata.id)) + .sort((a, b) => { + const aCopilot = a.metadata.vendor === 'copilot' ? 0 : 1; + const bCopilot = b.metadata.vendor === 'copilot' ? 0 : 1; + if (aCopilot !== bCopilot) { + return aCopilot - bCopilot; } - } - - // --- 3. Other Models (collapsible) --- - otherModels = models - .filter(m => !placed.has(m.identifier) && !placed.has(m.metadata.id)) - .sort((a, b) => { - const aCopilot = a.metadata.vendor === 'copilot' ? 0 : 1; - const bCopilot = b.metadata.vendor === 'copilot' ? 0 : 1; - if (aCopilot !== bCopilot) { - return aCopilot - bCopilot; - } - const vendorCmp = a.metadata.vendor.localeCompare(b.metadata.vendor); - return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); - }); + const vendorCmp = a.metadata.vendor.localeCompare(b.metadata.vendor); + return vendorCmp !== 0 ? vendorCmp : a.metadata.name.localeCompare(b.metadata.name); + }); - if (otherModels.length > 0) { - if (items.length > 0) { - items.push({ kind: ActionListItemKind.Separator }); - } - items.push({ - item: { - id: 'otherModels', - enabled: true, - checked: false, - class: undefined, - tooltip: localize('chat.modelPicker.otherModels', "Other Models"), - label: localize('chat.modelPicker.otherModels', "Other Models"), - run: () => { /* toggle handled by isSectionToggle */ } - }, - kind: ActionListItemKind.Action, + if (otherModels.length > 0) { + if (items.length > 0) { + items.push({ kind: ActionListItemKind.Separator }); + } + items.push({ + item: { + id: 'otherModels', + enabled: true, + checked: false, + class: undefined, + tooltip: localize('chat.modelPicker.otherModels', "Other Models"), label: localize('chat.modelPicker.otherModels', "Other Models"), - group: { title: '', icon: Codicon.chevronDown }, - hideIcon: false, - section: ModelPickerSection.Other, - isSectionToggle: true, - }); - for (const model of otherModels) { - const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; - if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { - items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', manageSettingsUrl, updateStateType, ModelPickerSection.Other, true)); - } else { - items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model)); - } + run: () => { /* toggle handled by isSectionToggle */ } + }, + kind: ActionListItemKind.Action, + label: localize('chat.modelPicker.otherModels', "Other Models"), + group: { title: '', icon: Codicon.chevronDown }, + hideIcon: false, + section: ModelPickerSection.Other, + isSectionToggle: true, + }); + for (const model of otherModels) { + const entry = controlModels[model.metadata.id] ?? controlModels[model.identifier]; + if (entry?.minVSCodeVersion && !isVersionAtLeast(currentVSCodeVersion, entry.minVSCodeVersion)) { + items.push(createUnavailableModelItem(model.metadata.id, entry, 'update', manageSettingsUrl, updateStateType, ModelPickerSection.Other, true)); + } else { + items.push(createModelItem(createModelAction(model, selectedModelId, onSelect, ModelPickerSection.Other), model)); } } } @@ -522,32 +524,28 @@ export class ModelPickerWidget extends Disposable { }; const models = this._delegate.getModels(); - const showCuratedModels = this._delegate.showCuratedModels?.() ?? true; const isPro = isProUser(this._entitlementService.entitlement); - let controlModelsForTier: IStringDictionary = {}; - if (showCuratedModels) { - const manifest = this._languageModelsService.getModelsControlManifest(); - controlModelsForTier = isPro ? manifest.paid : manifest.free; - } + const manifest = this._languageModelsService.getModelsControlManifest(); + const controlModelsForTier = isPro ? manifest.paid : manifest.free; const items = buildModelPickerItems( models, this._selectedModel?.identifier, - showCuratedModels ? this._languageModelsService.getRecentlyUsedModelIds() : [], + this._languageModelsService.getRecentlyUsedModelIds(), controlModelsForTier, this._productService.version, this._updateService.state.type, onSelect, this._productService.defaultChatAgent?.manageSettingsUrl, + this._delegate.canManageModels(), this._commandService, this._entitlementService, - showCuratedModels ); const listOptions = { showFilter: models.length >= 10, filterPlaceholder: localize('chat.modelPicker.search', "Search models"), focusFilterOnOpen: true, - collapsedByDefault: showCuratedModels ? new Set([ModelPickerSection.Other]) : new Set(), + collapsedByDefault: new Set([ModelPickerSection.Other]), minWidth: 300, }; const previouslyFocusedElement = dom.getActiveElement(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 451bd29c4f92d..af61812b3a91f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -29,11 +29,7 @@ export interface IModelPickerDelegate { readonly currentModel: IObservable; setModel(model: ILanguageModelChatMetadataAndIdentifier): void; getModels(): ILanguageModelChatMetadataAndIdentifier[]; - /** - * Whether to show curated models from the control manifest (featured, unavailable, upgrade prompts, etc.). - * Defaults to `true`. - */ - showCuratedModels?(): boolean; + canManageModels(): boolean; } type ChatModelChangeClassification = { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index 3cadcb3723610..e39c5df086f98 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -94,6 +94,7 @@ function callBuild( opts.updateStateType ?? StateType.Idle, onSelect, opts.manageSettingsUrl, + true, stubCommandService, entitlementService, ); @@ -445,6 +446,7 @@ suite('buildModelPickerItems', () => { StateType.Idle, onSelect, undefined, + true, stubCommandService, stubChatEntitlementService, ); @@ -526,6 +528,7 @@ suite('buildModelPickerItems', () => { StateType.Idle, () => { }, 'https://aka.ms/github-copilot-settings', + true, stubCommandService, stubChatEntitlementService, ); @@ -606,6 +609,7 @@ suite('buildModelPickerItems', () => { StateType.Idle, onSelect, undefined, + true, stubCommandService, anonymousEntitlementService, ); From 782dcfe6ea423d27d67e0e870cadac7c6d866cda Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 23 Feb 2026 09:13:55 +0100 Subject: [PATCH 6/9] fix duplicate models in sessions window (#296922) --- src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 40ae533a9268c..521b2e0b00cec 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -517,6 +517,9 @@ class NewChatWidget extends Disposable { if (!model.metadata.isUserSelectable) { return false; } + if (model.metadata.targetChatSessionType === AgentSessionProviders.Background) { + return false; + } return true; } From ca5f6863ba9800b01f2c862b166ebde8d65b1658 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:50:07 +0000 Subject: [PATCH 7/9] Fix: Chat "Files & Folders" context picker returns no results when searching by file name (#296300) * Initial plan * Fix chat context Files & Folders search not returning results by name Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jrieken <1794099+jrieken@users.noreply.github.com> --- src/vs/workbench/contrib/search/browser/searchChatContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/search/browser/searchChatContext.ts b/src/vs/workbench/contrib/search/browser/searchChatContext.ts index 1a463744b4cd8..ca4bcf769dd8b 100644 --- a/src/vs/workbench/contrib/search/browser/searchChatContext.ts +++ b/src/vs/workbench/contrib/search/browser/searchChatContext.ts @@ -254,7 +254,7 @@ export async function searchFilesAndFolders( let searchResult: ISearchComplete | undefined; try { - searchResult = await searchService.fileSearch({ ...searchOptions, filePattern: `{**/${segmentMatchPattern}/**,${pattern}}` }, token); + searchResult = await searchService.fileSearch({ ...searchOptions, filePattern: `{**/${segmentMatchPattern}/**,**/${segmentMatchPattern}}` }, token); } catch (e) { if (!isCancellationError(e)) { throw e; From c173f3e216e3db13668f2c522d94be24e4b77466 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 23 Feb 2026 10:12:04 +0100 Subject: [PATCH 8/9] improve trusted JSON schemas (#296928) * improve trusted JSON schemas * update --- extensions/json-language-features/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 5a90b8b7e1b0d..d5877954cde4b 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -131,10 +131,12 @@ "type": "object", "default": { "https://schemastore.azurewebsites.net/": true, - "https://raw.githubusercontent.com/": true, + "https://raw.githubusercontent.com/microsoft/vscode/": true, + "https://raw.githubusercontent.com/devcontainers/spec/": true, "https://www.schemastore.org/": true, "https://json.schemastore.org/": true, - "https://json-schema.org/": true + "https://json-schema.org/": true, + "https://developer.microsoft.com/json-schemas/": true }, "additionalProperties": { "type": "boolean" From 7bcd7fe84dfc012bf1c972bc5294c1f23c16e3ad Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Feb 2026 10:12:15 +0100 Subject: [PATCH 9/9] chat - opt team into `inlineChat.affordance: editor` (#296931) --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 57fcbf6a6ef48..057ee21eba7a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { // --- Chat --- "inlineChat.enableV2": true, - "inlineChat.affordance": "gutter", + "inlineChat.affordance": "editor", "inlineChat.renderMode": "hover", "chat.tools.terminal.autoApprove": { "scripts/test.bat": true,