From a98ee7a6239b20cc1c74b27d5d620081e6c9e83e Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 19 Feb 2026 15:36:45 -0800 Subject: [PATCH 01/28] terminal: support tab title templates for extension terminals --- .../common/extensionsApiProposals.ts | 3 ++ src/vs/platform/terminal/common/terminal.ts | 10 +++++ .../api/browser/mainThreadTerminalService.ts | 6 ++- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostTerminalService.ts | 19 +++++++--- .../terminal/browser/terminalInstance.ts | 15 ++++++-- .../browser/terminalProfileQuickpick.ts | 1 + .../browser/terminalProfileService.ts | 6 ++- .../terminal/browser/terminalService.ts | 4 +- .../contrib/terminal/common/terminal.ts | 6 ++- .../test/browser/terminalInstance.test.ts | 6 +++ .../vscode.proposed.terminalTitle.d.ts | 37 +++++++++++++++++++ 12 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.terminalTitle.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index c4cc744d7e430..1cbbd458e6d80 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -449,6 +449,9 @@ const _allApiProposals = { terminalShellEnv: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts', }, + terminalTitle: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalTitle.d.ts', + }, testObserver: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', }, diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 18c7915e8f24a..e061c47650b5d 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -671,6 +671,13 @@ export interface IShellLaunchConfig { * This allows extensions to control shell integration for terminals they create. */ shellIntegrationNonce?: string; + + /** + * A title template string that supports the same variables as the + * `terminal.integrated.tabs.title` setting. When set, this overrides the config-based + * title template for this terminal instance. + */ + title?: string; } export interface ITerminalTabAction { @@ -686,6 +693,7 @@ export interface ICreateContributedTerminalProfileOptions { color?: string; location?: TerminalLocation | { viewColumn: number; preserveState?: boolean } | { splitActiveTerminal: boolean }; cwd?: string | URI; + title?: string; } export enum TerminalLocation { @@ -713,6 +721,7 @@ export interface IShellLaunchConfigDto { isFeatureTerminal?: boolean; tabActions?: ITerminalTabAction[]; shellIntegrationEnvironmentReporting?: boolean; + title?: string; } /** @@ -962,6 +971,7 @@ export interface ITerminalProfileContribution { id: string; icon?: URI | { light: URI; dark: URI } | string; color?: string; + tabTitle?: string; } export interface IExtensionTerminalProfile extends ITerminalProfileContribution { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 8e85ad2cac5fa..a83feafd3ea65 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -173,7 +173,8 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, useShellEnvironment: launchConfig.useShellEnvironment, isTransient: launchConfig.isTransient, - shellIntegrationNonce: launchConfig.shellIntegrationNonce + shellIntegrationNonce: launchConfig.shellIntegrationNonce, + title: launchConfig.title, }; const terminal = Promises.withAsyncBody(async r => { const terminal = await this._terminalService.createTerminal({ @@ -415,7 +416,8 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT cwd: terminalInstance.shellLaunchConfig.cwd, env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser, - tabActions: terminalInstance.shellLaunchConfig.tabActions + tabActions: terminalInstance.shellLaunchConfig.tabActions, + title: terminalInstance.shellLaunchConfig.title }; this._proxy.$acceptTerminalOpened(terminalInstance.instanceId, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d10a973d8bc00..7e5b2e972b842 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -625,6 +625,7 @@ export interface TerminalLaunchConfig { location?: TerminalLocation | { viewColumn: number; preserveFocus?: boolean } | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean }; isTransient?: boolean; shellIntegrationNonce?: string; + title?: string; } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 225ed9c223111..f0469e819296e 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -193,11 +193,12 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(options.location, internalOptions?.resolvedExtHostIdentifier), isTransient: options.isTransient ?? undefined, shellIntegrationNonce: options.shellIntegrationNonce ?? undefined, + title: options.title ?? undefined, }); } - public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string): Promise { + public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string, title?: string): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } @@ -209,6 +210,7 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(location, parentTerminal), isTransient: true, shellIntegrationNonce: shellIntegrationNonce ?? undefined, + title: title ?? undefined, }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { @@ -513,7 +515,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce).then(id => { + terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.title).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -634,7 +636,8 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I shellArgs: shellLaunchConfigDto.args, cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), env: shellLaunchConfigDto.env, - hideFromUser: shellLaunchConfigDto.hideFromUser + hideFromUser: shellLaunchConfigDto.hideFromUser, + title: shellLaunchConfigDto.title }; const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); @@ -854,11 +857,15 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`No terminal profile options provided for id "${id}"`); } - if (hasKey(profile.options, { pty: true })) { - this.createExtensionTerminal(profile.options, options); + const profileOptions = options.title && !profile.options.title + ? { ...profile.options, title: options.title } + : profile.options; + + if (hasKey(profileOptions, { pty: true })) { + this.createExtensionTerminal(profileOptions, options); return; } - this.createTerminalFromOptions(profile.options, options); + this.createTerminalFromOptions(profileOptions, options); } public registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index a9fd028211a58..9e405b875d188 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -518,7 +518,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. - if (this.shellLaunchConfig.customPtyImplementation) { + if (this.shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.title) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } @@ -1471,7 +1471,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } })); } - if (this._shellLaunchConfig.name) { + if (this._shellLaunchConfig.name && !this._shellLaunchConfig.title) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Listen to xterm.js' sequence title change event, trigger this async to ensure @@ -1483,7 +1483,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } }); }); - this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); + // When a title template is provided, use the name as the initial process name + // so it can be referenced via ${process} in the template + if (this._shellLaunchConfig.title && this._shellLaunchConfig.name) { + this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Process); + } else { + this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); + } } })); this._register(processManager.onProcessExit(exitCode => this._onProcessExit(exitCode))); @@ -2633,7 +2639,8 @@ export class TerminalLabelComputer extends Disposable { } refreshLabel(instance: Pick, reset?: boolean): void { - this._title = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.title, TerminalLabelType.Title, reset); + const titleTemplate = instance.shellLaunchConfig.title ?? this._terminalConfigurationService.config.tabs.title; + this._title = this.computeLabel(instance, titleTemplate, TerminalLabelType.Title, reset); this._description = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.description, TerminalLabelType.Description); if (this._title !== instance.title || this._description !== instance.description || reset) { this._onDidChangeLabel.fire({ title: this._title, description: this._description }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index 1f06f04d3f0f4..6c7ab9e8457dc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -82,6 +82,7 @@ export class TerminalProfileQuickpick { extensionIdentifier: result.profile.extensionIdentifier, id: result.profile.id, title: result.profile.title, + tabTitle: result.profile.tabTitle, options: { icon: result.profile.icon, color: result.profile.color, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 0cbef05b9221b..7415ad5794f2d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -233,7 +233,8 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi icon: args.options.icon, id: args.id, title: args.title, - color: args.options.color + color: args.options.color, + tabTitle: args.tabTitle }; (profilesConfig as { [key: string]: ITerminalProfileObject })[args.title] = newProfile; @@ -271,5 +272,6 @@ function contributedProfilesEqual(one: IExtensionTerminalProfile, other: IExtens one.color === other.color && one.icon === other.icon && one.id === other.id && - one.title === other.title; + one.title === other.title && + one.tabTitle === other.tabTitle; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index a01da1a710cb6..ddfae73b74323 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -256,7 +256,8 @@ export class TerminalService extends Disposable implements ITerminalService { await this.createContributedTerminalProfile(result.config.extensionIdentifier, result.config.id, { icon: result.config.options?.icon, color: result.config.options?.color, - location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation + location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation, + title: result.config.tabTitle, }); return; } else if (result.config && hasKey(result.config, { profileName: true })) { @@ -1032,6 +1033,7 @@ export class TerminalService extends Disposable implements ITerminalService { color: contributedProfile.color, location, cwd: shellLaunchConfig.cwd, + title: contributedProfile.tabTitle, }); const instanceHost = resolvedLocation === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; // TODO@meganrogge: This returns undefined in the remote & web smoke tests but the function diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 12fa676c314ed..f442f17698def 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -67,7 +67,7 @@ export interface ITerminalProfileResolverService { export const ShellIntegrationExitCode = 633; export interface IRegisterContributedProfileArgs { - extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; + extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; tabTitle?: string; } export const ITerminalProfileService = createDecorator('terminalProfileService'); @@ -693,6 +693,10 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor { strictEqual(terminalLabelComputer.title, 'my-title'); strictEqual(terminalLabelComputer.description, 'folder'); }); + test('should use shellLaunchConfig.title as template when set', () => { + const terminalLabelComputer = createLabelComputer({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${cwd}' } } } }); + terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'my-sequence', processName: 'zsh', shellLaunchConfig: { title: '${sequence}' } })); + strictEqual(terminalLabelComputer.title, 'my-sequence'); + strictEqual(terminalLabelComputer.description, 'cwd'); + }); test('should provide cwdFolder for all cwds only when in multi-root', () => { const terminalLabelComputer = createLabelComputer({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } } } }); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: ROOT_1 }) } as IWorkspaceFolder, cwd: ROOT_1 })); diff --git a/src/vscode-dts/vscode.proposed.terminalTitle.d.ts b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts new file mode 100644 index 0000000000000..584b2d29cf4d7 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/xyz + + export interface TerminalOptions { + /** + * A title template string for the terminal tab. This supports the same variables as the + * `terminal.integrated.tabs.title` setting, such as `${sequence}`, `${process}`, `${cwd}`, + * `${cwdFolder}`, `${workspaceFolderName}`, etc. When set, this overrides the default title + * behavior (which uses the `name` as a static title) and instead uses the template for + * dynamic title resolution. + * + * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * title to be used as the tab title. + */ + title?: string; + } + + export interface ExtensionTerminalOptions { + /** + * A title template string for the terminal tab. This supports the same variables as the + * `terminal.integrated.tabs.title` setting, such as `${sequence}`, `${process}`, `${cwd}`, + * `${cwdFolder}`, `${workspaceFolderName}`, etc. When set, this overrides the default title + * behavior (which uses the `name` as a static title) and instead uses the template for + * dynamic title resolution. + * + * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * title to be used as the tab title. + */ + title?: string; + } +} From 1b8971bb128b2c83c5ee155d8c42b00b7742407d Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 19 Feb 2026 15:48:44 -0800 Subject: [PATCH 02/28] terminal: omit undefined tabTitle from profile quick pick --- .../browser/terminalProfileQuickpick.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index 6c7ab9e8457dc..569f84b4357ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -77,17 +77,29 @@ export class TerminalProfileQuickpick { await this._configurationService.updateValue(defaultProfileKey, result.profileName, ConfigurationTarget.USER); } else if (type === 'createInstance') { if (hasKey(result.profile, { id: true })) { + const config: { + extensionIdentifier: string; + id: string; + title: string; + tabTitle?: string; + options: { + icon: IExtensionTerminalProfile['icon']; + color: IExtensionTerminalProfile['color']; + }; + } = { + extensionIdentifier: result.profile.extensionIdentifier, + id: result.profile.id, + title: result.profile.title, + options: { + icon: result.profile.icon, + color: result.profile.color, + } + }; + if (result.profile.tabTitle !== undefined) { + config.tabTitle = result.profile.tabTitle; + } return { - config: { - extensionIdentifier: result.profile.extensionIdentifier, - id: result.profile.id, - title: result.profile.title, - tabTitle: result.profile.tabTitle, - options: { - icon: result.profile.icon, - color: result.profile.color, - } - }, + config, keyMods: result.keyMods }; } else { @@ -178,7 +190,8 @@ export class TerminalProfileQuickpick { title: contributed.title, icon: contributed.icon, id: contributed.id, - color: contributed.color + color: contributed.color, + tabTitle: contributed.tabTitle }, profileName: contributed.title, iconClasses From 3b7ab1fb2cbcb4d27740150e8c452f8488ef2c2b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Feb 2026 16:17:25 -0800 Subject: [PATCH 03/28] wip --- MCP_IN_PLUGINS.md | 538 ++++++++++++++++++ PLAN.md | 196 +++++++ PLUGINS.md | 481 ++++++++++++++++ PLUGINS_LOADING.md | 388 +++++++++++++ .../contrib/chat/browser/chat.contribution.ts | 28 + .../input/editor/chatInputCompletions.ts | 14 +- .../contrib/chat/common/constants.ts | 3 + .../chat/common/plugins/agentPluginService.ts | 88 +++ .../common/plugins/agentPluginServiceImpl.ts | 474 +++++++++++++++ .../promptSyntax/service/promptsService.ts | 8 + .../service/promptsServiceImpl.ts | 95 ++-- .../contrib/mcp/browser/mcp.contribution.ts | 2 + .../common/discovery/pluginMcpDiscovery.ts | 117 ++++ 13 files changed, 2386 insertions(+), 46 deletions(-) create mode 100644 MCP_IN_PLUGINS.md create mode 100644 PLAN.md create mode 100644 PLUGINS.md create mode 100644 PLUGINS_LOADING.md create mode 100644 src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts create mode 100644 src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts diff --git a/MCP_IN_PLUGINS.md b/MCP_IN_PLUGINS.md new file mode 100644 index 0000000000000..164ba5c81a48c --- /dev/null +++ b/MCP_IN_PLUGINS.md @@ -0,0 +1,538 @@ +--- +name: MCP Integration +description: This skill should be used when the user asks to "add MCP server", "integrate MCP", "configure MCP in plugin", "use .mcp.json", "set up Model Context Protocol", "connect external service", mentions "${CLAUDE_PLUGIN_ROOT} with MCP", or discusses MCP server types (SSE, stdio, HTTP, WebSocket). Provides comprehensive guidance for integrating Model Context Protocol servers into Claude Code plugins for external tool and service integration. +version: 0.1.0 +--- + +# MCP Integration for Claude Code Plugins + +## Overview + +Model Context Protocol (MCP) enables Claude Code plugins to integrate with external services and APIs by providing structured tool access. Use MCP integration to expose external service capabilities as tools within Claude Code. + +**Key capabilities:** +- Connect to external services (databases, APIs, file systems) +- Provide 10+ related tools from a single service +- Handle OAuth and complex authentication flows +- Bundle MCP servers with plugins for automatic setup + +## MCP Server Configuration Methods + +Plugins can bundle MCP servers in two ways: + +### Method 1: Dedicated .mcp.json (Recommended) + +Create `.mcp.json` at plugin root: + +```json +{ + "database-tools": { + "command": "${CLAUDE_PLUGIN_ROOT}/servers/db-server", + "args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"], + "env": { + "DB_URL": "${DB_URL}" + } + } +} +``` + +**Benefits:** +- Clear separation of concerns +- Easier to maintain +- Better for multiple servers + +### Method 2: Inline in plugin.json + +Add `mcpServers` field to plugin.json: + +```json +{ + "name": "my-plugin", + "version": "1.0.0", + "mcpServers": { + "plugin-api": { + "command": "${CLAUDE_PLUGIN_ROOT}/servers/api-server", + "args": ["--port", "8080"] + } + } +} +``` + +**Benefits:** +- Single configuration file +- Good for simple single-server plugins + +## MCP Server Types + +### stdio (Local Process) + +Execute local MCP servers as child processes. Best for local tools and custom servers. + +**Configuration:** +```json +{ + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"], + "env": { + "LOG_LEVEL": "debug" + } + } +} +``` + +**Use cases:** +- File system access +- Local database connections +- Custom MCP servers +- NPM-packaged MCP servers + +**Process management:** +- Claude Code spawns and manages the process +- Communicates via stdin/stdout +- Terminates when Claude Code exits + +### SSE (Server-Sent Events) + +Connect to hosted MCP servers with OAuth support. Best for cloud services. + +**Configuration:** +```json +{ + "asana": { + "type": "sse", + "url": "https://mcp.asana.com/sse" + } +} +``` + +**Use cases:** +- Official hosted MCP servers (Asana, GitHub, etc.) +- Cloud services with MCP endpoints +- OAuth-based authentication +- No local installation needed + +**Authentication:** +- OAuth flows handled automatically +- User prompted on first use +- Tokens managed by Claude Code + +### HTTP (REST API) + +Connect to RESTful MCP servers with token authentication. + +**Configuration:** +```json +{ + "api-service": { + "type": "http", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer ${API_TOKEN}", + "X-Custom-Header": "value" + } + } +} +``` + +**Use cases:** +- REST API-based MCP servers +- Token-based authentication +- Custom API backends +- Stateless interactions + +### WebSocket (Real-time) + +Connect to WebSocket MCP servers for real-time bidirectional communication. + +**Configuration:** +```json +{ + "realtime-service": { + "type": "ws", + "url": "wss://mcp.example.com/ws", + "headers": { + "Authorization": "Bearer ${TOKEN}" + } + } +} +``` + +**Use cases:** +- Real-time data streaming +- Persistent connections +- Push notifications from server +- Low-latency requirements + +## Environment Variable Expansion + +All MCP configurations support environment variable substitution: + +**${CLAUDE_PLUGIN_ROOT}** - Plugin directory (always use for portability): +```json +{ + "command": "${CLAUDE_PLUGIN_ROOT}/servers/my-server" +} +``` + +**User environment variables** - From user's shell: +```json +{ + "env": { + "API_KEY": "${MY_API_KEY}", + "DATABASE_URL": "${DB_URL}" + } +} +``` + +**Best practice:** Document all required environment variables in plugin README. + +## MCP Tool Naming + +When MCP servers provide tools, they're automatically prefixed: + +**Format:** `mcp__plugin____` + +**Example:** +- Plugin: `asana` +- Server: `asana` +- Tool: `create_task` +- **Full name:** `mcp__plugin_asana_asana__asana_create_task` + +### Using MCP Tools in Commands + +Pre-allow specific MCP tools in command frontmatter: + +```markdown +--- +allowed-tools: [ + "mcp__plugin_asana_asana__asana_create_task", + "mcp__plugin_asana_asana__asana_search_tasks" +] +--- +``` + +**Wildcard (use sparingly):** +```markdown +--- +allowed-tools: ["mcp__plugin_asana_asana__*"] +--- +``` + +**Best practice:** Pre-allow specific tools, not wildcards, for security. + +## Lifecycle Management + +**Automatic startup:** +- MCP servers start when plugin enables +- Connection established before first tool use +- Restart required for configuration changes + +**Lifecycle:** +1. Plugin loads +2. MCP configuration parsed +3. Server process started (stdio) or connection established (SSE/HTTP/WS) +4. Tools discovered and registered +5. Tools available as `mcp__plugin_...__...` + +**Viewing servers:** +Use `/mcp` command to see all servers including plugin-provided ones. + +## Authentication Patterns + +### OAuth (SSE/HTTP) + +OAuth handled automatically by Claude Code: + +```json +{ + "type": "sse", + "url": "https://mcp.example.com/sse" +} +``` + +User authenticates in browser on first use. No additional configuration needed. + +### Token-Based (Headers) + +Static or environment variable tokens: + +```json +{ + "type": "http", + "url": "https://api.example.com", + "headers": { + "Authorization": "Bearer ${API_TOKEN}" + } +} +``` + +Document required environment variables in README. + +### Environment Variables (stdio) + +Pass configuration to MCP server: + +```json +{ + "command": "python", + "args": ["-m", "my_mcp_server"], + "env": { + "DATABASE_URL": "${DB_URL}", + "API_KEY": "${API_KEY}", + "LOG_LEVEL": "info" + } +} +``` + +## Integration Patterns + +### Pattern 1: Simple Tool Wrapper + +Commands use MCP tools with user interaction: + +```markdown +# Command: create-item.md +--- +allowed-tools: ["mcp__plugin_name_server__create_item"] +--- + +Steps: +1. Gather item details from user +2. Use mcp__plugin_name_server__create_item +3. Confirm creation +``` + +**Use for:** Adding validation or preprocessing before MCP calls. + +### Pattern 2: Autonomous Agent + +Agents use MCP tools autonomously: + +```markdown +# Agent: data-analyzer.md + +Analysis Process: +1. Query data via mcp__plugin_db_server__query +2. Process and analyze results +3. Generate insights report +``` + +**Use for:** Multi-step MCP workflows without user interaction. + +### Pattern 3: Multi-Server Plugin + +Integrate multiple MCP servers: + +```json +{ + "github": { + "type": "sse", + "url": "https://mcp.github.com/sse" + }, + "jira": { + "type": "sse", + "url": "https://mcp.jira.com/sse" + } +} +``` + +**Use for:** Workflows spanning multiple services. + +## Security Best Practices + +### Use HTTPS/WSS + +Always use secure connections: + +```json +✅ "url": "https://mcp.example.com/sse" +❌ "url": "http://mcp.example.com/sse" +``` + +### Token Management + +**DO:** +- ✅ Use environment variables for tokens +- ✅ Document required env vars in README +- ✅ Let OAuth flow handle authentication + +**DON'T:** +- ❌ Hardcode tokens in configuration +- ❌ Commit tokens to git +- ❌ Share tokens in documentation + +### Permission Scoping + +Pre-allow only necessary MCP tools: + +```markdown +✅ allowed-tools: [ + "mcp__plugin_api_server__read_data", + "mcp__plugin_api_server__create_item" +] + +❌ allowed-tools: ["mcp__plugin_api_server__*"] +``` + +## Error Handling + +### Connection Failures + +Handle MCP server unavailability: +- Provide fallback behavior in commands +- Inform user of connection issues +- Check server URL and configuration + +### Tool Call Errors + +Handle failed MCP operations: +- Validate inputs before calling MCP tools +- Provide clear error messages +- Check rate limiting and quotas + +### Configuration Errors + +Validate MCP configuration: +- Test server connectivity during development +- Validate JSON syntax +- Check required environment variables + +## Performance Considerations + +### Lazy Loading + +MCP servers connect on-demand: +- Not all servers connect at startup +- First tool use triggers connection +- Connection pooling managed automatically + +### Batching + +Batch similar requests when possible: + +``` +# Good: Single query with filters +tasks = search_tasks(project="X", assignee="me", limit=50) + +# Avoid: Many individual queries +for id in task_ids: + task = get_task(id) +``` + +## Testing MCP Integration + +### Local Testing + +1. Configure MCP server in `.mcp.json` +2. Install plugin locally (`.claude-plugin/`) +3. Run `/mcp` to verify server appears +4. Test tool calls in commands +5. Check `claude --debug` logs for connection issues + +### Validation Checklist + +- [ ] MCP configuration is valid JSON +- [ ] Server URL is correct and accessible +- [ ] Required environment variables documented +- [ ] Tools appear in `/mcp` output +- [ ] Authentication works (OAuth or tokens) +- [ ] Tool calls succeed from commands +- [ ] Error cases handled gracefully + +## Debugging + +### Enable Debug Logging + +```bash +claude --debug +``` + +Look for: +- MCP server connection attempts +- Tool discovery logs +- Authentication flows +- Tool call errors + +### Common Issues + +**Server not connecting:** +- Check URL is correct +- Verify server is running (stdio) +- Check network connectivity +- Review authentication configuration + +**Tools not available:** +- Verify server connected successfully +- Check tool names match exactly +- Run `/mcp` to see available tools +- Restart Claude Code after config changes + +**Authentication failing:** +- Clear cached auth tokens +- Re-authenticate +- Check token scopes and permissions +- Verify environment variables set + +## Quick Reference + +### MCP Server Types + +| Type | Transport | Best For | Auth | +|------|-----------|----------|------| +| stdio | Process | Local tools, custom servers | Env vars | +| SSE | HTTP | Hosted services, cloud APIs | OAuth | +| HTTP | REST | API backends, token auth | Tokens | +| ws | WebSocket | Real-time, streaming | Tokens | + +### Configuration Checklist + +- [ ] Server type specified (stdio/SSE/HTTP/ws) +- [ ] Type-specific fields complete (command or url) +- [ ] Authentication configured +- [ ] Environment variables documented +- [ ] HTTPS/WSS used (not HTTP/WS) +- [ ] ${CLAUDE_PLUGIN_ROOT} used for paths + +### Best Practices + +**DO:** +- ✅ Use ${CLAUDE_PLUGIN_ROOT} for portable paths +- ✅ Document required environment variables +- ✅ Use secure connections (HTTPS/WSS) +- ✅ Pre-allow specific MCP tools in commands +- ✅ Test MCP integration before publishing +- ✅ Handle connection and tool errors gracefully + +**DON'T:** +- ❌ Hardcode absolute paths +- ❌ Commit credentials to git +- ❌ Use HTTP instead of HTTPS +- ❌ Pre-allow all tools with wildcards +- ❌ Skip error handling +- ❌ Forget to document setup + +## Additional Resources + +### Reference Files + +For detailed information, consult: + +- **`references/server-types.md`** - Deep dive on each server type +- **`references/authentication.md`** - Authentication patterns and OAuth +- **`references/tool-usage.md`** - Using MCP tools in commands and agents + +### Example Configurations + +Working examples in `examples/`: + +- **`stdio-server.json`** - Local stdio MCP server +- **`sse-server.json`** - Hosted SSE server with OAuth +- **`http-server.json`** - REST API with token auth + +### External Resources + +- **Official MCP Docs**: https://modelcontextprotocol.io/ +- **Claude Code MCP Docs**: https://docs.claude.com/en/docs/claude-code/mcp +- **MCP SDK**: @modelcontextprotocol/sdk +- **Testing**: Use `claude --debug` and `/mcp` command diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000000000..56b5ad83b7551 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,196 @@ +# Agent Plugin Implementation Plan (Handoff) + +## Objective +Implement dual support for Copilot-style and Claude-style agent plugins in VS Code chat, with modular discovery and a unified internal plugin service. + +This document summarizes: +- what is already implemented, +- what design decisions were made, +- what remains to be built, +- and a concrete continuation plan for another agent. + +--- + +## Current Status (Implemented) + +### 1) Core service contracts scaffolded +**File:** `src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts` + +Implemented: +- `IAgentPluginService` via `createDecorator('agentPluginService')` +- `IAgentPlugin` with: + - `uri: URI` + - `hooks: IObservable` +- `IAgentPluginHook` with: + - `event: string` + - `command: string` +- `IAgentPluginDiscovery` interface with: + - `plugins: IObservable` + - `start(): void` +- `agentPluginDiscoveryRegistry` (MCP-inspired descriptor registry) + +Notable refactor already completed: +- Removed `source`/`mode` from `IAgentPlugin` as redundant. +- Service-level source enablement logic was removed from `AgentPluginService` and moved into discovery implementations. + +--- + +### 2) Service implementation scaffolded and discovery modularized +**File:** `src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts` + +Implemented: +- `AgentPluginService` that is source-agnostic: + - instantiates all registered discoveries + - starts each discovery + - aggregates discovery observables + - dedupes by plugin URI + - deterministic sort by URI +- `WorkspaceAgentPluginDiscovery` base class: + - internal per-discovery enablement handling via config observable + - workspace folder-based candidate directory scan + - manual plugin path support (see config below) + - per-discovery `isPluginRoot(uri)` type check + - `toPlugin(uri)` currently returns plugin shell with empty hooks observable + +Implemented discovery types: +- `CopilotAgentPluginDiscovery` + - search paths: `.copilot/plugins`, `.vscode/plugins` + - root detection: `plugin.json` + - enablement key: `chat.plugins.copilot.enabled` (default true) +- `ClaudeAgentPluginDiscovery` + - search paths: `.claude/plugins`, `.vscode/plugins` + - root detection: `.claude-plugin/plugin.json` OR any of: + - `agents/`, `skills/`, `commands/`, `hooks.json`, `.mcp.json`, `.lsp.json` + - enablement key: `chat.plugins.claude.enabled` (default false) + +Manual path behavior implemented: +- `chat.plugins.paths` is read by base discovery class. +- Every path is treated as candidate plugin root if it resolves to a directory. +- Plugin type is inferred by each discovery via `isPluginRoot`, so the same manual path may be considered by either discovery depending on contents. + +--- + +### 3) Configuration keys added +**File:** `src/vs/workbench/contrib/chat/common/constants.ts` + +Added `ChatConfiguration` keys: +- `CopilotPluginsEnabled = 'chat.plugins.copilot.enabled'` +- `ClaudePluginsEnabled = 'chat.plugins.claude.enabled'` +- `PluginPaths = 'chat.plugins.paths'` + +--- + +### 4) Configuration contribution and registration wiring added +**File:** `src/vs/workbench/contrib/chat/browser/chat.contribution.ts` + +Added config schema entries: +- `chat.plugins.copilot.enabled` (boolean, default `true`, experimental) +- `chat.plugins.claude.enabled` (boolean, default `false`, experimental) +- `chat.plugins.paths` (string array, default `[]`, `ConfigurationScope.MACHINE`, experimental) + +Added discovery registrations: +- `agentPluginDiscoveryRegistry.register(new SyncDescriptor(CopilotAgentPluginDiscovery))` +- `agentPluginDiscoveryRegistry.register(new SyncDescriptor(ClaudeAgentPluginDiscovery))` + +Added singleton registration: +- `registerSingleton(IAgentPluginService, AgentPluginService, InstantiationType.Delayed)` + +--- + +## Design Decisions Locked In + +1. **Unified plugin model at this stage is intentionally minimal** + - Only URI + hooks observable for now. + - Component-level metadata parsing is deferred. + +2. **AgentPluginService is intentionally generic** + - No mode/source assumptions in service orchestration. + - Discovery implementations own mode-specific behavior. + +3. **Per-discovery enablement** + - `CopilotAgentPluginDiscovery` and `ClaudeAgentPluginDiscovery` each gate themselves via their own config key. + +4. **Manual plugin paths are global candidates** + - Path list is shared by all discoveries. + - Discovery type determined by each discovery’s `isPluginRoot` logic. + +5. **Deterministic aggregation** + - URI dedupe + stable sort in service. + +--- + +## What Is Left (Overall) + +## Phase 1: Normalize plugin metadata and structure (next recommended) +- Extend `IAgentPlugin` to include normalized metadata fields (minimal but useful): + - plugin id/name + - display name/description/version (if available) + - manifest presence/mode info (if needed internally) +- Implement metadata loading in `toPlugin` (or dedicated loader): + - Copilot: parse `plugin.json` + - Claude: parse `.claude-plugin/plugin.json` if present, else infer from folder +- Add robust JSON parse/error handling with non-fatal skip behavior. + +## Phase 2: Component discovery and parsing +- Discover component roots/files per plugin: + - `agents/`, `skills/`, `commands/`, `hooks.json`, `.mcp.json`, `.lsp.json` +- Parse markdown frontmatter for agents/skills/commands. +- Populate plugin model with parsed components/hook data. +- Keep malformed components as soft-fail (skip component, continue plugin). + +## Phase 3: Conflict semantics + namespacing behavior +- Implement mode-specific duplicate behavior: + - Copilot: first-wins + warning + - Claude: allow duplicates via namespace +- Decide where conflict resolution belongs (service-level merge vs registry-level registrar). + +## Phase 4: Sandbox/validation hardening +- Validate paths used by plugin configs: + - no absolute path escapes + - no `../` traversal outside plugin root +- Consider symlink escape protections. + +## Phase 5: Activation/runtime integration +- Hook execution model (controlled subprocess) +- MCP/LSP process integration (if required in this project scope) +- Lifecycle dispatch (`onStart`, `onExit`, etc.) + +## Phase 6: Test coverage +- Unit tests for: + - mode detection + - manual path handling + - enable/disable config behavior + - dedupe/sort determinism + - malformed manifest/component handling + +--- + +## Immediate TODOs for Next Agent (Concrete) + +1. **Introduce plugin metadata type(s)** in `agentPluginService.ts`. +2. **Add plugin loader helper(s)** in `agentPluginServiceImpl.ts` to parse manifests safely. +3. **Update `toPlugin`** to include parsed metadata (or to return richer object). +4. **Add logging hooks** (trace/warn) for skip/failure paths. +5. **Add tests** under the appropriate chat common test suite for discovery + manual paths. + +--- + +## Known External/Unrelated Build Noise +Build/watch output in this workspace has shown unrelated existing failures outside this implementation area (for example parse/transpile errors in unrelated files). Treat these as pre-existing unless reproduced directly from plugin-service changes. + +For validation of this feature work, rely on: +- targeted diagnostics for touched files, +- and `VS Code - Build` watch output to ensure no new plugin-service-related compile errors. + +--- + +## Touched Files (so far) +- `src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts` +- `src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts` +- `src/vs/workbench/contrib/chat/common/constants.ts` +- `src/vs/workbench/contrib/chat/browser/chat.contribution.ts` + +--- + +## Quick Continuation Prompt (for another agent) +“Continue from PLAN.md. Implement Phase 1 by extending IAgentPlugin with normalized metadata and parsing plugin manifests (`plugin.json` and optional `.claude-plugin/plugin.json`) with non-fatal error handling. Keep AgentPluginService source-agnostic and preserve per-discovery enablement/manual path behavior.” diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 0000000000000..9855a7e862dba --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,481 @@ +```markdown +# Dual Plugin Support Implementation Guide +## Supporting GitHub Copilot CLI and Claude Code Plugins + +This document provides complete technical guidance for implementing plugin support compatible with: + +- **GitHub Copilot CLI** +- **Claude Code CLI** + +It is written for an implementation agent that must: + +1. Detect plugin format +2. Load plugin components +3. Validate structure +4. Resolve differences between systems +5. Execute components consistently +6. Optionally generate dual-compatible plugin packages + +--- + +# 1. Core Architectural Model + +Both systems treat a plugin as: + +> A self-contained directory containing structured Markdown components and optional JSON configuration files. + +Supported root-level components: + +``` + +plugin.json +agents/ +skills/ +commands/ +hooks.json +.mcp.json +.lsp.json +.claude-plugin/plugin.json + +``` + +All functional content must live inside the plugin directory. + +--- + +# 2. Detection Rules + +When loading a plugin directory: + +## 2.1 Determine Target Mode + +### Copilot Mode Detection +A plugin is Copilot-compatible if: +- `plugin.json` exists at root + +Copilot requires this file. + +### Claude Mode Detection +A plugin is Claude-compatible if: +- `.claude-plugin/plugin.json` exists +OR +- Any of `agents/`, `skills/`, `commands/`, `hooks.json`, `.mcp.json`, `.lsp.json` exists + +Claude does **not** require a manifest. + +--- + +# 3. Manifest Handling + +## 3.1 Copilot CLI Manifest (Required) + +Location: +``` + +plugin.json + +```` + +### Required Fields +```json +{ + "name": "kebab-case-id" +} +```` + +### Common Optional Fields + +* version +* description +* author +* homepage +* repository +* license +* keywords +* agents +* skills +* commands +* hooks +* mcpServers +* lspServers + +If component paths are omitted, defaults should be assumed: + +* agents → `agents/` +* skills → `skills/` +* commands → `commands/` +* hooks → `hooks.json` +* mcpServers → `.mcp.json` +* lspServers → `.lsp.json` + +--- + +## 3.2 Claude Manifest (Optional) + +Location: + +``` +.claude-plugin/plugin.json +``` + +Supports same metadata fields as Copilot, plus: + +* `outputStyles` + +If missing, Claude auto-discovers components. + +--- + +## 3.3 Unified Manifest Strategy + +For maximum compatibility: + +* Always generate a root `plugin.json` (Copilot requirement) +* Mirror metadata into `.claude-plugin/plugin.json` +* Ignore `outputStyles` when running in Copilot mode + +--- + +# 4. Component Loading Rules + +## 4.1 Agents (`agents/`) + +### Format + +Markdown files with YAML frontmatter: + +```markdown +--- +name: my-agent +description: Short description +model: optional-model +--- + +System prompt content... +``` + +### Claude Behavior + +* Namespaced as: `plugin-name:agent-name` +* All agents coexist via namespacing + +### Copilot Behavior + +* First-found-wins on duplicate agent names +* Later duplicates are silently ignored + +### Implementation Rules + +* Extract YAML frontmatter +* Validate `name` +* Namespace internally as: + + ``` + / + ``` +* Provide collision detection warnings in Copilot mode + +--- + +## 4.2 Skills (`skills/`) + +### Format + +Markdown with frontmatter: + +```markdown +--- +name: my-skill +description: Description +--- + +Instructions... +``` + +### Behavior Differences + +* Claude namespaces +* Copilot uses precedence resolution + +Implementation identical to agents. + +--- + +## 4.3 Commands (`commands/`) + +### Format + +Markdown with frontmatter: + +```markdown +--- +name: hello +description: Say hello +--- + +Prompt template... +``` + +### Claude + +Invoked as: + +``` +/plugin-name:hello +``` + +### Copilot + +Exposed as CLI command +May conflict silently + +### Implementation + +* Parse frontmatter +* Map to internal command registry +* Namespace consistently +* Apply mode-specific exposure rules + +--- + +## 4.4 Hooks (`hooks.json`) + +### Format + +```json +{ + "onStart": "echo starting", + "onTaskComplete": "echo done" +} +``` + +### Behavior + +Conceptually identical across systems. + +### Implementation + +* Validate JSON +* Map lifecycle events +* Normalize event names if necessary +* Execute in isolated subprocess + +--- + +## 4.5 MCP Servers (`.mcp.json`) + +### Format + +```json +{ + "servers": { + "my-server": { + "command": "node server.js" + } + } +} +``` + +### Differences + +Claude enforces strict directory sandbox: + +* Server files must reside inside plugin directory + +Copilot does not explicitly enforce but should follow same rule. + +### Implementation + +* Validate commands +* Ensure no path traversal outside plugin root +* Start servers as managed child processes + +--- + +## 4.6 LSP Servers (`.lsp.json`) + +Same handling as MCP. + +Ensure: + +* Command paths are relative to plugin directory +* No external file references + +--- + +# 5. Conflict Resolution Strategy + +## Claude Mode + +* Allow duplicate component names across plugins +* Enforce full namespace resolution +* Do not suppress components + +## Copilot Mode + +* Detect duplicates +* Apply first-loaded precedence +* Emit warning for suppressed duplicates + +--- + +# 6. Filesystem Isolation + +Mandatory for Claude compatibility: + +* All referenced files must exist within plugin directory +* Reject: + + * `../` traversal + * Absolute paths +* Copy plugin into cache-safe location if needed + +Recommended for Copilot mode as well. + +--- + +# 7. Unified Internal Plugin Model + +Represent plugins internally as: + +``` +Plugin { + name + metadata + agents[] + skills[] + commands[] + hooks + mcpServers + lspServers + mode: claude | copilot +} +``` + +All components should be normalized into this structure before execution. + +--- + +# 8. Dual-Compatible Packaging Rules + +To generate a plugin compatible with both: + +## Required Layout + +``` +my-plugin/ +│ +├── plugin.json +├── agents/ +├── skills/ +├── commands/ +├── hooks.json +├── .mcp.json +├── .lsp.json +└── .claude-plugin/ + └── plugin.json +``` + +## Build Strategy + +1. Maintain single source metadata file (e.g., `plugin.config.json`) +2. Generate: + + * root `plugin.json` + * `.claude-plugin/plugin.json` +3. Validate: + + * No external file references + * All component directories exist + +--- + +# 9. Validation Checklist + +When loading a plugin: + +* [ ] Plugin directory exists +* [ ] Root `plugin.json` present (Copilot mode) +* [ ] Plugin name is kebab-case +* [ ] All declared component paths exist +* [ ] No directory traversal outside root +* [ ] All Markdown files contain valid YAML frontmatter +* [ ] No duplicate component IDs (handle per mode) +* [ ] JSON files parse successfully + +--- + +# 10. Mode Behavior Summary + +| Feature | Claude | Copilot | +| -------------------- | -------- | ----------------------- | +| Manifest required | No | Yes | +| Namespacing | Explicit | Implicit | +| Duplicate handling | Allowed | First-wins | +| Sandbox enforcement | Strict | Not strictly documented | +| outputStyles support | Yes | No | + +--- + +# 11. Recommended Implementation Order + +1. Filesystem sandbox layer +2. Manifest loader +3. Component discovery engine +4. Markdown frontmatter parser +5. Conflict resolver +6. Lifecycle hook executor +7. MCP/LSP process manager +8. Dual-manifest generator (optional) + +--- + +# 12. Important Behavioral Constraints + +* Do not rely on external filesystem paths +* Do not assume manifest exists in Claude mode +* Do not assume auto-discovery works in Copilot mode +* Do not silently suppress duplicates without logging +* Always namespace internally + +--- + +# 13. Testing Matrix + +Test each plugin in: + +| Scenario | Expected Result | +| ----------------------------- | -------------------------------- | +| Claude without manifest | Auto-discovery works | +| Copilot without manifest | Fail | +| Duplicate agent names | Claude: OK / Copilot: first wins | +| MCP server with external path | Fail | +| Missing component directory | Soft fail if optional | + +--- + +# 14. Final Design Principle + +Treat both systems as: + +> Same plugin architecture with different manifest requirements and collision semantics. + +If you normalize: + +* component parsing +* sandbox enforcement +* namespacing +* manifest loading + +You can support both systems with one shared loader and a small behavior switch for: + +* manifest requirement +* duplicate resolution +* outputStyles handling + +--- + +END OF SPECIFICATION + +``` +``` diff --git a/PLUGINS_LOADING.md b/PLUGINS_LOADING.md new file mode 100644 index 0000000000000..5b158037646b7 --- /dev/null +++ b/PLUGINS_LOADING.md @@ -0,0 +1,388 @@ +```markdown +# Plugin Loading Specification +## Folder-Targeted Plugin Resolution for Copilot CLI + Claude Code Compatibility + +This specification defines how the application must: + +1. Discover plugins relevant to a specific target folder +2. Determine applicable scope +3. Load and normalize plugins +4. Resolve conflicts +5. Enforce sandboxing +6. Activate components +7. Handle mode differences (Claude vs Copilot) + +This spec assumes the plugin format described in the Dual Plugin Support Implementation Guide. + +--- + +# 1. Goals + +When the application is pointed at a target folder (project directory), it must: + +- Load all plugins relevant to that folder +- Respect scope precedence +- Support both Claude-style and Copilot-style plugins +- Normalize into a unified internal model +- Avoid collisions and sandbox violations +- Be deterministic and reproducible + +--- + +# 2. Terminology + +| Term | Meaning | +|------|---------| +| Target Folder | The project directory currently being operated on | +| Plugin Root | Directory containing plugin components | +| Mode | `claude` or `copilot` | +| Scope | Installation level: global, user, project, local | +| Normalized Plugin | Internal representation after parsing | + +--- + +# 3. Supported Plugin Scopes + +Plugins may exist at: + +### 3.1 Global Scope +System-wide installation directory +Example: +``` + +/usr/local/share/app/plugins/ + +``` + +### 3.2 User Scope +User-specific directory +Example: +``` + +~/.app/plugins/ + +``` + +### 3.3 Project Scope +Within the target folder +Example: +``` + +/.app/plugins/ + +``` + +### 3.4 Local Explicit Path +Direct path passed by CLI flag + +--- + +# 4. Plugin Discovery Algorithm + +When targeting a folder: + +## 4.1 Resolve Candidate Plugin Directories (Ordered by Precedence) + +1. Explicitly passed plugin paths +2. `/.app/plugins/` +3. `/.claude/plugins/` (Claude compatibility) +4. User plugin directory +5. Global plugin directory + +Within each directory: +- Each subdirectory is treated as a plugin candidate. + +--- + +# 5. Plugin Validity Check + +For each candidate directory: + +### Step 1: Confirm Directory Exists +Must be a directory. + +### Step 2: Detect Compatibility Mode + +If root `plugin.json` exists → Copilot-compatible +If `.claude-plugin/plugin.json` exists → Claude-compatible +If components exist without manifest → Claude-compatible + +If neither manifest nor components found → ignore directory. + +### Step 3: Validate Structure + +- No symlinks escaping plugin root +- No `../` references +- No absolute paths in configs +- All referenced component paths must exist + +If validation fails → reject plugin. + +--- + +# 6. Plugin Loading Order + +Plugins must be loaded in deterministic order: + +``` + +Explicit > Project > User > Global + +``` + +Within each scope: +- Alphabetical order by plugin directory name + +This ensures consistent first-wins semantics in Copilot mode. + +--- + +# 7. Normalization Process + +For each valid plugin: + +## 7.1 Load Manifest + +If in Copilot mode: +- Require root `plugin.json` + +If in Claude mode: +- Load `.claude-plugin/plugin.json` if present +- Otherwise auto-discover components + +## 7.2 Discover Components + +Default paths: +``` + +agents/ +skills/ +commands/ +hooks.json +.mcp.json +.lsp.json + +``` + +If manifest overrides paths: +- Use declared paths instead + +## 7.3 Parse Markdown Components + +For each `.md` file: +- Extract YAML frontmatter +- Require `name` +- Capture description +- Store body as content + +## 7.4 Build Internal Representation + +``` + +NormalizedPlugin { +name +scope +path +metadata +agents[] +skills[] +commands[] +hooks +mcpServers +lspServers +} + +``` + +--- + +# 8. Conflict Resolution Rules + +Conflict resolution differs by mode. + +--- + +## 8.1 Claude Mode + +- All components are namespaced by plugin name. +- No suppression. +- Fully qualified name format: + +``` + +plugin-name/component-name + +``` + +Duplicates across plugins are allowed. + +--- + +## 8.2 Copilot Mode + +Apply first-loaded-wins: + +If component ID already exists: +- Ignore later duplicate +- Log warning + +Applies to: +- agents +- skills +- commands + +Hooks: +- Merge if events differ +- Override if same event key (first wins) + +--- + +# 9. Sandbox Enforcement + +Before activating any plugin: + +- Ensure all file references are inside plugin directory +- Disallow: + - Absolute paths + - Parent directory traversal +- Reject plugin if violation detected + +This is mandatory for Claude compatibility and recommended universally. + +--- + +# 10. Activation Phase + +After normalization and conflict resolution: + +## 10.1 Register Agents +Add to agent registry (namespaced internally). + +## 10.2 Register Skills +Add to skill registry. + +## 10.3 Register Commands +Bind slash or CLI commands. + +## 10.4 Initialize Hooks +Attach to lifecycle event dispatcher. + +## 10.5 Start MCP Servers +Launch as managed child processes. + +## 10.6 Start LSP Servers +Launch per configuration. + +--- + +# 11. Runtime Lifecycle + +When targeting a folder: + +1. Discover plugins +2. Validate and normalize +3. Resolve conflicts +4. Activate components +5. Dispatch `onStart` hooks +6. Process user commands +7. Dispatch lifecycle events +8. On shutdown: + - Stop MCP servers + - Stop LSP servers + - Dispatch `onExit` hooks + +--- + +# 12. Reloading Strategy + +If target folder changes: + +- Unload all project-scope plugins +- Re-run discovery +- Preserve user/global plugins + +If plugin directory changes: +- Require explicit reload +- Do not auto-watch filesystem unless configured + +--- + +# 13. Error Handling Rules + +If a plugin fails validation: +- Log error +- Continue loading others + +If a component file is malformed: +- Skip component +- Do not reject entire plugin + +If manifest missing in Copilot mode: +- Reject plugin + +--- + +# 14. Performance Considerations + +- Cache normalized plugins per folder +- Hash plugin directory contents +- Invalidate cache if hash changes +- Avoid re-parsing Markdown unnecessarily + +--- + +# 15. Determinism Requirements + +The loader must guarantee: + +- Same folder + same plugin directories → identical component registry +- Load order strictly defined +- Conflict handling predictable + +--- + +# 16. Testing Matrix + +| Scenario | Expected Result | +|----------|-----------------| +| Duplicate agent in two global plugins | Copilot: first wins | +| Same duplicate in Claude | Both available | +| Plugin without manifest in Claude | Loads | +| Plugin without manifest in Copilot | Rejected | +| Plugin referencing external file | Rejected | +| Corrupt Markdown frontmatter | Skip component | + +--- + +# 17. Security Requirements + +- No execution of arbitrary shell commands outside MCP/LSP explicitly configured +- Hooks must run in controlled subprocess +- Validate JSON before execution +- Enforce directory isolation + +--- + +# 18. Summary of Mode Differences + +| Feature | Claude | Copilot | +|----------|---------|----------| +| Manifest required | No | Yes | +| Auto-discovery | Yes | No | +| Duplicate handling | Namespaced | First-wins | +| Strict sandbox | Yes | Recommended | + +--- + +# 19. Final Design Principle + +Treat plugin loading as: + +> Scoped, sandboxed, deterministic component aggregation with mode-specific collision semantics. + +All plugins must normalize into a unified internal model before activation. + +--- + +END OF SPECIFICATION +``` diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9c86e875e9b01..7ecf4e288191e 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -49,6 +49,7 @@ import { ILanguageModelsService, LanguageModelsService } from '../common/languag import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsConfirmationService } from '../common/tools/languageModelToolsConfirmationService.js'; import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; +import { agentPluginDiscoveryRegistry, IAgentPluginService } from '../common/plugins/agentPluginService.js'; import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS } from '../common/promptSyntax/config/promptFileLocations.js'; @@ -131,6 +132,7 @@ import './widget/input/editor/chatInputEditorContrib.js'; import './widget/input/editor/chatInputEditorHover.js'; import { LanguageModelToolsConfirmationService } from './tools/languageModelToolsConfirmationService.js'; import { LanguageModelToolsService, globalAutoApproveDescription } from './tools/languageModelToolsService.js'; +import { AgentPluginService, ClaudeAgentPluginDiscovery, CopilotAgentPluginDiscovery } from '../common/plugins/agentPluginServiceImpl.js'; import './promptSyntax/promptCodingAgentActionContribution.js'; import './promptSyntax/promptToolsCodeLensProvider.js'; import { ChatSlashCommandsContribution } from './chatSlashCommands.js'; @@ -619,6 +621,28 @@ configurationRegistry.registerConfiguration({ }, } }, + [ChatConfiguration.CopilotPluginsEnabled]: { + type: 'boolean', + description: nls.localize('chat.plugins.copilot.enabled', "Enable discovery of Copilot-compatible agent plugins in the workspace."), + default: true, + tags: ['experimental'], + }, + [ChatConfiguration.ClaudePluginsEnabled]: { + type: 'boolean', + description: nls.localize('chat.plugins.claude.enabled', "Enable discovery of Claude-compatible agent plugins in the workspace."), + default: false, + tags: ['experimental'], + }, + [ChatConfiguration.PluginPaths]: { + type: 'array', + items: { + type: 'string', + }, + description: nls.localize('chat.plugins.paths', "Additional local plugin directories to discover. Each path should point directly to a plugin folder."), + default: [], + scope: ConfigurationScope.MACHINE, + tags: ['experimental'], + }, [ChatConfiguration.AgentEnabled]: { type: 'boolean', description: nls.localize('chat.agent.enabled.description', "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."), @@ -1527,6 +1551,9 @@ registerLanguageModelActions(); registerAction2(ConfigureToolSets); registerEditorFeature(ChatPasteProvidersFeature); +agentPluginDiscoveryRegistry.register(new SyncDescriptor(CopilotAgentPluginDiscovery)); +agentPluginDiscoveryRegistry.register(new SyncDescriptor(ClaudeAgentPluginDiscovery)); + registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); @@ -1541,6 +1568,7 @@ registerSingleton(IChatSlashCommandService, ChatSlashCommandService, Instantiati registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); +registerSingleton(IAgentPluginService, AgentPluginService, InstantiationType.Delayed); registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); registerSingleton(ILanguageModelToolsConfirmationService, LanguageModelToolsConfirmationService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index 66ff7a09fea72..ded66c6508973 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -83,7 +83,7 @@ class SlashCommandCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /\/\w*/g); + const range = computeCompletionRanges(model, position, /\/[a-z0-9_.:-]*/gi); if (!range) { return null; } @@ -175,7 +175,7 @@ class SlashCommandCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /\/\w*/g); + const range = computeCompletionRanges(model, position, /\/[a-z0-9_.:-]*/gi); if (!range) { return null; } @@ -291,7 +291,7 @@ class AgentCompletions extends Disposable { return; } - const range = computeCompletionRanges(model, position, /\/\w*/g); + const range = computeCompletionRanges(model, position, /\/[a-z0-9_.:-]*/gi); if (!range) { return; } @@ -332,7 +332,7 @@ class AgentCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /(@|\/)\w*/g); + const range = computeCompletionRanges(model, position, /(@|\/)[a-z0-9_.:-]*/gi); if (!range) { return null; } @@ -432,7 +432,7 @@ class AgentCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /(@|\/)\w*/g); + const range = computeCompletionRanges(model, position, /(@|\/)[a-z0-9_.:-]*/gi); if (!range) { return null; } @@ -499,7 +499,7 @@ class AgentCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /(@|\/)\w*/g); + const range = computeCompletionRanges(model, position, /(@|\/)[a-z0-9_.:-]*/gi); if (!range) { return; } @@ -552,7 +552,7 @@ class AgentCompletions extends Disposable { for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { // Could allow text after 'position' - if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { + if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/[a-z0-9_.:-]*)?$/i)) { // No text allowed between agent and subcommand return; } diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 693f7c1f9da74..baadd5b03aa8f 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -10,6 +10,9 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey export enum ChatConfiguration { AIDisabled = 'chat.disableAIFeatures', + CopilotPluginsEnabled = 'chat.plugins.copilot.enabled', + ClaudePluginsEnabled = 'chat.plugins.claude.enabled', + PluginPaths = 'chat.plugins.paths', AgentEnabled = 'chat.agent.enabled', PlanAgentDefaultModel = 'chat.planAgent.defaultModel', ExploreAgentDefaultModel = 'chat.exploreAgent.defaultModel', diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts new file mode 100644 index 0000000000000..750d65a37bf2a --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { SyncDescriptor0 } from '../../../../../platform/instantiation/common/descriptors.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IMcpServerConfiguration } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; + +export const IAgentPluginService = createDecorator('agentPluginService'); + +export interface IAgentPluginHook { + readonly event: string; + readonly command: string; +} + +export interface IAgentPluginCommand { + readonly uri: URI; + readonly name: string; + readonly description?: string; + readonly content: string; +} + +export interface IAgentPluginMcpServerDefinition { + readonly name: string; + readonly configuration: IMcpServerConfiguration; +} + +export interface IAgentPlugin { + readonly uri: URI; + readonly hooks: IObservable; + readonly commands: IObservable; + readonly mcpServerDefinitions: IObservable; +} + +export interface IAgentPluginService { + readonly _serviceBrand: undefined; + readonly plugins: IObservable; +} + +export interface IAgentPluginDiscovery extends IDisposable { + readonly plugins: IObservable; + start(): void; +} + +export function getCanonicalPluginCommandId(plugin: IAgentPlugin, commandName: string): string { + const pluginSegment = plugin.uri.path.split('/').at(-1) ?? ''; + const prefix = normalizePluginToken(pluginSegment); + const normalizedCommand = normalizePluginToken(commandName); + if (!prefix || !normalizedCommand) { + return ''; + } + + if (normalizedCommand.startsWith(`${prefix}:`)) { + return normalizedCommand; + } + + return `${prefix}:${normalizedCommand}`; +} + +function normalizePluginToken(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9_.:-]/g, '-') + .replace(/-+/g, '-') + .replace(/^[-:.]+|[-:.]+$/g, ''); +} + +class AgentPluginDiscoveryRegistry { + private readonly _discovery: SyncDescriptor0[] = []; + + register(descriptor: SyncDescriptor0): void { + this._discovery.push(descriptor); + } + + getAll(): readonly SyncDescriptor0[] { + return this._discovery; + } +} + +export const agentPluginDiscoveryRegistry = new AgentPluginDiscoveryRegistry(); + + diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts new file mode 100644 index 0000000000000..ea7d201dc90aa --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -0,0 +1,474 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { parse as parseJSONC } from '../../../../../base/common/json.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { extname, joinPath } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IMcpServerConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { ChatConfiguration } from '../constants.js'; +import { PromptFileParser } from '../promptSyntax/promptFileParser.js'; +import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService } from './agentPluginService.js'; + +export class AgentPluginService extends Disposable implements IAgentPluginService { + + declare readonly _serviceBrand: undefined; + + private readonly _plugins = observableValue('agentPlugins', []); + public readonly plugins: IObservable = this._plugins; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + const store = this._register(new DisposableStore()); + + this._register(autorun(reader => { + store.clear(); + + const discoveries: IAgentPluginDiscovery[] = []; + for (const descriptor of agentPluginDiscoveryRegistry.getAll()) { + const discovery = instantiationService.createInstance(descriptor); + store.add(discovery); + discoveries.push(discovery); + discovery.start(); + } + + store.add(autorun(innerReader => { + const discoveredPlugins: IAgentPlugin[] = []; + for (const discovery of discoveries) { + discoveredPlugins.push(...discovery.plugins.read(innerReader)); + } + + this._plugins.set(this._dedupeAndSort(discoveredPlugins), undefined); + })); + })); + } + + private _dedupeAndSort(plugins: readonly IAgentPlugin[]): readonly IAgentPlugin[] { + const unique: IAgentPlugin[] = []; + const seen = new Set(); + + for (const plugin of plugins) { + const key = plugin.uri.toString(); + if (seen.has(key)) { + continue; + } + + seen.add(key); + unique.push(plugin); + } + + unique.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString())); + return unique; + } +} + +abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgentPluginDiscovery { + + private readonly _enabled: IObservable; + private readonly _manualPluginPaths: IObservable; + protected abstract readonly pluginSearchPaths: readonly string[]; + private readonly _pluginEntries = new Map(); + + private readonly _plugins = observableValue('discoveredAgentPlugins', []); + public readonly plugins: IObservable = this._plugins; + + private _discoverVersion = 0; + + constructor( + enabledConfigKey: ChatConfiguration, + enabledDefault: boolean, + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IFileService protected readonly fileService: IFileService, + ) { + super(); + this._enabled = observableConfigValue(enabledConfigKey, enabledDefault, configurationService); + this._manualPluginPaths = observableConfigValue(ChatConfiguration.PluginPaths, [], configurationService); + } + + public start(): void { + const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); + this._register(autorun(reader => { + this._enabled.read(reader); + this._manualPluginPaths.read(reader); + scheduler.schedule(); + })); + this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(() => scheduler.schedule())); + scheduler.schedule(); + } + + private async _refreshPlugins(): Promise { + const version = ++this._discoverVersion; + const plugins = await this.discoverPlugins(); + if (version !== this._discoverVersion || this._store.isDisposed) { + return; + } + + this._plugins.set(plugins, undefined); + } + + protected async discoverPlugins(): Promise { + if (this._enabled.get() === false) { + this._disposePluginEntriesExcept(new Set()); + return []; + } + + const candidates = await this.findCandidatePluginDirectories(); + const plugins: IAgentPlugin[] = []; + const seenPluginUris = new Set(); + + for (const uri of candidates) { + if (!(await this.isPluginRoot(uri))) { + continue; + } + + const key = uri.toString(); + seenPluginUris.add(key); + plugins.push(this.toPlugin(uri)); + } + + this._disposePluginEntriesExcept(seenPluginUris); + + plugins.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString())); + return plugins; + } + + protected toPlugin(uri: URI): IAgentPlugin { + const key = uri.toString(); + const existing = this._pluginEntries.get(key); + if (existing) { + return existing.plugin; + } + + const store = this._register(new DisposableStore()); + const commands = observableValue('agentPluginCommands', []); + const mcpServerDefinitions = observableValue('agentPluginMcpServerDefinitions', []); + const plugin: IAgentPlugin = { + uri, + hooks: observableValue('agentPluginHooks', []), + commands, + mcpServerDefinitions, + }; + + const scheduler = store.add(new RunOnceScheduler(() => { + void (async () => { + const [nextCommands, nextMcpDefinitions] = await Promise.all([ + this.readCommands(uri), + this.readMcpDefinitions(uri), + ]); + if (!store.isDisposed) { + commands.set(nextCommands, undefined); + mcpServerDefinitions.set(nextMcpDefinitions, undefined); + } + })(); + }, 200)); + + store.add(this.fileService.watch(uri, { recursive: true, excludes: [] })); + store.add(this.fileService.onDidFilesChange(e => { + if (e.affects(uri)) { + scheduler.schedule(); + } + })); + scheduler.schedule(); + + this._pluginEntries.set(key, { plugin, store }); + return plugin; + } + + private async readMcpDefinitions(pluginUri: URI): Promise { + const mcpUri = joinPath(pluginUri, '.mcp.json'); + + const mcpFileConfig = await this.readJsonFile(mcpUri); + const fileDefinitions = this.parseMcpServerDefinitionMap(mcpFileConfig); + + const pluginJsonDefinitions = await this.readInlinePluginJsonMcpDefinitions(pluginUri); + + const merged = new Map(); + for (const definition of fileDefinitions) { + merged.set(definition.name, definition.configuration); + } + for (const definition of pluginJsonDefinitions) { + if (!merged.has(definition.name)) { + merged.set(definition.name, definition.configuration); + } + } + + const definitions = [...merged.entries()] + .map(([name, configuration]) => ({ name, configuration } satisfies IAgentPluginMcpServerDefinition)) + .sort((a, b) => a.name.localeCompare(b.name)); + + return definitions; + } + + private async readInlinePluginJsonMcpDefinitions(pluginUri: URI): Promise { + const manifestPaths = [ + joinPath(pluginUri, 'plugin.json'), + joinPath(pluginUri, '.claude-plugin', 'plugin.json'), + ]; + + for (const manifestPath of manifestPaths) { + const manifest = await this.readJsonFile(manifestPath); + if (!manifest || typeof manifest !== 'object') { + continue; + } + + const manifestRecord = manifest as Record; + const mcpServers = manifestRecord['mcpServers']; + const definitions = this.parseMcpServerDefinitionMap(mcpServers); + if (definitions.length > 0) { + return definitions; + } + } + + return []; + } + + private parseMcpServerDefinitionMap(raw: unknown): IAgentPluginMcpServerDefinition[] { + if (!raw || typeof raw !== 'object') { + return []; + } + + const definitions: IAgentPluginMcpServerDefinition[] = []; + for (const [name, configValue] of Object.entries(raw as Record)) { + const configuration = this.normalizeMcpServerConfiguration(configValue); + if (!configuration) { + continue; + } + + definitions.push({ name, configuration }); + } + + return definitions; + } + + private normalizeMcpServerConfiguration(rawConfig: unknown): IMcpServerConfiguration | undefined { + if (!rawConfig || typeof rawConfig !== 'object') { + return undefined; + } + + const candidate = rawConfig as Record; + const type = typeof candidate['type'] === 'string' ? candidate['type'] : undefined; + + const command = typeof candidate['command'] === 'string' ? candidate['command'] : undefined; + const url = typeof candidate['url'] === 'string' ? candidate['url'] : undefined; + const args = Array.isArray(candidate['args']) ? candidate['args'].filter((value): value is string => typeof value === 'string') : undefined; + const env = candidate['env'] && typeof candidate['env'] === 'object' + ? Object.fromEntries(Object.entries(candidate['env'] as Record) + .filter(([, value]) => typeof value === 'string' || typeof value === 'number' || value === null) + .map(([key, value]) => [key, value as string | number | null])) + : undefined; + const envFile = typeof candidate['envFile'] === 'string' ? candidate['envFile'] : undefined; + const cwd = typeof candidate['cwd'] === 'string' ? candidate['cwd'] : undefined; + const headers = candidate['headers'] && typeof candidate['headers'] === 'object' + ? Object.fromEntries(Object.entries(candidate['headers'] as Record) + .filter(([, value]) => typeof value === 'string') + .map(([key, value]) => [key, value as string])) + : undefined; + const dev = candidate['dev'] && typeof candidate['dev'] === 'object' ? candidate['dev'] as IMcpStdioServerConfiguration['dev'] : undefined; + + if (type === 'ws') { + return undefined; + } + + if (type === McpServerType.LOCAL || (!type && command)) { + if (!command) { + return undefined; + } + + return { + type: McpServerType.LOCAL, + command, + args, + env, + envFile, + cwd, + dev, + }; + } + + if (type === McpServerType.REMOTE || type === 'sse' || (!type && url)) { + if (!url) { + return undefined; + } + + return { + type: McpServerType.REMOTE, + url, + headers, + dev, + }; + } + + return undefined; + } + + private async readJsonFile(uri: URI): Promise { + try { + const fileContents = await this.fileService.readFile(uri); + return parseJSONC(fileContents.value.toString()); + } catch { + return undefined; + } + } + + private async readCommands(uri: URI): Promise { + const commandsDir = joinPath(uri, 'commands'); + let stat; + try { + stat = await this.fileService.resolve(commandsDir); + } catch { + return []; + } + + if (!stat.isDirectory || !stat.children) { + return []; + } + + const parser = new PromptFileParser(); + const commands: IAgentPluginCommand[] = []; + for (const child of stat.children) { + if (!child.isFile || extname(child.resource) !== '.md') { + continue; + } + + let fileContents; + try { + fileContents = await this.fileService.readFile(child.resource); + } catch { + continue; + } + + const parsed = parser.parse(child.resource, fileContents.value.toString()); + const name = parsed.header?.name?.trim(); + if (!name) { + continue; + } + + commands.push({ + uri: child.resource, + name, + description: parsed.header?.description, + content: parsed.body?.getContent()?.trim() ?? '', + }); + } + + commands.sort((a, b) => a.name.localeCompare(b.name)); + return commands; + } + + protected abstract isPluginRoot(uri: URI): Promise; + + private async findCandidatePluginDirectories(): Promise { + const pluginDirectories = new Map(); + for (const folder of this._workspaceContextService.getWorkspace().folders) { + for (const searchPath of this.pluginSearchPaths) { + const pluginRoot = joinPath(folder.uri, searchPath); + let stat; + try { + stat = await this.fileService.resolve(pluginRoot); + } catch { + continue; + } + + if (!stat.isDirectory || !stat.children) { + continue; + } + + const children = stat.children.slice().sort((a, b) => a.name.localeCompare(b.name)); + for (const child of children) { + if (child.isDirectory) { + pluginDirectories.set(child.resource.toString(), child.resource); + } + } + } + } + + for (const path of this._manualPluginPaths.get()) { + if (typeof path !== 'string' || !path.trim()) { + continue; + } + + const resource = URI.file(path); + let stat; + try { + stat = await this.fileService.resolve(resource); + } catch { + continue; + } + + if (stat.isDirectory) { + pluginDirectories.set(stat.resource.toString(), stat.resource); + } + } + + return [...pluginDirectories.values()]; + } + + private _disposePluginEntriesExcept(keep: Set): void { + for (const [key, entry] of this._pluginEntries) { + if (!keep.has(key)) { + entry.store.dispose(); + this._pluginEntries.delete(key); + } + } + } + + public override dispose(): void { + this._disposePluginEntriesExcept(new Set()); + super.dispose(); + } +} + +export class CopilotAgentPluginDiscovery extends WorkspaceAgentPluginDiscovery { + protected readonly pluginSearchPaths = ['.copilot/plugins', '.vscode/plugins']; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IFileService fileService: IFileService, + ) { + super(ChatConfiguration.CopilotPluginsEnabled, true, configurationService, workspaceContextService, fileService); + } + + protected isPluginRoot(uri: URI): Promise { + return this.fileService.exists(joinPath(uri, 'plugin.json')); + } +} + +export class ClaudeAgentPluginDiscovery extends WorkspaceAgentPluginDiscovery { + protected readonly pluginSearchPaths = ['.claude/plugins', '.vscode/plugins']; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IFileService fileService: IFileService, + ) { + super(ChatConfiguration.ClaudePluginsEnabled, false, configurationService, workspaceContextService, fileService); + } + + protected async isPluginRoot(uri: URI): Promise { + const checks = await Promise.all([ + this.fileService.exists(joinPath(uri, '.claude-plugin/plugin.json')), + this.fileService.exists(joinPath(uri, 'agents')), + this.fileService.exists(joinPath(uri, 'skills')), + this.fileService.exists(joinPath(uri, 'commands')), + this.fileService.exists(joinPath(uri, 'hooks.json')), + this.fileService.exists(joinPath(uri, '.mcp.json')), + this.fileService.exists(joinPath(uri, '.lsp.json')), + ]); + + return checks.some(Boolean); + } +} + diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 1ba51c3703aed..ce3cc0de19324 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -38,6 +38,14 @@ export interface IPromptFileResource { * The URI to the agent or prompt resource file. */ readonly uri: URI; + /** + * Optional externally provided prompt command name. + */ + readonly name?: string; + /** + * Optional externally provided prompt command description. + */ + readonly description?: string; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index ded88d9804ab3..4c685dce1f379 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -8,6 +8,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { parse as parseJSONC } from '../../../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../../base/common/observable.js'; import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; import { basename, dirname, isEqual, joinPath } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -41,6 +42,7 @@ import { IWorkspaceContextService } from '../../../../../../platform/workspace/c import { IPathService } from '../../../../../services/path/common/pathService.js'; import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptValidator.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { getCanonicalPluginCommandId, IAgentPluginService } from '../../plugins/agentPluginService.js'; /** * Error thrown when a skill file is missing the required name attribute. @@ -140,6 +142,7 @@ export class PromptsService extends Disposable implements IPromptsService { private readonly _contributedWhenKeys = new Set(); private readonly _contributedWhenClauses = new Map(); private readonly _onDidContributedWhenChange = this._register(new Emitter()); + private _pluginPromptFiles: readonly ILocalPromptPath[] = []; constructor( @ILogService public readonly logger: ILogService, @@ -156,6 +159,7 @@ export class PromptsService extends Disposable implements IPromptsService { @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IPathService private readonly pathService: IPathService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, ) { super(); @@ -205,6 +209,37 @@ export class PromptsService extends Disposable implements IPromptsService { // Hack: Subscribe to activate caching (CachedPromise only caches when onDidChange has listeners) this._register(this.cachedSkills.onDidChange(() => { })); this._register(this.cachedHooks.onDidChange(() => { })); + + this._register(this.watchPluginPromptFiles()); + } + + private watchPluginPromptFiles() { + return autorun(reader => { + const plugins = this.agentPluginService.plugins.read(reader); + const nextPromptFiles: ILocalPromptPath[] = []; + const seen = new Set(); + for (const plugin of plugins) { + for (const pluginCommand of plugin.commands.read(reader)) { + const commandId = getCanonicalPluginCommandId(plugin, pluginCommand.name); + if (!commandId || seen.has(commandId)) { + continue; + } + + seen.add(commandId); + nextPromptFiles.push({ + uri: pluginCommand.uri, + storage: PromptsStorage.local, + type: PromptsType.prompt, + name: commandId, + description: pluginCommand.description, + }); + } + } + + nextPromptFiles.sort((a, b) => `${a.name ?? ''}|${a.uri.toString()}`.localeCompare(`${b.name ?? ''}|${b.uri.toString()}`)); + this._pluginPromptFiles = nextPromptFiles; + this.invalidatePromptFileCache(PromptsType.prompt); + }); } protected createPromptFilesLocator(): PromptFilesLocator { @@ -252,6 +287,7 @@ export class PromptsService extends Disposable implements IPromptsService { this.fileLocator.listFiles(type, PromptsStorage.user, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.user, type } satisfies IUserPromptPath))), this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath))), this.getExtensionPromptFiles(type, token), + this._pluginPromptFiles, ]); return [...prompts.flat()]; @@ -286,23 +322,27 @@ export class PromptsService extends Disposable implements IPromptsService { // Listen to provider change events to rerun computeListPromptFiles if (provider.onDidChangePromptFiles) { disposables.add(provider.onDidChangePromptFiles(() => { - if (type === PromptsType.agent) { - this.cachedFileLocations[PromptsType.agent] = undefined; - this.cachedCustomAgents.refresh(); - } else if (type === PromptsType.instructions) { - this.cachedFileLocations[PromptsType.instructions] = undefined; - } else if (type === PromptsType.prompt) { - this.cachedFileLocations[PromptsType.prompt] = undefined; - this.cachedSlashCommands.refresh(); - } else if (type === PromptsType.skill) { - this.cachedFileLocations[PromptsType.skill] = undefined; - this.cachedSkills.refresh(); - this.cachedSlashCommands.refresh(); - } + this.invalidatePromptFileCache(type); })); } // Invalidate cache when providers change + this.invalidatePromptFileCache(type); + + disposables.add({ + dispose: () => { + const index = this.promptFileProviders.findIndex((p) => p === providerEntry); + if (index >= 0) { + this.promptFileProviders.splice(index, 1); + this.invalidatePromptFileCache(type); + } + } + }); + + return disposables; + } + + private invalidatePromptFileCache(type: PromptsType): void { if (type === PromptsType.agent) { this.cachedFileLocations[PromptsType.agent] = undefined; this.cachedCustomAgents.refresh(); @@ -316,30 +356,6 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedSkills.refresh(); this.cachedSlashCommands.refresh(); } - - disposables.add({ - dispose: () => { - const index = this.promptFileProviders.findIndex((p) => p === providerEntry); - if (index >= 0) { - this.promptFileProviders.splice(index, 1); - if (type === PromptsType.agent) { - this.cachedFileLocations[PromptsType.agent] = undefined; - this.cachedCustomAgents.refresh(); - } else if (type === PromptsType.instructions) { - this.cachedFileLocations[PromptsType.instructions] = undefined; - } else if (type === PromptsType.prompt) { - this.cachedFileLocations[PromptsType.prompt] = undefined; - this.cachedSlashCommands.refresh(); - } else if (type === PromptsType.skill) { - this.cachedFileLocations[PromptsType.skill] = undefined; - this.cachedSkills.refresh(); - this.cachedSlashCommands.refresh(); - } - } - } - }); - - return disposables; } /** @@ -376,7 +392,9 @@ export class PromptsService extends Disposable implements IPromptsService { storage: PromptsStorage.extension, type, extension: providerEntry.extension, - source: ExtensionAgentSourceType.provider + source: ExtensionAgentSourceType.provider, + name: file.name, + description: file.description, } satisfies IExtensionPromptPath); } } catch (e) { @@ -388,7 +406,6 @@ export class PromptsService extends Disposable implements IPromptsService { } - public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { switch (storage) { case PromptsStorage.extension: diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 60c1c4049c2cf..9bdf3329e64ee 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -21,6 +21,7 @@ import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery import { InstalledMcpServersDiscovery } from '../common/discovery/installedMcpServersDiscovery.js'; import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; import { RemoteNativeMpcDiscovery } from '../common/discovery/nativeMcpRemoteDiscovery.js'; +import { PluginMcpDiscovery } from '../common/discovery/pluginMcpDiscovery.js'; import { CursorWorkspaceMcpDiscoveryAdapter } from '../common/discovery/workspaceMcpDiscoveryAdapter.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; import { mcpServerSchema } from '../common/mcpConfiguration.js'; @@ -60,6 +61,7 @@ mcpDiscoveryRegistry.register(new SyncDescriptor(RemoteNativeMpcDiscovery)); mcpDiscoveryRegistry.register(new SyncDescriptor(InstalledMcpServersDiscovery)); mcpDiscoveryRegistry.register(new SyncDescriptor(ExtensionMcpDiscovery)); mcpDiscoveryRegistry.register(new SyncDescriptor(CursorWorkspaceMcpDiscoveryAdapter)); +mcpDiscoveryRegistry.register(new SyncDescriptor(PluginMcpDiscovery)); registerWorkbenchContribution2('mcpDiscovery', McpDiscovery, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2('mcpContextKeys', McpContextKeysController, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts new file mode 100644 index 0000000000000..6c4f50fea5c1f --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { hash } from '../../../../../base/common/hash.js'; +import { Disposable, DisposableResourceMap } from '../../../../../base/common/lifecycle.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { basename } from '../../../../../base/common/resources.js'; +import { isDefined } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { IMcpServerConfiguration, McpServerType } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { + IAgentPlugin, + IAgentPluginMcpServerDefinition, + IAgentPluginService +} from '../../../chat/common/plugins/agentPluginService.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { McpCollectionSortOrder, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; + +export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { + readonly fromGallery = false; + + private readonly _collections = this._register(new DisposableResourceMap()); + + constructor( + @IAgentPluginService private readonly _agentPluginService: IAgentPluginService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + ) { + super(); + } + + public start(): void { + this._register(autorun(reader => { + const plugins = this._agentPluginService.plugins.read(reader); + const seen = new ResourceSet(); + for (const plugin of plugins) { + seen.add(plugin.uri); + + let collectionState = this._collections.get(plugin.uri); + if (!collectionState) { + collectionState = this.createCollectionState(plugin); + this._collections.set(plugin.uri, collectionState); + } + } + + for (const [pluginUri] of this._collections) { + if (!seen.has(pluginUri)) { + this._collections.deleteAndDispose(pluginUri); + } + } + })); + } + + private createCollectionState(plugin: IAgentPlugin) { + const collectionId = `plugin.${plugin.uri}`; + return this._mcpRegistry.registerCollection({ + id: collectionId, + label: `${basename(plugin.uri)}/.mcp.json`, + remoteAuthority: plugin.uri.scheme === Schemas.vscodeRemote ? plugin.uri.authority : null, + configTarget: ConfigurationTarget.USER, + scope: StorageScope.PROFILE, + trustBehavior: McpServerTrust.Kind.TrustedOnNonce, + serverDefinitions: plugin.mcpServerDefinitions.map(defs => + defs.map(d => this._toServerDefinition(collectionId, d)).filter(isDefined)), + presentation: { + origin: plugin.uri, + order: McpCollectionSortOrder.Filesystem + 1, + }, + }); + } + + private _toServerDefinition( + collectionId: string, + { name, configuration }: IAgentPluginMcpServerDefinition, + ): McpServerDefinition | undefined { + const launch = this._toLaunch(configuration); + if (!launch) { + return undefined; + } + + return { + id: `${collectionId}.${name}`, + label: name, + launch, + cacheNonce: String(hash(launch)), + }; + } + + private _toLaunch(config: IMcpServerConfiguration): McpServerLaunch | undefined { + if (config.type === McpServerType.LOCAL) { + return { + type: McpServerTransportType.Stdio, + command: config.command, + args: config.args ? [...config.args] : [], + env: config.env ? { ...config.env } : {}, + envFile: config.envFile, + cwd: config.cwd, + }; + } + + try { + return { + type: McpServerTransportType.HTTP, + uri: URI.parse(config.url), + headers: Object.entries(config.headers ?? {}), + }; + } catch { + return undefined; + } + } +} From fc1c5495530307a73fb9a054b2356f7dbb49d4e6 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 20 Feb 2026 09:44:34 -0800 Subject: [PATCH 04/28] wip --- .../chat/browser/actions/chatPluginActions.ts | 223 ++++++++++++++ .../contrib/chat/browser/chat.contribution.ts | 31 +- .../contrib/chat/common/constants.ts | 3 +- .../chat/common/plugins/agentPluginService.ts | 3 + .../common/plugins/agentPluginServiceImpl.ts | 274 +++++++----------- .../plugins/pluginMarketplaceService.ts | 98 +++++++ 6 files changed, 453 insertions(+), 179 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts create mode 100644 src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts new file mode 100644 index 0000000000000..5cc448db4a35d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { dirname } from '../../../../../base/common/resources.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPluginService.js'; +import { IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plugins/pluginMarketplaceService.js'; +import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; + +const enum ManagePluginItemKind { + Plugin = 'plugin', + FindMore = 'findMore', +} + +interface IPluginPickItem extends IQuickPickItem { + readonly kind: ManagePluginItemKind.Plugin; + plugin: IAgentPlugin; +} + +interface IFindMorePickItem extends IQuickPickItem { + readonly kind: ManagePluginItemKind.FindMore; +} + +interface IMarketplacePluginPickItem extends IQuickPickItem { + marketplacePlugin: IMarketplacePlugin; +} + +class ManagePluginsAction extends Action2 { + static readonly ID = 'workbench.action.chat.managePlugins'; + + constructor() { + super({ + id: ManagePluginsAction.ID, + title: localize2('managePlugins', 'Manage Plugins...'), + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled, + menu: [{ + id: CHAT_CONFIG_MENU_ID, + }], + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const agentPluginService = accessor.get(IAgentPluginService); + const quickInputService = accessor.get(IQuickInputService); + const labelService = accessor.get(ILabelService); + const dialogService = accessor.get(IDialogService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + const pluginMarketplaceService = accessor.get(IPluginMarketplaceService); + + const allPlugins = agentPluginService.allPlugins.get(); + const disabledUris = agentPluginService.disabledPluginUris.get(); + const hasWorkspace = workspaceContextService.getWorkspace().folders.length > 0; + + if (allPlugins.length === 0 && !hasWorkspace) { + dialogService.info( + localize('noPlugins', 'No plugins found.'), + localize('noPluginsDetail', 'There are currently no agent plugins discovered in this workspace.') + ); + return; + } + + // Group plugins by parent directory label + const groups = new Map(); + for (const plugin of allPlugins) { + const groupLabel = labelService.getUriLabel(dirname(plugin.uri), { relative: true }); + let group = groups.get(groupLabel); + if (!group) { + group = []; + groups.set(groupLabel, group); + } + group.push(plugin); + } + + const items: QuickPickInput[] = []; + for (const [groupLabel, plugins] of groups) { + items.push({ type: 'separator', label: groupLabel }); + for (const plugin of plugins) { + const pluginName = plugin.uri.path.split('/').at(-1) ?? ''; + items.push({ + kind: ManagePluginItemKind.Plugin, + label: pluginName, + plugin, + picked: !disabledUris.has(plugin.uri), + } satisfies IPluginPickItem); + } + } + + if (hasWorkspace) { + items.push({ type: 'separator' }); + items.push({ + kind: ManagePluginItemKind.FindMore, + label: localize('findMorePlugins', 'Find More Plugins...'), + } satisfies IFindMorePickItem); + } + + const result = await quickInputService.pick( + items, + { + canPickMany: true, + title: localize('managePluginsTitle', 'Manage Plugins'), + placeHolder: localize('managePluginsPlaceholder', 'Choose which plugins are enabled'), + } + ); + + if (!result) { + return; + } + + // Check if "Find More Plugins..." was selected + const findMoreSelected = result.some(item => item.kind === ManagePluginItemKind.FindMore); + if (findMoreSelected) { + await showMarketplaceQuickPick(quickInputService, pluginMarketplaceService, dialogService); + return; + } + + const pluginResults = result.filter((item): item is IPluginPickItem => item.kind === ManagePluginItemKind.Plugin); + const enabledUris = new ResourceSet(pluginResults.map(i => i.plugin.uri)); + for (const plugin of allPlugins) { + const wasDisabled = disabledUris.has(plugin.uri); + const isNowEnabled = enabledUris.has(plugin.uri); + + if (wasDisabled && isNowEnabled) { + agentPluginService.setPluginEnabled(plugin.uri, true); + } else if (!wasDisabled && !isNowEnabled) { + agentPluginService.setPluginEnabled(plugin.uri, false); + } + } + } +} + +async function showMarketplaceQuickPick( + quickInputService: IQuickInputService, + pluginMarketplaceService: IPluginMarketplaceService, + dialogService: IDialogService, +): Promise { + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); + quickPick.title = localize('marketplaceTitle', 'Plugin Marketplace'); + quickPick.placeholder = localize('marketplacePlaceholder', 'Select a plugin to install'); + quickPick.busy = true; + quickPick.show(); + + const cts = new CancellationTokenSource(); + quickPick.onDidHide(() => cts.dispose(true)); + + try { + const plugins = await pluginMarketplaceService.fetchMarketplacePlugins(cts.token); + + if (cts.token.isCancellationRequested) { + return; + } + + if (plugins.length === 0) { + quickPick.items = []; + quickPick.busy = false; + quickPick.placeholder = localize('noMarketplacePlugins', 'No plugins found in configured marketplaces'); + return; + } + + // Group by marketplace + const groups = new Map(); + for (const plugin of plugins) { + let group = groups.get(plugin.marketplace); + if (!group) { + group = []; + groups.set(plugin.marketplace, group); + } + group.push(plugin); + } + + const items: QuickPickInput[] = []; + for (const [marketplace, marketplacePlugins] of groups) { + items.push({ type: 'separator', label: marketplace }); + for (const plugin of marketplacePlugins) { + items.push({ + label: plugin.name, + detail: plugin.description, + description: plugin.version, + marketplacePlugin: plugin, + }); + } + } + + quickPick.items = items; + quickPick.busy = false; + } catch { + quickPick.busy = false; + quickPick.placeholder = localize('marketplaceError', 'Failed to fetch plugins from marketplaces'); + return; + } + + const selection = await new Promise(resolve => { + quickPick.onDidAccept(() => { + resolve(quickPick.selectedItems[0]); + quickPick.hide(); + }); + quickPick.onDidHide(() => resolve(undefined)); + }); + + if (selection) { + // TODO: Implement plugin installation + dialogService.info( + localize('installNotSupported', 'Plugin Installation'), + localize('installNotSupportedDetail', "Installing '{0}' from '{1}' is not yet supported.", selection.marketplacePlugin.name, selection.marketplacePlugin.marketplace) + ); + } +} + +export function registerChatPluginActions() { + registerAction2(ManagePluginsAction); +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7ecf4e288191e..d4a1f1980219f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -78,6 +78,7 @@ import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js' import { registerChatForkActions } from './actions/chatForkActions.js'; import { registerChatExportActions } from './actions/chatImportExport.js'; import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js'; +import { registerChatPluginActions } from './actions/chatPluginActions.js'; import { registerMoveActions } from './actions/chatMoveActions.js'; import { registerNewChatActions } from './actions/chatNewActions.js'; import { registerChatPromptNavigationActions } from './actions/chatPromptNavigationActions.js'; @@ -132,7 +133,8 @@ import './widget/input/editor/chatInputEditorContrib.js'; import './widget/input/editor/chatInputEditorHover.js'; import { LanguageModelToolsConfirmationService } from './tools/languageModelToolsConfirmationService.js'; import { LanguageModelToolsService, globalAutoApproveDescription } from './tools/languageModelToolsService.js'; -import { AgentPluginService, ClaudeAgentPluginDiscovery, CopilotAgentPluginDiscovery } from '../common/plugins/agentPluginServiceImpl.js'; +import { AgentPluginService, ConfiguredAgentPluginDiscovery } from '../common/plugins/agentPluginServiceImpl.js'; +import { IPluginMarketplaceService, PluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js'; import './promptSyntax/promptCodingAgentActionContribution.js'; import './promptSyntax/promptToolsCodeLensProvider.js'; import { ChatSlashCommandsContribution } from './chatSlashCommands.js'; @@ -621,18 +623,6 @@ configurationRegistry.registerConfiguration({ }, } }, - [ChatConfiguration.CopilotPluginsEnabled]: { - type: 'boolean', - description: nls.localize('chat.plugins.copilot.enabled', "Enable discovery of Copilot-compatible agent plugins in the workspace."), - default: true, - tags: ['experimental'], - }, - [ChatConfiguration.ClaudePluginsEnabled]: { - type: 'boolean', - description: nls.localize('chat.plugins.claude.enabled', "Enable discovery of Claude-compatible agent plugins in the workspace."), - default: false, - tags: ['experimental'], - }, [ChatConfiguration.PluginPaths]: { type: 'array', items: { @@ -643,6 +633,16 @@ configurationRegistry.registerConfiguration({ scope: ConfigurationScope.MACHINE, tags: ['experimental'], }, + [ChatConfiguration.PluginMarketplaces]: { + type: 'array', + items: { + type: 'string', + }, + markdownDescription: nls.localize('chat.plugins.marketplaces', "GitHub repositories to use as plugin marketplaces. Each entry should be in `owner/repo` format."), + default: ['github/copilot-plugins', 'github/awesome-copilot'], + scope: ConfigurationScope.APPLICATION, + tags: ['experimental'], + }, [ChatConfiguration.AgentEnabled]: { type: 'boolean', description: nls.localize('chat.agent.enabled.description', "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."), @@ -1548,11 +1548,11 @@ registerChatEditorActions(); registerChatElicitationActions(); registerChatToolActions(); registerLanguageModelActions(); +registerChatPluginActions(); registerAction2(ConfigureToolSets); registerEditorFeature(ChatPasteProvidersFeature); -agentPluginDiscoveryRegistry.register(new SyncDescriptor(CopilotAgentPluginDiscovery)); -agentPluginDiscoveryRegistry.register(new SyncDescriptor(ClaudeAgentPluginDiscovery)); +agentPluginDiscoveryRegistry.register(new SyncDescriptor(ConfiguredAgentPluginDiscovery)); registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); @@ -1569,6 +1569,7 @@ registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed registerSingleton(IChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); registerSingleton(IAgentPluginService, AgentPluginService, InstantiationType.Delayed); +registerSingleton(IPluginMarketplaceService, PluginMarketplaceService, InstantiationType.Delayed); registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); registerSingleton(ILanguageModelToolsConfirmationService, LanguageModelToolsConfirmationService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index baadd5b03aa8f..371ef3f271b5a 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -10,9 +10,8 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey export enum ChatConfiguration { AIDisabled = 'chat.disableAIFeatures', - CopilotPluginsEnabled = 'chat.plugins.copilot.enabled', - ClaudePluginsEnabled = 'chat.plugins.claude.enabled', PluginPaths = 'chat.plugins.paths', + PluginMarketplaces = 'chat.plugins.marketplaces', AgentEnabled = 'chat.agent.enabled', PlanAgentDefaultModel = 'chat.planAgent.defaultModel', ExploreAgentDefaultModel = 'chat.exploreAgent.defaultModel', diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index 750d65a37bf2a..ab71ab3799bc3 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -39,6 +39,9 @@ export interface IAgentPlugin { export interface IAgentPluginService { readonly _serviceBrand: undefined; readonly plugins: IObservable; + readonly allPlugins: IObservable; + readonly disabledPluginUris: IObservable>; + setPluginEnabled(pluginUri: URI, enabled: boolean): void; } export interface IAgentPluginDiscovery extends IDisposable { diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index ea7d201dc90aa..6121c2d067503 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -6,65 +6,105 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { autorun, IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; +import { autorun, derived, IObservable, observableValue } from '../../../../../base/common/observable.js'; import { extname, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMcpServerConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { ObservableMemento, observableMemento } from '../../../../../platform/observable/common/observableMemento.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ChatConfiguration } from '../constants.js'; import { PromptFileParser } from '../promptSyntax/promptFileParser.js'; import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService } from './agentPluginService.js'; +const STORAGE_KEY = 'workbench.chat.plugins.disabled'; +const disabledPluginUrisMemento = observableMemento>({ + key: STORAGE_KEY, + defaultValue: new ResourceSet(), + fromStorage: value => { + try { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) { + return new ResourceSet(); + } + + const uris = parsed + .filter((entry): entry is string => typeof entry === 'string') + .map(entry => URI.parse(entry)); + + return new ResourceSet(uris); + } catch { + return new ResourceSet(); + } + }, + toStorage: value => JSON.stringify([...value].map(uri => uri.toString()).sort((a, b) => a.localeCompare(b))) +}); + export class AgentPluginService extends Disposable implements IAgentPluginService { declare readonly _serviceBrand: undefined; - private readonly _plugins = observableValue('agentPlugins', []); - public readonly plugins: IObservable = this._plugins; + public readonly allPlugins: IObservable; + private readonly _disabledPluginUrisMemento: ObservableMemento>; + + public readonly disabledPluginUris: IObservable>; + public readonly plugins: IObservable; constructor( @IInstantiationService instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, ) { super(); - const store = this._register(new DisposableStore()); + this._disabledPluginUrisMemento = this._register(disabledPluginUrisMemento(StorageScope.PROFILE, StorageTarget.MACHINE, storageService)); - this._register(autorun(reader => { - store.clear(); - - const discoveries: IAgentPluginDiscovery[] = []; - for (const descriptor of agentPluginDiscoveryRegistry.getAll()) { - const discovery = instantiationService.createInstance(descriptor); - store.add(discovery); - discoveries.push(discovery); - discovery.start(); + this.disabledPluginUris = this._disabledPluginUrisMemento; + + const discoveries: IAgentPluginDiscovery[] = []; + for (const descriptor of agentPluginDiscoveryRegistry.getAll()) { + const discovery = instantiationService.createInstance(descriptor); + this._register(discovery); + discoveries.push(discovery); + discovery.start(); + } + + + this.allPlugins = derived(read => this._dedupeAndSort(discoveries.flatMap(d => d.plugins.read(read)))); + + this.plugins = derived(reader => { + const all = this.allPlugins.read(reader); + const disabled = this.disabledPluginUris.read(reader); + if (disabled.size === 0) { + return all; } - store.add(autorun(innerReader => { - const discoveredPlugins: IAgentPlugin[] = []; - for (const discovery of discoveries) { - discoveredPlugins.push(...discovery.plugins.read(innerReader)); - } + return all.filter(p => !disabled.has(p.uri)); + }); + } - this._plugins.set(this._dedupeAndSort(discoveredPlugins), undefined); - })); - })); + public setPluginEnabled(pluginUri: URI, enabled: boolean): void { + const current = new ResourceSet([...this._disabledPluginUrisMemento.get()]); + if (enabled) { + current.delete(pluginUri); + } else { + current.add(pluginUri); + } + this._disabledPluginUrisMemento.set(current, undefined); } private _dedupeAndSort(plugins: readonly IAgentPlugin[]): readonly IAgentPlugin[] { const unique: IAgentPlugin[] = []; - const seen = new Set(); + const seen = new ResourceSet(); for (const plugin of plugins) { - const key = plugin.uri.toString(); - if (seen.has(key)) { + if (seen.has(plugin.uri)) { continue; } - seen.add(key); + seen.add(plugin.uri); unique.push(plugin); } @@ -73,11 +113,9 @@ export class AgentPluginService extends Disposable implements IAgentPluginServic } } -abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgentPluginDiscovery { +export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgentPluginDiscovery { - private readonly _enabled: IObservable; - private readonly _manualPluginPaths: IObservable; - protected abstract readonly pluginSearchPaths: readonly string[]; + private readonly _pluginPaths: IObservable; private readonly _pluginEntries = new Map(); private readonly _plugins = observableValue('discoveredAgentPlugins', []); @@ -86,31 +124,25 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen private _discoverVersion = 0; constructor( - enabledConfigKey: ChatConfiguration, - enabledDefault: boolean, @IConfigurationService configurationService: IConfigurationService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @IFileService protected readonly fileService: IFileService, + @IFileService private readonly _fileService: IFileService, ) { super(); - this._enabled = observableConfigValue(enabledConfigKey, enabledDefault, configurationService); - this._manualPluginPaths = observableConfigValue(ChatConfiguration.PluginPaths, [], configurationService); + this._pluginPaths = observableConfigValue(ChatConfiguration.PluginPaths, [], configurationService); } public start(): void { const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); this._register(autorun(reader => { - this._enabled.read(reader); - this._manualPluginPaths.read(reader); + this._pluginPaths.read(reader); scheduler.schedule(); })); - this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(() => scheduler.schedule())); scheduler.schedule(); } private async _refreshPlugins(): Promise { const version = ++this._discoverVersion; - const plugins = await this.discoverPlugins(); + const plugins = await this._discoverPlugins(); if (version !== this._discoverVersion || this._store.isDisposed) { return; } @@ -118,24 +150,32 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen this._plugins.set(plugins, undefined); } - protected async discoverPlugins(): Promise { - if (this._enabled.get() === false) { - this._disposePluginEntriesExcept(new Set()); - return []; - } - - const candidates = await this.findCandidatePluginDirectories(); + private async _discoverPlugins(): Promise { const plugins: IAgentPlugin[] = []; const seenPluginUris = new Set(); - for (const uri of candidates) { - if (!(await this.isPluginRoot(uri))) { + for (const path of this._pluginPaths.get()) { + if (typeof path !== 'string' || !path.trim()) { continue; } - const key = uri.toString(); - seenPluginUris.add(key); - plugins.push(this.toPlugin(uri)); + const resource = URI.file(path); + let stat; + try { + stat = await this._fileService.resolve(resource); + } catch { + continue; + } + + if (!stat.isDirectory) { + continue; + } + + const key = stat.resource.toString(); + if (!seenPluginUris.has(key)) { + seenPluginUris.add(key); + plugins.push(this._toPlugin(stat.resource)); + } } this._disposePluginEntriesExcept(seenPluginUris); @@ -144,7 +184,7 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return plugins; } - protected toPlugin(uri: URI): IAgentPlugin { + private _toPlugin(uri: URI): IAgentPlugin { const key = uri.toString(); const existing = this._pluginEntries.get(key); if (existing) { @@ -164,8 +204,8 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen const scheduler = store.add(new RunOnceScheduler(() => { void (async () => { const [nextCommands, nextMcpDefinitions] = await Promise.all([ - this.readCommands(uri), - this.readMcpDefinitions(uri), + this._readCommands(uri), + this._readMcpDefinitions(uri), ]); if (!store.isDisposed) { commands.set(nextCommands, undefined); @@ -174,8 +214,8 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen })(); }, 200)); - store.add(this.fileService.watch(uri, { recursive: true, excludes: [] })); - store.add(this.fileService.onDidFilesChange(e => { + store.add(this._fileService.watch(uri, { recursive: true, excludes: [] })); + store.add(this._fileService.onDidFilesChange(e => { if (e.affects(uri)) { scheduler.schedule(); } @@ -186,13 +226,13 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return plugin; } - private async readMcpDefinitions(pluginUri: URI): Promise { + private async _readMcpDefinitions(pluginUri: URI): Promise { const mcpUri = joinPath(pluginUri, '.mcp.json'); - const mcpFileConfig = await this.readJsonFile(mcpUri); - const fileDefinitions = this.parseMcpServerDefinitionMap(mcpFileConfig); + const mcpFileConfig = await this._readJsonFile(mcpUri); + const fileDefinitions = this._parseMcpServerDefinitionMap(mcpFileConfig); - const pluginJsonDefinitions = await this.readInlinePluginJsonMcpDefinitions(pluginUri); + const pluginJsonDefinitions = await this._readInlinePluginJsonMcpDefinitions(pluginUri); const merged = new Map(); for (const definition of fileDefinitions) { @@ -211,21 +251,21 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return definitions; } - private async readInlinePluginJsonMcpDefinitions(pluginUri: URI): Promise { + private async _readInlinePluginJsonMcpDefinitions(pluginUri: URI): Promise { const manifestPaths = [ joinPath(pluginUri, 'plugin.json'), joinPath(pluginUri, '.claude-plugin', 'plugin.json'), ]; for (const manifestPath of manifestPaths) { - const manifest = await this.readJsonFile(manifestPath); + const manifest = await this._readJsonFile(manifestPath); if (!manifest || typeof manifest !== 'object') { continue; } const manifestRecord = manifest as Record; const mcpServers = manifestRecord['mcpServers']; - const definitions = this.parseMcpServerDefinitionMap(mcpServers); + const definitions = this._parseMcpServerDefinitionMap(mcpServers); if (definitions.length > 0) { return definitions; } @@ -234,14 +274,14 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return []; } - private parseMcpServerDefinitionMap(raw: unknown): IAgentPluginMcpServerDefinition[] { + private _parseMcpServerDefinitionMap(raw: unknown): IAgentPluginMcpServerDefinition[] { if (!raw || typeof raw !== 'object') { return []; } const definitions: IAgentPluginMcpServerDefinition[] = []; for (const [name, configValue] of Object.entries(raw as Record)) { - const configuration = this.normalizeMcpServerConfiguration(configValue); + const configuration = this._normalizeMcpServerConfiguration(configValue); if (!configuration) { continue; } @@ -252,7 +292,7 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return definitions; } - private normalizeMcpServerConfiguration(rawConfig: unknown): IMcpServerConfiguration | undefined { + private _normalizeMcpServerConfiguration(rawConfig: unknown): IMcpServerConfiguration | undefined { if (!rawConfig || typeof rawConfig !== 'object') { return undefined; } @@ -313,20 +353,20 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return undefined; } - private async readJsonFile(uri: URI): Promise { + private async _readJsonFile(uri: URI): Promise { try { - const fileContents = await this.fileService.readFile(uri); + const fileContents = await this._fileService.readFile(uri); return parseJSONC(fileContents.value.toString()); } catch { return undefined; } } - private async readCommands(uri: URI): Promise { + private async _readCommands(uri: URI): Promise { const commandsDir = joinPath(uri, 'commands'); let stat; try { - stat = await this.fileService.resolve(commandsDir); + stat = await this._fileService.resolve(commandsDir); } catch { return []; } @@ -344,7 +384,7 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen let fileContents; try { - fileContents = await this.fileService.readFile(child.resource); + fileContents = await this._fileService.readFile(child.resource); } catch { continue; } @@ -367,54 +407,6 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen return commands; } - protected abstract isPluginRoot(uri: URI): Promise; - - private async findCandidatePluginDirectories(): Promise { - const pluginDirectories = new Map(); - for (const folder of this._workspaceContextService.getWorkspace().folders) { - for (const searchPath of this.pluginSearchPaths) { - const pluginRoot = joinPath(folder.uri, searchPath); - let stat; - try { - stat = await this.fileService.resolve(pluginRoot); - } catch { - continue; - } - - if (!stat.isDirectory || !stat.children) { - continue; - } - - const children = stat.children.slice().sort((a, b) => a.name.localeCompare(b.name)); - for (const child of children) { - if (child.isDirectory) { - pluginDirectories.set(child.resource.toString(), child.resource); - } - } - } - } - - for (const path of this._manualPluginPaths.get()) { - if (typeof path !== 'string' || !path.trim()) { - continue; - } - - const resource = URI.file(path); - let stat; - try { - stat = await this.fileService.resolve(resource); - } catch { - continue; - } - - if (stat.isDirectory) { - pluginDirectories.set(stat.resource.toString(), stat.resource); - } - } - - return [...pluginDirectories.values()]; - } - private _disposePluginEntriesExcept(keep: Set): void { for (const [key, entry] of this._pluginEntries) { if (!keep.has(key)) { @@ -430,45 +422,3 @@ abstract class WorkspaceAgentPluginDiscovery extends Disposable implements IAgen } } -export class CopilotAgentPluginDiscovery extends WorkspaceAgentPluginDiscovery { - protected readonly pluginSearchPaths = ['.copilot/plugins', '.vscode/plugins']; - - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IFileService fileService: IFileService, - ) { - super(ChatConfiguration.CopilotPluginsEnabled, true, configurationService, workspaceContextService, fileService); - } - - protected isPluginRoot(uri: URI): Promise { - return this.fileService.exists(joinPath(uri, 'plugin.json')); - } -} - -export class ClaudeAgentPluginDiscovery extends WorkspaceAgentPluginDiscovery { - protected readonly pluginSearchPaths = ['.claude/plugins', '.vscode/plugins']; - - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IFileService fileService: IFileService, - ) { - super(ChatConfiguration.ClaudePluginsEnabled, false, configurationService, workspaceContextService, fileService); - } - - protected async isPluginRoot(uri: URI): Promise { - const checks = await Promise.all([ - this.fileService.exists(joinPath(uri, '.claude-plugin/plugin.json')), - this.fileService.exists(joinPath(uri, 'agents')), - this.fileService.exists(joinPath(uri, 'skills')), - this.fileService.exists(joinPath(uri, 'commands')), - this.fileService.exists(joinPath(uri, 'hooks.json')), - this.fileService.exists(joinPath(uri, '.mcp.json')), - this.fileService.exists(joinPath(uri, '.lsp.json')), - ]); - - return checks.some(Boolean); - } -} - diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts new file mode 100644 index 0000000000000..d4cd8217a282b --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { asJson, IRequestService } from '../../../../../platform/request/common/request.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { ChatConfiguration } from '../constants.js'; + +export interface IMarketplacePlugin { + readonly name: string; + readonly description: string; + readonly version: string; + readonly source: string; + readonly marketplace: string; +} + +interface IMarketplaceJson { + readonly plugins?: readonly { + readonly name?: string; + readonly description?: string; + readonly version?: string; + readonly source?: string; + }[]; +} + +export const IPluginMarketplaceService = createDecorator('pluginMarketplaceService'); + +export interface IPluginMarketplaceService { + readonly _serviceBrand: undefined; + fetchMarketplacePlugins(token: CancellationToken): Promise; +} + +/** + * Paths within a repository where marketplace.json can be found, checked in order. + */ +const MARKETPLACE_JSON_PATHS = [ + '.github/plugin/marketplace.json', + '.claude-plugin/marketplace.json', +]; + +export class PluginMarketplaceService implements IPluginMarketplaceService { + declare readonly _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IRequestService private readonly _requestService: IRequestService, + @ILogService private readonly _logService: ILogService, + ) { } + + async fetchMarketplacePlugins(token: CancellationToken): Promise { + const repos: string[] = this._configurationService.getValue(ChatConfiguration.PluginMarketplaces) ?? []; + const results = await Promise.all( + repos + .filter(repo => typeof repo === 'string' && /^[^/]+\/[^/]+$/.test(repo.trim())) + .map(repo => this._fetchFromRepo(repo.trim(), token)) + ); + return results.flat(); + } + + private async _fetchFromRepo(repo: string, token: CancellationToken): Promise { + for (const jsonPath of MARKETPLACE_JSON_PATHS) { + if (token.isCancellationRequested) { + return []; + } + const url = `https://raw.githubusercontent.com/${repo}/main/${jsonPath}`; + try { + const context = await this._requestService.request({ type: 'GET', url }, token); + if (context.res.statusCode !== 200) { + continue; + } + const json = await asJson(context); + if (!json?.plugins || !Array.isArray(json.plugins)) { + continue; + } + return json.plugins + .filter((p): p is { name: string; description: string; version: string; source: string } => + typeof p.name === 'string' && !!p.name + ) + .map(p => ({ + name: p.name, + description: p.description ?? '', + version: p.version ?? '', + source: p.source ?? '', + marketplace: repo, + })); + } catch (err) { + this._logService.debug(`[PluginMarketplaceService] Failed to fetch marketplace.json from ${url}:`, err); + continue; + } + } + this._logService.debug(`[PluginMarketplaceService] No marketplace.json found in ${repo}`); + return []; + } +} From 0dce1fc5d8410263e0397dbb8c488478c57b2556 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:48:00 -0800 Subject: [PATCH 05/28] Make isUntitled as deprecated --- src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 5f93a7fe90990..61458837f954b 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -482,7 +482,10 @@ declare module 'vscode' { export interface ChatSessionContext { readonly chatSessionItem: ChatSessionItem; // Maps to URI of chat session editor (could be 'untitled-1', etc..) + + /** @deprecated This will be removed along with the concept of `untitled-` sessions. */ readonly isUntitled: boolean; + /** * The initial option selections for the session, provided with the first request. * Contains the options the user selected (or defaults) before the session was created. From 3705b424b333b2bc939c53cf2b143e171ff8ce9e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 20 Feb 2026 12:14:18 -0800 Subject: [PATCH 06/28] agent: add preliminary plugin support Supports Copilot-style and the almost identical Claude-style plugins. In this PR we support skills, commands, and prompts. There is also a "browse" experience, based on the same registries that Copilot CLI comes with out of the box, but there is not yet an "Install" experience. Demo: https://memes.peet.io/img/26-02-6b29afc1-c3df-4f32-8364-c5307c35d43a.mp4 --- MCP_IN_PLUGINS.md | 538 ------------------ PLAN.md | 196 ------- PLUGINS.md | 481 ---------------- PLUGINS_LOADING.md | 388 ------------- .../chat/browser/actions/chatPluginActions.ts | 192 +++++-- .../contrib/chat/browser/chat.contribution.ts | 1 - .../chat/common/plugins/agentPluginService.ts | 15 +- .../common/plugins/agentPluginServiceImpl.ts | 98 +++- .../plugins/pluginMarketplaceService.ts | 26 +- .../service/promptsServiceImpl.ts | 53 +- 10 files changed, 294 insertions(+), 1694 deletions(-) delete mode 100644 MCP_IN_PLUGINS.md delete mode 100644 PLAN.md delete mode 100644 PLUGINS.md delete mode 100644 PLUGINS_LOADING.md diff --git a/MCP_IN_PLUGINS.md b/MCP_IN_PLUGINS.md deleted file mode 100644 index 164ba5c81a48c..0000000000000 --- a/MCP_IN_PLUGINS.md +++ /dev/null @@ -1,538 +0,0 @@ ---- -name: MCP Integration -description: This skill should be used when the user asks to "add MCP server", "integrate MCP", "configure MCP in plugin", "use .mcp.json", "set up Model Context Protocol", "connect external service", mentions "${CLAUDE_PLUGIN_ROOT} with MCP", or discusses MCP server types (SSE, stdio, HTTP, WebSocket). Provides comprehensive guidance for integrating Model Context Protocol servers into Claude Code plugins for external tool and service integration. -version: 0.1.0 ---- - -# MCP Integration for Claude Code Plugins - -## Overview - -Model Context Protocol (MCP) enables Claude Code plugins to integrate with external services and APIs by providing structured tool access. Use MCP integration to expose external service capabilities as tools within Claude Code. - -**Key capabilities:** -- Connect to external services (databases, APIs, file systems) -- Provide 10+ related tools from a single service -- Handle OAuth and complex authentication flows -- Bundle MCP servers with plugins for automatic setup - -## MCP Server Configuration Methods - -Plugins can bundle MCP servers in two ways: - -### Method 1: Dedicated .mcp.json (Recommended) - -Create `.mcp.json` at plugin root: - -```json -{ - "database-tools": { - "command": "${CLAUDE_PLUGIN_ROOT}/servers/db-server", - "args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"], - "env": { - "DB_URL": "${DB_URL}" - } - } -} -``` - -**Benefits:** -- Clear separation of concerns -- Easier to maintain -- Better for multiple servers - -### Method 2: Inline in plugin.json - -Add `mcpServers` field to plugin.json: - -```json -{ - "name": "my-plugin", - "version": "1.0.0", - "mcpServers": { - "plugin-api": { - "command": "${CLAUDE_PLUGIN_ROOT}/servers/api-server", - "args": ["--port", "8080"] - } - } -} -``` - -**Benefits:** -- Single configuration file -- Good for simple single-server plugins - -## MCP Server Types - -### stdio (Local Process) - -Execute local MCP servers as child processes. Best for local tools and custom servers. - -**Configuration:** -```json -{ - "filesystem": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"], - "env": { - "LOG_LEVEL": "debug" - } - } -} -``` - -**Use cases:** -- File system access -- Local database connections -- Custom MCP servers -- NPM-packaged MCP servers - -**Process management:** -- Claude Code spawns and manages the process -- Communicates via stdin/stdout -- Terminates when Claude Code exits - -### SSE (Server-Sent Events) - -Connect to hosted MCP servers with OAuth support. Best for cloud services. - -**Configuration:** -```json -{ - "asana": { - "type": "sse", - "url": "https://mcp.asana.com/sse" - } -} -``` - -**Use cases:** -- Official hosted MCP servers (Asana, GitHub, etc.) -- Cloud services with MCP endpoints -- OAuth-based authentication -- No local installation needed - -**Authentication:** -- OAuth flows handled automatically -- User prompted on first use -- Tokens managed by Claude Code - -### HTTP (REST API) - -Connect to RESTful MCP servers with token authentication. - -**Configuration:** -```json -{ - "api-service": { - "type": "http", - "url": "https://api.example.com/mcp", - "headers": { - "Authorization": "Bearer ${API_TOKEN}", - "X-Custom-Header": "value" - } - } -} -``` - -**Use cases:** -- REST API-based MCP servers -- Token-based authentication -- Custom API backends -- Stateless interactions - -### WebSocket (Real-time) - -Connect to WebSocket MCP servers for real-time bidirectional communication. - -**Configuration:** -```json -{ - "realtime-service": { - "type": "ws", - "url": "wss://mcp.example.com/ws", - "headers": { - "Authorization": "Bearer ${TOKEN}" - } - } -} -``` - -**Use cases:** -- Real-time data streaming -- Persistent connections -- Push notifications from server -- Low-latency requirements - -## Environment Variable Expansion - -All MCP configurations support environment variable substitution: - -**${CLAUDE_PLUGIN_ROOT}** - Plugin directory (always use for portability): -```json -{ - "command": "${CLAUDE_PLUGIN_ROOT}/servers/my-server" -} -``` - -**User environment variables** - From user's shell: -```json -{ - "env": { - "API_KEY": "${MY_API_KEY}", - "DATABASE_URL": "${DB_URL}" - } -} -``` - -**Best practice:** Document all required environment variables in plugin README. - -## MCP Tool Naming - -When MCP servers provide tools, they're automatically prefixed: - -**Format:** `mcp__plugin____` - -**Example:** -- Plugin: `asana` -- Server: `asana` -- Tool: `create_task` -- **Full name:** `mcp__plugin_asana_asana__asana_create_task` - -### Using MCP Tools in Commands - -Pre-allow specific MCP tools in command frontmatter: - -```markdown ---- -allowed-tools: [ - "mcp__plugin_asana_asana__asana_create_task", - "mcp__plugin_asana_asana__asana_search_tasks" -] ---- -``` - -**Wildcard (use sparingly):** -```markdown ---- -allowed-tools: ["mcp__plugin_asana_asana__*"] ---- -``` - -**Best practice:** Pre-allow specific tools, not wildcards, for security. - -## Lifecycle Management - -**Automatic startup:** -- MCP servers start when plugin enables -- Connection established before first tool use -- Restart required for configuration changes - -**Lifecycle:** -1. Plugin loads -2. MCP configuration parsed -3. Server process started (stdio) or connection established (SSE/HTTP/WS) -4. Tools discovered and registered -5. Tools available as `mcp__plugin_...__...` - -**Viewing servers:** -Use `/mcp` command to see all servers including plugin-provided ones. - -## Authentication Patterns - -### OAuth (SSE/HTTP) - -OAuth handled automatically by Claude Code: - -```json -{ - "type": "sse", - "url": "https://mcp.example.com/sse" -} -``` - -User authenticates in browser on first use. No additional configuration needed. - -### Token-Based (Headers) - -Static or environment variable tokens: - -```json -{ - "type": "http", - "url": "https://api.example.com", - "headers": { - "Authorization": "Bearer ${API_TOKEN}" - } -} -``` - -Document required environment variables in README. - -### Environment Variables (stdio) - -Pass configuration to MCP server: - -```json -{ - "command": "python", - "args": ["-m", "my_mcp_server"], - "env": { - "DATABASE_URL": "${DB_URL}", - "API_KEY": "${API_KEY}", - "LOG_LEVEL": "info" - } -} -``` - -## Integration Patterns - -### Pattern 1: Simple Tool Wrapper - -Commands use MCP tools with user interaction: - -```markdown -# Command: create-item.md ---- -allowed-tools: ["mcp__plugin_name_server__create_item"] ---- - -Steps: -1. Gather item details from user -2. Use mcp__plugin_name_server__create_item -3. Confirm creation -``` - -**Use for:** Adding validation or preprocessing before MCP calls. - -### Pattern 2: Autonomous Agent - -Agents use MCP tools autonomously: - -```markdown -# Agent: data-analyzer.md - -Analysis Process: -1. Query data via mcp__plugin_db_server__query -2. Process and analyze results -3. Generate insights report -``` - -**Use for:** Multi-step MCP workflows without user interaction. - -### Pattern 3: Multi-Server Plugin - -Integrate multiple MCP servers: - -```json -{ - "github": { - "type": "sse", - "url": "https://mcp.github.com/sse" - }, - "jira": { - "type": "sse", - "url": "https://mcp.jira.com/sse" - } -} -``` - -**Use for:** Workflows spanning multiple services. - -## Security Best Practices - -### Use HTTPS/WSS - -Always use secure connections: - -```json -✅ "url": "https://mcp.example.com/sse" -❌ "url": "http://mcp.example.com/sse" -``` - -### Token Management - -**DO:** -- ✅ Use environment variables for tokens -- ✅ Document required env vars in README -- ✅ Let OAuth flow handle authentication - -**DON'T:** -- ❌ Hardcode tokens in configuration -- ❌ Commit tokens to git -- ❌ Share tokens in documentation - -### Permission Scoping - -Pre-allow only necessary MCP tools: - -```markdown -✅ allowed-tools: [ - "mcp__plugin_api_server__read_data", - "mcp__plugin_api_server__create_item" -] - -❌ allowed-tools: ["mcp__plugin_api_server__*"] -``` - -## Error Handling - -### Connection Failures - -Handle MCP server unavailability: -- Provide fallback behavior in commands -- Inform user of connection issues -- Check server URL and configuration - -### Tool Call Errors - -Handle failed MCP operations: -- Validate inputs before calling MCP tools -- Provide clear error messages -- Check rate limiting and quotas - -### Configuration Errors - -Validate MCP configuration: -- Test server connectivity during development -- Validate JSON syntax -- Check required environment variables - -## Performance Considerations - -### Lazy Loading - -MCP servers connect on-demand: -- Not all servers connect at startup -- First tool use triggers connection -- Connection pooling managed automatically - -### Batching - -Batch similar requests when possible: - -``` -# Good: Single query with filters -tasks = search_tasks(project="X", assignee="me", limit=50) - -# Avoid: Many individual queries -for id in task_ids: - task = get_task(id) -``` - -## Testing MCP Integration - -### Local Testing - -1. Configure MCP server in `.mcp.json` -2. Install plugin locally (`.claude-plugin/`) -3. Run `/mcp` to verify server appears -4. Test tool calls in commands -5. Check `claude --debug` logs for connection issues - -### Validation Checklist - -- [ ] MCP configuration is valid JSON -- [ ] Server URL is correct and accessible -- [ ] Required environment variables documented -- [ ] Tools appear in `/mcp` output -- [ ] Authentication works (OAuth or tokens) -- [ ] Tool calls succeed from commands -- [ ] Error cases handled gracefully - -## Debugging - -### Enable Debug Logging - -```bash -claude --debug -``` - -Look for: -- MCP server connection attempts -- Tool discovery logs -- Authentication flows -- Tool call errors - -### Common Issues - -**Server not connecting:** -- Check URL is correct -- Verify server is running (stdio) -- Check network connectivity -- Review authentication configuration - -**Tools not available:** -- Verify server connected successfully -- Check tool names match exactly -- Run `/mcp` to see available tools -- Restart Claude Code after config changes - -**Authentication failing:** -- Clear cached auth tokens -- Re-authenticate -- Check token scopes and permissions -- Verify environment variables set - -## Quick Reference - -### MCP Server Types - -| Type | Transport | Best For | Auth | -|------|-----------|----------|------| -| stdio | Process | Local tools, custom servers | Env vars | -| SSE | HTTP | Hosted services, cloud APIs | OAuth | -| HTTP | REST | API backends, token auth | Tokens | -| ws | WebSocket | Real-time, streaming | Tokens | - -### Configuration Checklist - -- [ ] Server type specified (stdio/SSE/HTTP/ws) -- [ ] Type-specific fields complete (command or url) -- [ ] Authentication configured -- [ ] Environment variables documented -- [ ] HTTPS/WSS used (not HTTP/WS) -- [ ] ${CLAUDE_PLUGIN_ROOT} used for paths - -### Best Practices - -**DO:** -- ✅ Use ${CLAUDE_PLUGIN_ROOT} for portable paths -- ✅ Document required environment variables -- ✅ Use secure connections (HTTPS/WSS) -- ✅ Pre-allow specific MCP tools in commands -- ✅ Test MCP integration before publishing -- ✅ Handle connection and tool errors gracefully - -**DON'T:** -- ❌ Hardcode absolute paths -- ❌ Commit credentials to git -- ❌ Use HTTP instead of HTTPS -- ❌ Pre-allow all tools with wildcards -- ❌ Skip error handling -- ❌ Forget to document setup - -## Additional Resources - -### Reference Files - -For detailed information, consult: - -- **`references/server-types.md`** - Deep dive on each server type -- **`references/authentication.md`** - Authentication patterns and OAuth -- **`references/tool-usage.md`** - Using MCP tools in commands and agents - -### Example Configurations - -Working examples in `examples/`: - -- **`stdio-server.json`** - Local stdio MCP server -- **`sse-server.json`** - Hosted SSE server with OAuth -- **`http-server.json`** - REST API with token auth - -### External Resources - -- **Official MCP Docs**: https://modelcontextprotocol.io/ -- **Claude Code MCP Docs**: https://docs.claude.com/en/docs/claude-code/mcp -- **MCP SDK**: @modelcontextprotocol/sdk -- **Testing**: Use `claude --debug` and `/mcp` command diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 56b5ad83b7551..0000000000000 --- a/PLAN.md +++ /dev/null @@ -1,196 +0,0 @@ -# Agent Plugin Implementation Plan (Handoff) - -## Objective -Implement dual support for Copilot-style and Claude-style agent plugins in VS Code chat, with modular discovery and a unified internal plugin service. - -This document summarizes: -- what is already implemented, -- what design decisions were made, -- what remains to be built, -- and a concrete continuation plan for another agent. - ---- - -## Current Status (Implemented) - -### 1) Core service contracts scaffolded -**File:** `src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts` - -Implemented: -- `IAgentPluginService` via `createDecorator('agentPluginService')` -- `IAgentPlugin` with: - - `uri: URI` - - `hooks: IObservable` -- `IAgentPluginHook` with: - - `event: string` - - `command: string` -- `IAgentPluginDiscovery` interface with: - - `plugins: IObservable` - - `start(): void` -- `agentPluginDiscoveryRegistry` (MCP-inspired descriptor registry) - -Notable refactor already completed: -- Removed `source`/`mode` from `IAgentPlugin` as redundant. -- Service-level source enablement logic was removed from `AgentPluginService` and moved into discovery implementations. - ---- - -### 2) Service implementation scaffolded and discovery modularized -**File:** `src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts` - -Implemented: -- `AgentPluginService` that is source-agnostic: - - instantiates all registered discoveries - - starts each discovery - - aggregates discovery observables - - dedupes by plugin URI - - deterministic sort by URI -- `WorkspaceAgentPluginDiscovery` base class: - - internal per-discovery enablement handling via config observable - - workspace folder-based candidate directory scan - - manual plugin path support (see config below) - - per-discovery `isPluginRoot(uri)` type check - - `toPlugin(uri)` currently returns plugin shell with empty hooks observable - -Implemented discovery types: -- `CopilotAgentPluginDiscovery` - - search paths: `.copilot/plugins`, `.vscode/plugins` - - root detection: `plugin.json` - - enablement key: `chat.plugins.copilot.enabled` (default true) -- `ClaudeAgentPluginDiscovery` - - search paths: `.claude/plugins`, `.vscode/plugins` - - root detection: `.claude-plugin/plugin.json` OR any of: - - `agents/`, `skills/`, `commands/`, `hooks.json`, `.mcp.json`, `.lsp.json` - - enablement key: `chat.plugins.claude.enabled` (default false) - -Manual path behavior implemented: -- `chat.plugins.paths` is read by base discovery class. -- Every path is treated as candidate plugin root if it resolves to a directory. -- Plugin type is inferred by each discovery via `isPluginRoot`, so the same manual path may be considered by either discovery depending on contents. - ---- - -### 3) Configuration keys added -**File:** `src/vs/workbench/contrib/chat/common/constants.ts` - -Added `ChatConfiguration` keys: -- `CopilotPluginsEnabled = 'chat.plugins.copilot.enabled'` -- `ClaudePluginsEnabled = 'chat.plugins.claude.enabled'` -- `PluginPaths = 'chat.plugins.paths'` - ---- - -### 4) Configuration contribution and registration wiring added -**File:** `src/vs/workbench/contrib/chat/browser/chat.contribution.ts` - -Added config schema entries: -- `chat.plugins.copilot.enabled` (boolean, default `true`, experimental) -- `chat.plugins.claude.enabled` (boolean, default `false`, experimental) -- `chat.plugins.paths` (string array, default `[]`, `ConfigurationScope.MACHINE`, experimental) - -Added discovery registrations: -- `agentPluginDiscoveryRegistry.register(new SyncDescriptor(CopilotAgentPluginDiscovery))` -- `agentPluginDiscoveryRegistry.register(new SyncDescriptor(ClaudeAgentPluginDiscovery))` - -Added singleton registration: -- `registerSingleton(IAgentPluginService, AgentPluginService, InstantiationType.Delayed)` - ---- - -## Design Decisions Locked In - -1. **Unified plugin model at this stage is intentionally minimal** - - Only URI + hooks observable for now. - - Component-level metadata parsing is deferred. - -2. **AgentPluginService is intentionally generic** - - No mode/source assumptions in service orchestration. - - Discovery implementations own mode-specific behavior. - -3. **Per-discovery enablement** - - `CopilotAgentPluginDiscovery` and `ClaudeAgentPluginDiscovery` each gate themselves via their own config key. - -4. **Manual plugin paths are global candidates** - - Path list is shared by all discoveries. - - Discovery type determined by each discovery’s `isPluginRoot` logic. - -5. **Deterministic aggregation** - - URI dedupe + stable sort in service. - ---- - -## What Is Left (Overall) - -## Phase 1: Normalize plugin metadata and structure (next recommended) -- Extend `IAgentPlugin` to include normalized metadata fields (minimal but useful): - - plugin id/name - - display name/description/version (if available) - - manifest presence/mode info (if needed internally) -- Implement metadata loading in `toPlugin` (or dedicated loader): - - Copilot: parse `plugin.json` - - Claude: parse `.claude-plugin/plugin.json` if present, else infer from folder -- Add robust JSON parse/error handling with non-fatal skip behavior. - -## Phase 2: Component discovery and parsing -- Discover component roots/files per plugin: - - `agents/`, `skills/`, `commands/`, `hooks.json`, `.mcp.json`, `.lsp.json` -- Parse markdown frontmatter for agents/skills/commands. -- Populate plugin model with parsed components/hook data. -- Keep malformed components as soft-fail (skip component, continue plugin). - -## Phase 3: Conflict semantics + namespacing behavior -- Implement mode-specific duplicate behavior: - - Copilot: first-wins + warning - - Claude: allow duplicates via namespace -- Decide where conflict resolution belongs (service-level merge vs registry-level registrar). - -## Phase 4: Sandbox/validation hardening -- Validate paths used by plugin configs: - - no absolute path escapes - - no `../` traversal outside plugin root -- Consider symlink escape protections. - -## Phase 5: Activation/runtime integration -- Hook execution model (controlled subprocess) -- MCP/LSP process integration (if required in this project scope) -- Lifecycle dispatch (`onStart`, `onExit`, etc.) - -## Phase 6: Test coverage -- Unit tests for: - - mode detection - - manual path handling - - enable/disable config behavior - - dedupe/sort determinism - - malformed manifest/component handling - ---- - -## Immediate TODOs for Next Agent (Concrete) - -1. **Introduce plugin metadata type(s)** in `agentPluginService.ts`. -2. **Add plugin loader helper(s)** in `agentPluginServiceImpl.ts` to parse manifests safely. -3. **Update `toPlugin`** to include parsed metadata (or to return richer object). -4. **Add logging hooks** (trace/warn) for skip/failure paths. -5. **Add tests** under the appropriate chat common test suite for discovery + manual paths. - ---- - -## Known External/Unrelated Build Noise -Build/watch output in this workspace has shown unrelated existing failures outside this implementation area (for example parse/transpile errors in unrelated files). Treat these as pre-existing unless reproduced directly from plugin-service changes. - -For validation of this feature work, rely on: -- targeted diagnostics for touched files, -- and `VS Code - Build` watch output to ensure no new plugin-service-related compile errors. - ---- - -## Touched Files (so far) -- `src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts` -- `src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts` -- `src/vs/workbench/contrib/chat/common/constants.ts` -- `src/vs/workbench/contrib/chat/browser/chat.contribution.ts` - ---- - -## Quick Continuation Prompt (for another agent) -“Continue from PLAN.md. Implement Phase 1 by extending IAgentPlugin with normalized metadata and parsing plugin manifests (`plugin.json` and optional `.claude-plugin/plugin.json`) with non-fatal error handling. Keep AgentPluginService source-agnostic and preserve per-discovery enablement/manual path behavior.” diff --git a/PLUGINS.md b/PLUGINS.md deleted file mode 100644 index 9855a7e862dba..0000000000000 --- a/PLUGINS.md +++ /dev/null @@ -1,481 +0,0 @@ -```markdown -# Dual Plugin Support Implementation Guide -## Supporting GitHub Copilot CLI and Claude Code Plugins - -This document provides complete technical guidance for implementing plugin support compatible with: - -- **GitHub Copilot CLI** -- **Claude Code CLI** - -It is written for an implementation agent that must: - -1. Detect plugin format -2. Load plugin components -3. Validate structure -4. Resolve differences between systems -5. Execute components consistently -6. Optionally generate dual-compatible plugin packages - ---- - -# 1. Core Architectural Model - -Both systems treat a plugin as: - -> A self-contained directory containing structured Markdown components and optional JSON configuration files. - -Supported root-level components: - -``` - -plugin.json -agents/ -skills/ -commands/ -hooks.json -.mcp.json -.lsp.json -.claude-plugin/plugin.json - -``` - -All functional content must live inside the plugin directory. - ---- - -# 2. Detection Rules - -When loading a plugin directory: - -## 2.1 Determine Target Mode - -### Copilot Mode Detection -A plugin is Copilot-compatible if: -- `plugin.json` exists at root - -Copilot requires this file. - -### Claude Mode Detection -A plugin is Claude-compatible if: -- `.claude-plugin/plugin.json` exists -OR -- Any of `agents/`, `skills/`, `commands/`, `hooks.json`, `.mcp.json`, `.lsp.json` exists - -Claude does **not** require a manifest. - ---- - -# 3. Manifest Handling - -## 3.1 Copilot CLI Manifest (Required) - -Location: -``` - -plugin.json - -```` - -### Required Fields -```json -{ - "name": "kebab-case-id" -} -```` - -### Common Optional Fields - -* version -* description -* author -* homepage -* repository -* license -* keywords -* agents -* skills -* commands -* hooks -* mcpServers -* lspServers - -If component paths are omitted, defaults should be assumed: - -* agents → `agents/` -* skills → `skills/` -* commands → `commands/` -* hooks → `hooks.json` -* mcpServers → `.mcp.json` -* lspServers → `.lsp.json` - ---- - -## 3.2 Claude Manifest (Optional) - -Location: - -``` -.claude-plugin/plugin.json -``` - -Supports same metadata fields as Copilot, plus: - -* `outputStyles` - -If missing, Claude auto-discovers components. - ---- - -## 3.3 Unified Manifest Strategy - -For maximum compatibility: - -* Always generate a root `plugin.json` (Copilot requirement) -* Mirror metadata into `.claude-plugin/plugin.json` -* Ignore `outputStyles` when running in Copilot mode - ---- - -# 4. Component Loading Rules - -## 4.1 Agents (`agents/`) - -### Format - -Markdown files with YAML frontmatter: - -```markdown ---- -name: my-agent -description: Short description -model: optional-model ---- - -System prompt content... -``` - -### Claude Behavior - -* Namespaced as: `plugin-name:agent-name` -* All agents coexist via namespacing - -### Copilot Behavior - -* First-found-wins on duplicate agent names -* Later duplicates are silently ignored - -### Implementation Rules - -* Extract YAML frontmatter -* Validate `name` -* Namespace internally as: - - ``` - / - ``` -* Provide collision detection warnings in Copilot mode - ---- - -## 4.2 Skills (`skills/`) - -### Format - -Markdown with frontmatter: - -```markdown ---- -name: my-skill -description: Description ---- - -Instructions... -``` - -### Behavior Differences - -* Claude namespaces -* Copilot uses precedence resolution - -Implementation identical to agents. - ---- - -## 4.3 Commands (`commands/`) - -### Format - -Markdown with frontmatter: - -```markdown ---- -name: hello -description: Say hello ---- - -Prompt template... -``` - -### Claude - -Invoked as: - -``` -/plugin-name:hello -``` - -### Copilot - -Exposed as CLI command -May conflict silently - -### Implementation - -* Parse frontmatter -* Map to internal command registry -* Namespace consistently -* Apply mode-specific exposure rules - ---- - -## 4.4 Hooks (`hooks.json`) - -### Format - -```json -{ - "onStart": "echo starting", - "onTaskComplete": "echo done" -} -``` - -### Behavior - -Conceptually identical across systems. - -### Implementation - -* Validate JSON -* Map lifecycle events -* Normalize event names if necessary -* Execute in isolated subprocess - ---- - -## 4.5 MCP Servers (`.mcp.json`) - -### Format - -```json -{ - "servers": { - "my-server": { - "command": "node server.js" - } - } -} -``` - -### Differences - -Claude enforces strict directory sandbox: - -* Server files must reside inside plugin directory - -Copilot does not explicitly enforce but should follow same rule. - -### Implementation - -* Validate commands -* Ensure no path traversal outside plugin root -* Start servers as managed child processes - ---- - -## 4.6 LSP Servers (`.lsp.json`) - -Same handling as MCP. - -Ensure: - -* Command paths are relative to plugin directory -* No external file references - ---- - -# 5. Conflict Resolution Strategy - -## Claude Mode - -* Allow duplicate component names across plugins -* Enforce full namespace resolution -* Do not suppress components - -## Copilot Mode - -* Detect duplicates -* Apply first-loaded precedence -* Emit warning for suppressed duplicates - ---- - -# 6. Filesystem Isolation - -Mandatory for Claude compatibility: - -* All referenced files must exist within plugin directory -* Reject: - - * `../` traversal - * Absolute paths -* Copy plugin into cache-safe location if needed - -Recommended for Copilot mode as well. - ---- - -# 7. Unified Internal Plugin Model - -Represent plugins internally as: - -``` -Plugin { - name - metadata - agents[] - skills[] - commands[] - hooks - mcpServers - lspServers - mode: claude | copilot -} -``` - -All components should be normalized into this structure before execution. - ---- - -# 8. Dual-Compatible Packaging Rules - -To generate a plugin compatible with both: - -## Required Layout - -``` -my-plugin/ -│ -├── plugin.json -├── agents/ -├── skills/ -├── commands/ -├── hooks.json -├── .mcp.json -├── .lsp.json -└── .claude-plugin/ - └── plugin.json -``` - -## Build Strategy - -1. Maintain single source metadata file (e.g., `plugin.config.json`) -2. Generate: - - * root `plugin.json` - * `.claude-plugin/plugin.json` -3. Validate: - - * No external file references - * All component directories exist - ---- - -# 9. Validation Checklist - -When loading a plugin: - -* [ ] Plugin directory exists -* [ ] Root `plugin.json` present (Copilot mode) -* [ ] Plugin name is kebab-case -* [ ] All declared component paths exist -* [ ] No directory traversal outside root -* [ ] All Markdown files contain valid YAML frontmatter -* [ ] No duplicate component IDs (handle per mode) -* [ ] JSON files parse successfully - ---- - -# 10. Mode Behavior Summary - -| Feature | Claude | Copilot | -| -------------------- | -------- | ----------------------- | -| Manifest required | No | Yes | -| Namespacing | Explicit | Implicit | -| Duplicate handling | Allowed | First-wins | -| Sandbox enforcement | Strict | Not strictly documented | -| outputStyles support | Yes | No | - ---- - -# 11. Recommended Implementation Order - -1. Filesystem sandbox layer -2. Manifest loader -3. Component discovery engine -4. Markdown frontmatter parser -5. Conflict resolver -6. Lifecycle hook executor -7. MCP/LSP process manager -8. Dual-manifest generator (optional) - ---- - -# 12. Important Behavioral Constraints - -* Do not rely on external filesystem paths -* Do not assume manifest exists in Claude mode -* Do not assume auto-discovery works in Copilot mode -* Do not silently suppress duplicates without logging -* Always namespace internally - ---- - -# 13. Testing Matrix - -Test each plugin in: - -| Scenario | Expected Result | -| ----------------------------- | -------------------------------- | -| Claude without manifest | Auto-discovery works | -| Copilot without manifest | Fail | -| Duplicate agent names | Claude: OK / Copilot: first wins | -| MCP server with external path | Fail | -| Missing component directory | Soft fail if optional | - ---- - -# 14. Final Design Principle - -Treat both systems as: - -> Same plugin architecture with different manifest requirements and collision semantics. - -If you normalize: - -* component parsing -* sandbox enforcement -* namespacing -* manifest loading - -You can support both systems with one shared loader and a small behavior switch for: - -* manifest requirement -* duplicate resolution -* outputStyles handling - ---- - -END OF SPECIFICATION - -``` -``` diff --git a/PLUGINS_LOADING.md b/PLUGINS_LOADING.md deleted file mode 100644 index 5b158037646b7..0000000000000 --- a/PLUGINS_LOADING.md +++ /dev/null @@ -1,388 +0,0 @@ -```markdown -# Plugin Loading Specification -## Folder-Targeted Plugin Resolution for Copilot CLI + Claude Code Compatibility - -This specification defines how the application must: - -1. Discover plugins relevant to a specific target folder -2. Determine applicable scope -3. Load and normalize plugins -4. Resolve conflicts -5. Enforce sandboxing -6. Activate components -7. Handle mode differences (Claude vs Copilot) - -This spec assumes the plugin format described in the Dual Plugin Support Implementation Guide. - ---- - -# 1. Goals - -When the application is pointed at a target folder (project directory), it must: - -- Load all plugins relevant to that folder -- Respect scope precedence -- Support both Claude-style and Copilot-style plugins -- Normalize into a unified internal model -- Avoid collisions and sandbox violations -- Be deterministic and reproducible - ---- - -# 2. Terminology - -| Term | Meaning | -|------|---------| -| Target Folder | The project directory currently being operated on | -| Plugin Root | Directory containing plugin components | -| Mode | `claude` or `copilot` | -| Scope | Installation level: global, user, project, local | -| Normalized Plugin | Internal representation after parsing | - ---- - -# 3. Supported Plugin Scopes - -Plugins may exist at: - -### 3.1 Global Scope -System-wide installation directory -Example: -``` - -/usr/local/share/app/plugins/ - -``` - -### 3.2 User Scope -User-specific directory -Example: -``` - -~/.app/plugins/ - -``` - -### 3.3 Project Scope -Within the target folder -Example: -``` - -/.app/plugins/ - -``` - -### 3.4 Local Explicit Path -Direct path passed by CLI flag - ---- - -# 4. Plugin Discovery Algorithm - -When targeting a folder: - -## 4.1 Resolve Candidate Plugin Directories (Ordered by Precedence) - -1. Explicitly passed plugin paths -2. `/.app/plugins/` -3. `/.claude/plugins/` (Claude compatibility) -4. User plugin directory -5. Global plugin directory - -Within each directory: -- Each subdirectory is treated as a plugin candidate. - ---- - -# 5. Plugin Validity Check - -For each candidate directory: - -### Step 1: Confirm Directory Exists -Must be a directory. - -### Step 2: Detect Compatibility Mode - -If root `plugin.json` exists → Copilot-compatible -If `.claude-plugin/plugin.json` exists → Claude-compatible -If components exist without manifest → Claude-compatible - -If neither manifest nor components found → ignore directory. - -### Step 3: Validate Structure - -- No symlinks escaping plugin root -- No `../` references -- No absolute paths in configs -- All referenced component paths must exist - -If validation fails → reject plugin. - ---- - -# 6. Plugin Loading Order - -Plugins must be loaded in deterministic order: - -``` - -Explicit > Project > User > Global - -``` - -Within each scope: -- Alphabetical order by plugin directory name - -This ensures consistent first-wins semantics in Copilot mode. - ---- - -# 7. Normalization Process - -For each valid plugin: - -## 7.1 Load Manifest - -If in Copilot mode: -- Require root `plugin.json` - -If in Claude mode: -- Load `.claude-plugin/plugin.json` if present -- Otherwise auto-discover components - -## 7.2 Discover Components - -Default paths: -``` - -agents/ -skills/ -commands/ -hooks.json -.mcp.json -.lsp.json - -``` - -If manifest overrides paths: -- Use declared paths instead - -## 7.3 Parse Markdown Components - -For each `.md` file: -- Extract YAML frontmatter -- Require `name` -- Capture description -- Store body as content - -## 7.4 Build Internal Representation - -``` - -NormalizedPlugin { -name -scope -path -metadata -agents[] -skills[] -commands[] -hooks -mcpServers -lspServers -} - -``` - ---- - -# 8. Conflict Resolution Rules - -Conflict resolution differs by mode. - ---- - -## 8.1 Claude Mode - -- All components are namespaced by plugin name. -- No suppression. -- Fully qualified name format: - -``` - -plugin-name/component-name - -``` - -Duplicates across plugins are allowed. - ---- - -## 8.2 Copilot Mode - -Apply first-loaded-wins: - -If component ID already exists: -- Ignore later duplicate -- Log warning - -Applies to: -- agents -- skills -- commands - -Hooks: -- Merge if events differ -- Override if same event key (first wins) - ---- - -# 9. Sandbox Enforcement - -Before activating any plugin: - -- Ensure all file references are inside plugin directory -- Disallow: - - Absolute paths - - Parent directory traversal -- Reject plugin if violation detected - -This is mandatory for Claude compatibility and recommended universally. - ---- - -# 10. Activation Phase - -After normalization and conflict resolution: - -## 10.1 Register Agents -Add to agent registry (namespaced internally). - -## 10.2 Register Skills -Add to skill registry. - -## 10.3 Register Commands -Bind slash or CLI commands. - -## 10.4 Initialize Hooks -Attach to lifecycle event dispatcher. - -## 10.5 Start MCP Servers -Launch as managed child processes. - -## 10.6 Start LSP Servers -Launch per configuration. - ---- - -# 11. Runtime Lifecycle - -When targeting a folder: - -1. Discover plugins -2. Validate and normalize -3. Resolve conflicts -4. Activate components -5. Dispatch `onStart` hooks -6. Process user commands -7. Dispatch lifecycle events -8. On shutdown: - - Stop MCP servers - - Stop LSP servers - - Dispatch `onExit` hooks - ---- - -# 12. Reloading Strategy - -If target folder changes: - -- Unload all project-scope plugins -- Re-run discovery -- Preserve user/global plugins - -If plugin directory changes: -- Require explicit reload -- Do not auto-watch filesystem unless configured - ---- - -# 13. Error Handling Rules - -If a plugin fails validation: -- Log error -- Continue loading others - -If a component file is malformed: -- Skip component -- Do not reject entire plugin - -If manifest missing in Copilot mode: -- Reject plugin - ---- - -# 14. Performance Considerations - -- Cache normalized plugins per folder -- Hash plugin directory contents -- Invalidate cache if hash changes -- Avoid re-parsing Markdown unnecessarily - ---- - -# 15. Determinism Requirements - -The loader must guarantee: - -- Same folder + same plugin directories → identical component registry -- Load order strictly defined -- Conflict handling predictable - ---- - -# 16. Testing Matrix - -| Scenario | Expected Result | -|----------|-----------------| -| Duplicate agent in two global plugins | Copilot: first wins | -| Same duplicate in Claude | Both available | -| Plugin without manifest in Claude | Loads | -| Plugin without manifest in Copilot | Rejected | -| Plugin referencing external file | Rejected | -| Corrupt Markdown frontmatter | Skip component | - ---- - -# 17. Security Requirements - -- No execution of arbitrary shell commands outside MCP/LSP explicitly configured -- Hooks must run in controlled subprocess -- Validate JSON before execution -- Enforce directory isolation - ---- - -# 18. Summary of Mode Differences - -| Feature | Claude | Copilot | -|----------|---------|----------| -| Manifest required | No | Yes | -| Auto-discovery | Yes | No | -| Duplicate handling | Namespaced | First-wins | -| Strict sandbox | Yes | Recommended | - ---- - -# 19. Final Design Principle - -Treat plugin loading as: - -> Scoped, sandboxed, deterministic component aggregation with mode-specific collision semantics. - -All plugins must normalize into a unified internal model before activation. - ---- - -END OF SPECIFICATION -``` diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts index 5cc448db4a35d..d8a25479ebc98 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts @@ -7,9 +7,11 @@ import { CancellationTokenSource } from '../../../../../base/common/cancellation import { localize, localize2 } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputButton, IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IDialogService, IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { dirname } from '../../../../../base/common/resources.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -17,10 +19,15 @@ import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPlu import { IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plugins/pluginMarketplaceService.js'; import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from './chatActions.js'; import { ResourceSet } from '../../../../../base/common/map.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { ChatConfiguration } from '../../common/constants.js'; +import { IConfigurationService, ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; const enum ManagePluginItemKind { Plugin = 'plugin', FindMore = 'findMore', + AddFromFolder = 'addFromFolder', } interface IPluginPickItem extends IQuickPickItem { @@ -32,10 +39,19 @@ interface IFindMorePickItem extends IQuickPickItem { readonly kind: ManagePluginItemKind.FindMore; } +interface IAddFromFolderPickItem extends IQuickPickItem { + readonly kind: ManagePluginItemKind.AddFromFolder; +} + interface IMarketplacePluginPickItem extends IQuickPickItem { marketplacePlugin: IMarketplacePlugin; } +interface IManagePluginsPickResult { + action: 'apply' | 'findMore' | 'addFromFolder'; + selectedPluginItems: IPluginPickItem[]; +} + class ManagePluginsAction extends Action2 { static readonly ID = 'workbench.action.chat.managePlugins'; @@ -57,6 +73,9 @@ class ManagePluginsAction extends Action2 { const quickInputService = accessor.get(IQuickInputService); const labelService = accessor.get(ILabelService); const dialogService = accessor.get(IDialogService); + const fileDialogService = accessor.get(IFileDialogService); + const openerService = accessor.get(IOpenerService); + const configurationService = accessor.get(IConfigurationService); const workspaceContextService = accessor.get(IWorkspaceContextService); const pluginMarketplaceService = accessor.get(IPluginMarketplaceService); @@ -64,14 +83,6 @@ class ManagePluginsAction extends Action2 { const disabledUris = agentPluginService.disabledPluginUris.get(); const hasWorkspace = workspaceContextService.getWorkspace().folders.length > 0; - if (allPlugins.length === 0 && !hasWorkspace) { - dialogService.info( - localize('noPlugins', 'No plugins found.'), - localize('noPluginsDetail', 'There are currently no agent plugins discovered in this workspace.') - ); - return; - } - // Group plugins by parent directory label const groups = new Map(); for (const plugin of allPlugins) { @@ -84,50 +95,86 @@ class ManagePluginsAction extends Action2 { group.push(plugin); } - const items: QuickPickInput[] = []; + const items: QuickPickInput[] = []; + const preselectedPluginItems: IPluginPickItem[] = []; for (const [groupLabel, plugins] of groups) { items.push({ type: 'separator', label: groupLabel }); for (const plugin of plugins) { const pluginName = plugin.uri.path.split('/').at(-1) ?? ''; - items.push({ + const item: IPluginPickItem = { kind: ManagePluginItemKind.Plugin, label: pluginName, plugin, picked: !disabledUris.has(plugin.uri), - } satisfies IPluginPickItem); + }; + if (item.picked) { + preselectedPluginItems.push(item); + } + items.push(item); } } - if (hasWorkspace) { + if (items.length > 0 || hasWorkspace) { items.push({ type: 'separator' }); + } + + if (hasWorkspace) { items.push({ kind: ManagePluginItemKind.FindMore, label: localize('findMorePlugins', 'Find More Plugins...'), + pickable: false, } satisfies IFindMorePickItem); } - const result = await quickInputService.pick( - items, - { - canPickMany: true, - title: localize('managePluginsTitle', 'Manage Plugins'), - placeHolder: localize('managePluginsPlaceholder', 'Choose which plugins are enabled'), - } - ); + items.push({ + kind: ManagePluginItemKind.AddFromFolder, + label: localize('addFromFolder', 'Add from Folder...'), + pickable: false, + } satisfies IAddFromFolderPickItem); + + const result = await showManagePluginsQuickPick(quickInputService, items, preselectedPluginItems); if (!result) { return; } - // Check if "Find More Plugins..." was selected - const findMoreSelected = result.some(item => item.kind === ManagePluginItemKind.FindMore); - if (findMoreSelected) { - await showMarketplaceQuickPick(quickInputService, pluginMarketplaceService, dialogService); + if (result.action === 'findMore') { + await showMarketplaceQuickPick(quickInputService, pluginMarketplaceService, dialogService, openerService); + return; + } + + if (result.action === 'addFromFolder') { + const selectedUris = await fileDialogService.showOpenDialog({ + title: localize('pickPluginFolderTitle', 'Pick Plugin Folder'), + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: workspaceContextService.getWorkspace().folders[0]?.uri, + }); + + const folderUri = selectedUris?.[0]; + if (!folderUri) { + return; + } + + const currentPaths = configurationService.getValue(ChatConfiguration.PluginPaths) ?? []; + const normalizedPaths = currentPaths.filter((path): path is string => typeof path === 'string' && path.length > 0); + const nextPath = folderUri.fsPath; + if (!normalizedPaths.includes(nextPath)) { + await configurationService.updateValue(ChatConfiguration.PluginPaths, [...normalizedPaths, nextPath], ConfigurationTarget.USER_LOCAL); + } return; } - const pluginResults = result.filter((item): item is IPluginPickItem => item.kind === ManagePluginItemKind.Plugin); - const enabledUris = new ResourceSet(pluginResults.map(i => i.plugin.uri)); + if (allPlugins.length === 0) { + dialogService.info( + localize('noPlugins', 'No plugins found.'), + localize('noPluginsDetail', 'There are currently no agent plugins discovered in this workspace.') + ); + return; + } + + const enabledUris = new ResourceSet(result.selectedPluginItems.map(i => i.plugin.uri)); for (const plugin of allPlugins) { const wasDisabled = disabledUris.has(plugin.uri); const isNowEnabled = enabledUris.has(plugin.uri); @@ -141,19 +188,89 @@ class ManagePluginsAction extends Action2 { } } +async function showManagePluginsQuickPick( + quickInputService: IQuickInputService, + items: QuickPickInput[], + preselectedPluginItems: IPluginPickItem[] +): Promise { + const quickPick = quickInputService.createQuickPick({ useSeparators: true }); + const disposables = new DisposableStore(); + disposables.add(quickPick); + + quickPick.canSelectMany = true; + quickPick.title = localize('managePluginsTitle', 'Manage Plugins'); + quickPick.placeholder = localize('managePluginsPlaceholder', 'Choose which plugins are enabled'); + quickPick.items = items; + quickPick.selectedItems = preselectedPluginItems; + + const result = await new Promise(resolve => { + let resolved = false; + + const complete = (value: IManagePluginsPickResult | undefined) => { + if (resolved) { + return; + } + resolved = true; + resolve(value); + }; + + disposables.add(quickPick.onDidAccept(() => { + const activeItem = quickPick.activeItems[0]; + if (activeItem?.kind === ManagePluginItemKind.FindMore) { + complete({ + action: 'findMore', + selectedPluginItems: [], + }); + quickPick.hide(); + return; + } + + if (activeItem?.kind === ManagePluginItemKind.AddFromFolder) { + complete({ + action: 'addFromFolder', + selectedPluginItems: [], + }); + quickPick.hide(); + return; + } + + complete({ + action: 'apply', + selectedPluginItems: quickPick.selectedItems.filter((item): item is IPluginPickItem => item.kind === ManagePluginItemKind.Plugin), + }); + quickPick.hide(); + })); + + disposables.add(quickPick.onDidHide(() => { + complete(undefined); + disposables.dispose(); + })); + + quickPick.show(); + }); + return result; +} + async function showMarketplaceQuickPick( quickInputService: IQuickInputService, pluginMarketplaceService: IPluginMarketplaceService, dialogService: IDialogService, + openerService: IOpenerService, ): Promise { const quickPick = quickInputService.createQuickPick({ useSeparators: true }); + const disposables = new DisposableStore(); + disposables.add(quickPick); + const openReadmeButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.book), + tooltip: localize('openPluginReadme', 'Open README on GitHub'), + }; quickPick.title = localize('marketplaceTitle', 'Plugin Marketplace'); quickPick.placeholder = localize('marketplacePlaceholder', 'Select a plugin to install'); quickPick.busy = true; quickPick.show(); const cts = new CancellationTokenSource(); - quickPick.onDidHide(() => cts.dispose(true)); + disposables.add(cts); try { const plugins = await pluginMarketplaceService.fetchMarketplacePlugins(cts.token); @@ -189,6 +306,7 @@ async function showMarketplaceQuickPick( detail: plugin.description, description: plugin.version, marketplacePlugin: plugin, + buttons: plugin.readmeUri ? [openReadmeButton] : undefined, }); } } @@ -201,12 +319,22 @@ async function showMarketplaceQuickPick( return; } + disposables.add(quickPick.onDidTriggerItemButton(e => { + if (e.button !== openReadmeButton || !e.item.marketplacePlugin.readmeUri) { + return; + } + void openerService.open(e.item.marketplacePlugin.readmeUri); + })); + const selection = await new Promise(resolve => { - quickPick.onDidAccept(() => { + disposables.add(quickPick.onDidAccept(() => { resolve(quickPick.selectedItems[0]); quickPick.hide(); - }); - quickPick.onDidHide(() => resolve(undefined)); + })); + disposables.add(quickPick.onDidHide(() => { + resolve(undefined); + disposables.dispose(); + })); }); if (selection) { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d4a1f1980219f..ec53e32d5f044 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1554,7 +1554,6 @@ registerEditorFeature(ChatPasteProvidersFeature); agentPluginDiscoveryRegistry.register(new SyncDescriptor(ConfiguredAgentPluginDiscovery)); - registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index ab71ab3799bc3..553da59465314 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -5,6 +5,7 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IObservable } from '../../../../../base/common/observable.js'; +import { basename } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { SyncDescriptor0 } from '../../../../../platform/instantiation/common/descriptors.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -24,6 +25,13 @@ export interface IAgentPluginCommand { readonly content: string; } +export interface IAgentPluginSkill { + readonly uri: URI; + readonly name: string; + readonly description?: string; + readonly content: string; +} + export interface IAgentPluginMcpServerDefinition { readonly name: string; readonly configuration: IMcpServerConfiguration; @@ -33,6 +41,7 @@ export interface IAgentPlugin { readonly uri: URI; readonly hooks: IObservable; readonly commands: IObservable; + readonly skills: IObservable; readonly mcpServerDefinitions: IObservable; } @@ -50,13 +59,9 @@ export interface IAgentPluginDiscovery extends IDisposable { } export function getCanonicalPluginCommandId(plugin: IAgentPlugin, commandName: string): string { - const pluginSegment = plugin.uri.path.split('/').at(-1) ?? ''; + const pluginSegment = basename(plugin.uri); const prefix = normalizePluginToken(pluginSegment); const normalizedCommand = normalizePluginToken(commandName); - if (!prefix || !normalizedCommand) { - return ''; - } - if (normalizedCommand.startsWith(`${prefix}:`)) { return normalizedCommand; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 6121c2d067503..a709199c8b224 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -8,7 +8,10 @@ import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { autorun, derived, IObservable, observableValue } from '../../../../../base/common/observable.js'; -import { extname, joinPath } from '../../../../../base/common/resources.js'; +import { + basename, + extname, joinPath +} from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -19,9 +22,11 @@ import { observableConfigValue } from '../../../../../platform/observable/common import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ChatConfiguration } from '../constants.js'; import { PromptFileParser } from '../promptSyntax/promptFileParser.js'; -import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService } from './agentPluginService.js'; +import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; const STORAGE_KEY = 'workbench.chat.plugins.disabled'; +const COMMAND_FILE_SUFFIX = '.md'; + const disabledPluginUrisMemento = observableMemento>({ key: STORAGE_KEY, defaultValue: new ResourceSet(), @@ -193,34 +198,46 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent const store = this._register(new DisposableStore()); const commands = observableValue('agentPluginCommands', []); + const skills = observableValue('agentPluginSkills', []); const mcpServerDefinitions = observableValue('agentPluginMcpServerDefinitions', []); const plugin: IAgentPlugin = { uri, hooks: observableValue('agentPluginHooks', []), commands, + skills, mcpServerDefinitions, }; - const scheduler = store.add(new RunOnceScheduler(() => { - void (async () => { - const [nextCommands, nextMcpDefinitions] = await Promise.all([ - this._readCommands(uri), - this._readMcpDefinitions(uri), - ]); - if (!store.isDisposed) { - commands.set(nextCommands, undefined); - mcpServerDefinitions.set(nextMcpDefinitions, undefined); - } - })(); + const commandsDir = joinPath(uri, 'commands'); + const skillsDir = joinPath(uri, 'skills'); + + const commandsScheduler = store.add(new RunOnceScheduler(async () => { + commands.set(await this._readCommands(uri), undefined); + }, 200)); + const skillsScheduler = store.add(new RunOnceScheduler(async () => { + skills.set(await this._readSkills(uri), undefined); + }, 200)); + const mcpScheduler = store.add(new RunOnceScheduler(async () => { + mcpServerDefinitions.set(await this._readMcpDefinitions(uri), undefined); }, 200)); store.add(this._fileService.watch(uri, { recursive: true, excludes: [] })); store.add(this._fileService.onDidFilesChange(e => { - if (e.affects(uri)) { - scheduler.schedule(); + if (e.affects(commandsDir)) { + commandsScheduler.schedule(); + } + if (e.affects(skillsDir)) { + skillsScheduler.schedule(); + } + // MCP definitions come from .mcp.json, plugin.json, or .claude-plugin/plugin.json + if (e.affects(joinPath(uri, '.mcp.json')) || e.affects(joinPath(uri, 'plugin.json')) || e.affects(joinPath(uri, '.claude-plugin'))) { + mcpScheduler.schedule(); } })); - scheduler.schedule(); + + commandsScheduler.schedule(); + skillsScheduler.schedule(); + mcpScheduler.schedule(); this._pluginEntries.set(key, { plugin, store }); return plugin; @@ -362,6 +379,48 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent } } + private async _readSkills(uri: URI): Promise { + const skillsDir = joinPath(uri, 'skills'); + let stat; + try { + stat = await this._fileService.resolve(skillsDir); + } catch { + return []; + } + + if (!stat.isDirectory || !stat.children) { + return []; + } + + const parser = new PromptFileParser(); + const skills: IAgentPluginSkill[] = []; + for (const child of stat.children) { + if (!child.isFile || extname(child.resource).toLowerCase() !== COMMAND_FILE_SUFFIX) { + continue; + } + + let fileContents; + try { + fileContents = await this._fileService.readFile(child.resource); + } catch { + continue; + } + + const parsed = parser.parse(child.resource, fileContents.value.toString()); + const name = parsed.header?.name?.trim() || basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); + + skills.push({ + uri: child.resource, + name, + description: parsed.header?.description, + content: parsed.body?.getContent()?.trim() ?? '', + }); + } + + skills.sort((a, b) => a.name.localeCompare(b.name)); + return skills; + } + private async _readCommands(uri: URI): Promise { const commandsDir = joinPath(uri, 'commands'); let stat; @@ -378,7 +437,7 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent const parser = new PromptFileParser(); const commands: IAgentPluginCommand[] = []; for (const child of stat.children) { - if (!child.isFile || extname(child.resource) !== '.md') { + if (!child.isFile || extname(child.resource).toLowerCase() !== COMMAND_FILE_SUFFIX) { continue; } @@ -390,10 +449,7 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent } const parsed = parser.parse(child.resource, fileContents.value.toString()); - const name = parsed.header?.name?.trim(); - if (!name) { - continue; - } + const name = parsed.header?.name?.trim() || basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); commands.push({ uri: child.resource, diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index d4cd8217a282b..67382d1d1e24f 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../base/common/uri.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { asJson, IRequestService } from '../../../../../platform/request/common/request.js'; @@ -16,6 +17,7 @@ export interface IMarketplacePlugin { readonly version: string; readonly source: string; readonly marketplace: string; + readonly readmeUri?: URI; } interface IMarketplaceJson { @@ -80,13 +82,17 @@ export class PluginMarketplaceService implements IPluginMarketplaceService { .filter((p): p is { name: string; description: string; version: string; source: string } => typeof p.name === 'string' && !!p.name ) - .map(p => ({ - name: p.name, - description: p.description ?? '', - version: p.version ?? '', - source: p.source ?? '', - marketplace: repo, - })); + .map(p => { + const source = p.source ?? ''; + return { + name: p.name, + description: p.description ?? '', + version: p.version ?? '', + source, + marketplace: repo, + readmeUri: getMarketplaceReadmeUri(repo, source), + }; + }); } catch (err) { this._logService.debug(`[PluginMarketplaceService] Failed to fetch marketplace.json from ${url}:`, err); continue; @@ -96,3 +102,9 @@ export class PluginMarketplaceService implements IPluginMarketplaceService { return []; } } + +function getMarketplaceReadmeUri(repo: string, source: string): URI { + const normalizedSource = source.trim().replace(/^\/+|\/+$/g, ''); + const readmePath = normalizedSource ? `${normalizedSource}/README.md` : 'README.md'; + return URI.parse(`https://github.com/${repo}/blob/main/${readmePath}`); +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 4c685dce1f379..daabef3b34f16 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -8,7 +8,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { parse as parseJSONC } from '../../../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../../../base/common/observable.js'; +import { autorun, IReader } from '../../../../../../base/common/observable.js'; import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; import { basename, dirname, isEqual, joinPath } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -42,7 +42,7 @@ import { IWorkspaceContextService } from '../../../../../../platform/workspace/c import { IPathService } from '../../../../../services/path/common/pathService.js'; import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptValidator.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { getCanonicalPluginCommandId, IAgentPluginService } from '../../plugins/agentPluginService.js'; +import { getCanonicalPluginCommandId, IAgentPlugin, IAgentPluginService } from '../../plugins/agentPluginService.js'; /** * Error thrown when a skill file is missing the required name attribute. @@ -142,7 +142,7 @@ export class PromptsService extends Disposable implements IPromptsService { private readonly _contributedWhenKeys = new Set(); private readonly _contributedWhenClauses = new Map(); private readonly _onDidContributedWhenChange = this._register(new Emitter()); - private _pluginPromptFiles: readonly ILocalPromptPath[] = []; + private _pluginPromptFilesByType = new Map(); constructor( @ILogService public readonly logger: ILogService, @@ -210,35 +210,38 @@ export class PromptsService extends Disposable implements IPromptsService { this._register(this.cachedSkills.onDidChange(() => { })); this._register(this.cachedHooks.onDidChange(() => { })); - this._register(this.watchPluginPromptFiles()); + this._register(this.watchPluginPromptFilesForType( + PromptsType.prompt, + (plugin, reader) => plugin.commands.read(reader), + )); + this._register(this.watchPluginPromptFilesForType( + PromptsType.skill, + (plugin, reader) => plugin.skills.read(reader), + )); } - private watchPluginPromptFiles() { + private watchPluginPromptFilesForType( + type: PromptsType, + getItems: (plugin: IAgentPlugin, reader: IReader) => readonly { uri: URI; name: string; description?: string }[], + ) { return autorun(reader => { const plugins = this.agentPluginService.plugins.read(reader); - const nextPromptFiles: ILocalPromptPath[] = []; - const seen = new Set(); + const nextFiles: ILocalPromptPath[] = []; for (const plugin of plugins) { - for (const pluginCommand of plugin.commands.read(reader)) { - const commandId = getCanonicalPluginCommandId(plugin, pluginCommand.name); - if (!commandId || seen.has(commandId)) { - continue; - } - - seen.add(commandId); - nextPromptFiles.push({ - uri: pluginCommand.uri, + for (const item of getItems(plugin, reader)) { + nextFiles.push({ + uri: item.uri, storage: PromptsStorage.local, - type: PromptsType.prompt, - name: commandId, - description: pluginCommand.description, + type, + name: getCanonicalPluginCommandId(plugin, item.name), + description: item.description, }); } } - nextPromptFiles.sort((a, b) => `${a.name ?? ''}|${a.uri.toString()}`.localeCompare(`${b.name ?? ''}|${b.uri.toString()}`)); - this._pluginPromptFiles = nextPromptFiles; - this.invalidatePromptFileCache(PromptsType.prompt); + nextFiles.sort((a, b) => `${a.name ?? ''}|${a.uri.toString()}`.localeCompare(`${b.name ?? ''}|${b.uri.toString()}`)); + this._pluginPromptFilesByType.set(type, nextFiles); + this.invalidatePromptFileCache(type); }); } @@ -287,7 +290,7 @@ export class PromptsService extends Disposable implements IPromptsService { this.fileLocator.listFiles(type, PromptsStorage.user, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.user, type } satisfies IUserPromptPath))), this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath))), this.getExtensionPromptFiles(type, token), - this._pluginPromptFiles, + this._pluginPromptFilesByType.get(type) ?? [], ]); return [...prompts.flat()]; @@ -540,7 +543,7 @@ export class PromptsService extends Disposable implements IPromptsService { } public isValidSlashCommandName(command: string): boolean { - return command.match(/^[\p{L}\d_\-\.]+$/u) !== null; + return command.match(/^[\p{L}\d_\-\.:]+$/u) !== null; } public async resolvePromptSlashCommand(name: string, token: CancellationToken): Promise { @@ -550,7 +553,7 @@ export class PromptsService extends Disposable implements IPromptsService { private asChatPromptSlashCommand(parsedPromptFile: ParsedPromptFile, promptPath: IPromptPath): IChatPromptSlashCommand { let name = parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(promptPath.uri); - name = name.replace(/[^\p{L}\d_\-\.]+/gu, '-'); // replace spaces with dashes + name = name.replace(/[^\p{L}\d_\-\.:]+/gu, '-'); // replace spaces with dashes return { name: name, description: parsedPromptFile?.header?.description ?? promptPath.description, From cfd0d61d6687873ecc2f2a05bd2ad420b2801d46 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:07:09 -0800 Subject: [PATCH 07/28] Try cleaning up api around acquiring/loading/creating sessions We have a number of methods that have fairly similar names (get, load, acquire, start). This change tries to align these names a bit more so it's easier to understand which one to use --- .../chat/browser/branchChatSessionAction.ts | 5 +- .../browser/sessionsManagementService.ts | 4 +- .../api/browser/mainThreadChatSessions.ts | 4 +- .../chat/browser/actions/chatForkActions.ts | 7 +- .../chat/browser/actions/chatImportExport.ts | 5 +- .../agentSessions/agentSessionHoverWidget.ts | 2 +- .../chatSessions/chatSessions.contribution.ts | 2 +- .../chat/browser/widgetHosts/chatQuick.ts | 2 +- .../widgetHosts/editor/chatEditorInput.ts | 8 +-- .../widgetHosts/viewPane/chatViewPane.ts | 15 ++--- .../chat/common/chatService/chatService.ts | 42 ++++++++---- .../common/chatService/chatServiceImpl.ts | 64 ++++++++++--------- .../contrib/chat/common/model/chatModel.ts | 4 +- .../chat/common/model/chatModelStore.ts | 4 +- .../localAgentSessionsController.test.ts | 12 ++-- .../chatEditing/chatEditingService.test.ts | 12 ++-- .../common/chatService/chatService.test.ts | 15 +++-- .../common/chatService/mockChatService.ts | 12 ++-- .../browser/inlineChatController.ts | 4 +- .../browser/inlineChatSessionService.ts | 2 +- .../browser/inlineChatSessionServiceImpl.ts | 2 +- .../chat/browser/terminalChatWidget.ts | 2 +- .../browser/agentSessionsWelcome.ts | 6 +- 23 files changed, 124 insertions(+), 111 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/branchChatSessionAction.ts b/src/vs/sessions/contrib/chat/browser/branchChatSessionAction.ts index f5575ce5a9bb8..6766f48b2b419 100644 --- a/src/vs/sessions/contrib/chat/browser/branchChatSessionAction.ts +++ b/src/vs/sessions/contrib/chat/browser/branchChatSessionAction.ts @@ -101,10 +101,7 @@ export class BranchChatSessionAction extends Action2 { } // Load the branched data into a new session model - const modelRef = chatService.loadSessionFromContent(serializedData); - if (!modelRef) { - return; - } + const modelRef = chatService.loadSessionFromData(serializedData); // Open the branched session in the chat view pane await widgetService.openSession(modelRef.object.sessionResource, ChatViewPaneTarget); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index d3a7e96111910..8d4124b8b940a 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -307,7 +307,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa * Open a new remote session - load the model first, then show it in the ChatViewPane. */ private async openNewRemoteSession(sessionResource: URI): Promise { - const modelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + const modelRef = await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); if (!chatWidget?.viewModel) { this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`); @@ -372,7 +372,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa // 2. Apply selected options (repository, branch, etc.) to the contributed session if (selectedOptions && selectedOptions.size > 0) { - const modelRef = this.chatService.getActiveSessionReference(sessionResource); + const modelRef = this.chatService.acquireExistingSession(sessionResource); if (modelRef) { const model = modelRef.object; const contributedSession = model.contributedChatSession; diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index cd45ead9ff57d..a683f75dfb8f3 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -529,7 +529,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat } const originalEditor = this._editorService.editors.find(editor => editor.resource?.toString() === originalResource.toString()); - const originalModel = this._chatService.getActiveSessionReference(originalResource); + const originalModel = this._chatService.acquireExistingSession(originalResource); const contribution = this._chatSessionsService.getAllChatSessionContributions().find(c => c.type === chatSessionType); try { @@ -584,7 +584,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat await this._chatWidgetService.openSession(modifiedResource, undefined, { preserveFocus: true }); } else { // Loading the session to ensure the session is created and editing session is transferred. - const ref = await this._chatService.loadSessionForResource(modifiedResource, ChatAgentLocation.Chat, CancellationToken.None); + const ref = await this._chatService.acquireOrLoadSession(modifiedResource, ChatAgentLocation.Chat, CancellationToken.None); ref?.dispose(); } } finally { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts index 1521270c59bec..fd008ea10c9f4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatForkActions.ts @@ -78,10 +78,7 @@ export function registerChatForkActions() { } } - const modelRef = chatService.loadSessionFromContent(cleanData); - if (!modelRef) { - return; - } + const modelRef = chatService.loadSessionFromData(cleanData); // Defer navigation until after the slash command flow completes. const newSessionResource = modelRef.object.sessionResource; @@ -177,7 +174,7 @@ export function registerChatForkActions() { } } - const modelRef = chatService.loadSessionFromContent(forkedData); + const modelRef = chatService.loadSessionFromData(forkedData); if (!modelRef) { return; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts index 7f350b188d71f..059f2febb5c6a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatImportExport.ts @@ -123,10 +123,7 @@ export function registerChatExportActions() { let options: IChatEditorOptions; if (opts?.target === 'chatViewPane') { - const modelRef = chatService.loadSessionFromContent(data); - if (!modelRef) { - return; - } + const modelRef = chatService.loadSessionFromData(data); sessionResource = modelRef.object.sessionResource; resolvedTarget = ChatViewPaneTarget; options = { pinned: true }; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts index dbe4495753a7b..27aaaa4f88f1b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -80,7 +80,7 @@ export class AgentSessionHoverWidget extends Disposable { } private async loadModel() { - const modelRef = await this.chatService.loadSessionForResource(this.session.resource, ChatAgentLocation.Chat, this.cts.token); + const modelRef = await this.chatService.acquireOrLoadSession(this.session.resource, ChatAgentLocation.Chat, this.cts.token); if (this._store.isDisposed) { modelRef?.dispose(); return; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index ada489b06acfe..deff82e869367 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -575,7 +575,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ if (chatOptions) { const resource = URI.revive(chatOptions.resource); - const ref = await chatService.loadSessionForResource(resource, ChatAgentLocation.Chat, CancellationToken.None); + const ref = await chatService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None); try { const result = await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext }); if (result.kind === 'queued') { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts index fda57a33cdce0..c6a61b981522f 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts @@ -394,7 +394,7 @@ class QuickChat extends Disposable { } private updateModel(): void { - this.modelRef ??= this.chatService.startSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true }); + this.modelRef ??= this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true }); const model = this.modelRef?.object; if (!model) { throw new Error('Could not start chat session'); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts index 9653b59d817b9..47214b2d68d4e 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts @@ -211,16 +211,16 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler const inputType = chatSessionType ?? this.resource.authority; if (this._sessionResource) { - this.modelRef.value = await this.chatService.loadSessionForResource(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + this.modelRef.value = await this.chatService.acquireOrLoadSession(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None); // For local session only, if we find no existing session, create a new one if (!this.model && LocalChatSessionUri.parseLocalSessionId(this._sessionResource)) { - this.modelRef.value = this.chatService.startSession(ChatAgentLocation.Chat, { canUseTools: true }); + this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: true }); } } else if (!this.options.target) { - this.modelRef.value = this.chatService.startSession(ChatAgentLocation.Chat, { canUseTools: !inputType }); + this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: !inputType }); } else if (this.options.target.data) { - this.modelRef.value = this.chatService.loadSessionFromContent(this.options.target.data); + this.modelRef.value = this.chatService.loadSessionFromData(this.options.target.data); } if (!this.model || this.isDisposed()) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 9ff8c08231977..19519ce7abbfb 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -248,7 +248,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { if (!this._widget?.viewModel && !this.restoringSession) { const sessionResource = this.getTransferredOrPersistedSessionInfo(); this.restoringSession = - (sessionResource ? this.chatService.getOrRestoreSession(sessionResource) : Promise.resolve(undefined)).then(async modelRef => { + (sessionResource ? this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None) : Promise.resolve(undefined)).then(async modelRef => { if (!this._widget) { return; // renderBody has not been called yet } @@ -675,7 +675,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private async _applyModel(): Promise { const sessionResource = this.getTransferredOrPersistedSessionInfo(); - const modelRef = sessionResource ? await this.chatService.getOrRestoreSession(sessionResource) : undefined; + const modelRef = sessionResource ? await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None) : undefined; await this.showModel(modelRef); } @@ -686,8 +686,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let ref: IChatModelReference | undefined; if (startNewSession) { ref = modelRef ?? (this.chatService.transferredSessionResource - ? await this.chatService.getOrRestoreSession(this.chatService.transferredSessionResource) - : this.chatService.startSession(ChatAgentLocation.Chat)); + ? await this.chatService.acquireOrLoadSession(this.chatService.transferredSessionResource, ChatAgentLocation.Chat, CancellationToken.None) + : this.chatService.startNewLocalSession(ChatAgentLocation.Chat)); if (!ref) { throw new Error('Could not start chat session'); } @@ -773,12 +773,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { }, 100); try { - const sessionType = getChatSessionType(sessionResource); - if (sessionType !== localChatSessionType) { - await this.chatSessionsService.canResolveChatSession(sessionResource); - } - - const newModelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + const newModelRef = await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); clearWidget.dispose(); await queue; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index a826cd3b65816..654eaa98fb43c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1354,25 +1354,46 @@ export interface IChatService { */ readonly chatModels: IObservable>; + readonly editingSessions: readonly IChatEditingSession[]; + isEnabled(location: ChatAgentLocation): boolean; + hasSessions(): boolean; - startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference; + + /** + * Starts a new chat session at the given location. + * + * @returns A reference to the session's model. + */ + startNewLocalSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference; /** * Get an active session without holding a reference to it. + * + * @returns The session's model, or undefined if no active session exists. */ getSession(sessionResource: URI): IChatModel | undefined; /** * Acquire a reference to an active session. + * + * @returns A reference to the session's model or undefined if there is no active session for the given resource. */ - getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined; + acquireExistingSession(sessionResource: URI): IChatModelReference | undefined; + + /** + * Tries to acquire an existing a chat session for the resource. If no session exists, tries to load one for the given + * session resource and location. This may load the session from an external provider. + * + * @returns A reference to the session's model, or undefined if the session could not be loaded + */ + acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; + + /** + * Loads a session from exported chat data + */ + loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference; - getOrRestoreSession(sessionResource: URI): Promise; - getSessionTitle(sessionResource: URI): string | undefined; - loadSessionFromContent(data: IExportableChatData | ISerializableChatData | URI): IChatModelReference | undefined; - loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; - readonly editingSessions: IChatEditingSession[]; getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined; /** @@ -1381,10 +1402,9 @@ export interface IChatService { */ sendRequest(sessionResource: URI, message: string, options?: IChatSendRequestOptions): Promise; - /** - * Sets a custom title for a chat model. - */ - setTitle(sessionResource: URI, title: string): void; + getSessionTitle(sessionResource: URI): string | undefined; + setSessionTitle(sessionResource: URI, title: string): void; + appendProgress(request: IChatRequestModel, progress: IChatProgress): void; resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index e2b5b29c409fe..7b937fb65fb8b 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -356,7 +356,7 @@ export class ChatService extends Disposable implements IChatService { } sessionResource ??= LocalChatSessionUri.forSession(session.sessionId); - const sessionRef = await this.getOrRestoreSession(sessionResource); + const sessionRef = await this.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); if (sessionRef?.object.editingSession) { await chatEditingSessionIsReady(sessionRef.object.editingSession); // the session will hold a self-reference as long as there are modified files @@ -444,10 +444,9 @@ export class ChatService extends Disposable implements IChatService { await this._chatSessionStore.clearAllSessions(); } - startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { + startNewLocalSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { this.trace('startSession'); - const sessionId = generateUuid(); - const sessionResource = LocalChatSessionUri.forSession(sessionId); + const sessionResource = LocalChatSessionUri.forSession(generateUuid()); return this._sessionModels.acquireOrCreate({ initialData: undefined, location, @@ -506,13 +505,13 @@ export class ChatService extends Disposable implements IChatService { return this._sessionModels.get(sessionResource); } - getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined { + acquireExistingSession(sessionResource: URI): IChatModelReference | undefined { return this._sessionModels.acquireExisting(sessionResource); } - async getOrRestoreSession(sessionResource: URI): Promise { - this.trace('getOrRestoreSession', `${sessionResource}`); - const existingRef = this._sessionModels.acquireExisting(sessionResource); + private async acquireOrRestoreLocalSession(sessionResource: URI): Promise { + this.trace('acquireOrRestoreSession', `${sessionResource}`); + const existingRef = this.acquireExistingSession(sessionResource); if (existingRef) { return existingRef; } @@ -525,8 +524,6 @@ export class ChatService extends Disposable implements IChatService { const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); if (localSessionId) { sessionData = await this._chatSessionStore.readSession(localSessionId); - } else { - return this.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); } } @@ -556,7 +553,7 @@ export class ChatService extends Disposable implements IChatService { this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title; } - loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModelReference | undefined { + loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference { const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid(); const sessionResource = LocalChatSessionUri.forSession(sessionId); return this._sessionModels.acquireOrCreate({ @@ -567,43 +564,52 @@ export class ChatService extends Disposable implements IChatService { }); } - async loadSessionForResource(chatSessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { - // TODO: Move this into a new ChatModelService - - if (chatSessionResource.scheme === Schemas.vscodeLocalChatSession) { - return this.getOrRestoreSession(chatSessionResource); + async acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { + if (sessionResource.scheme === Schemas.vscodeLocalChatSession) { + return this.acquireOrRestoreLocalSession(sessionResource); + } else { + return this.loadRemoteSession(sessionResource, location, token); } + } - const existingRef = this._sessionModels.acquireExisting(chatSessionResource); - if (existingRef) { - return existingRef; + private async loadRemoteSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { + await this.chatSessionService.canResolveChatSession(sessionResource); + + // Check if session already exists + { + const existingRef = this.acquireExistingSession(sessionResource); + if (existingRef) { + return existingRef; + } } - const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None); + const providedSession = await this.chatSessionService.getOrCreateChatSession(sessionResource, token); // Make sure we haven't created this in the meantime - const existingRefAfterProvision = this._sessionModels.acquireExisting(chatSessionResource); - if (existingRefAfterProvision) { - providedSession.dispose(); - return existingRefAfterProvision; + { + const existingRef = this.acquireExistingSession(sessionResource); + if (existingRef) { + providedSession.dispose(); + return existingRef; + } } - const chatSessionType = chatSessionResource.scheme; + const chatSessionType = sessionResource.scheme; // Contributed sessions do not use UI tools const modelRef = this._sessionModels.acquireOrCreate({ initialData: undefined, location, - sessionResource: chatSessionResource, + sessionResource: sessionResource, canUseTools: false, transferEditingSession: providedSession.transferredState?.editingSession, inputState: providedSession.transferredState?.inputState, }); modelRef.object.setContributedChatSession({ - chatSessionResource, + chatSessionResource: sessionResource, chatSessionType, - isUntitled: chatSessionResource.path.startsWith('/untitled-') //TODO(jospicer) + isUntitled: sessionResource.path.startsWith('/untitled-') //TODO(jospicer) }); const model = modelRef.object; @@ -1489,7 +1495,7 @@ export class ChatService extends Disposable implements IChatService { this._chatSessionStore.logIndex(); } - setTitle(sessionResource: URI, title: string): void { + setSessionTitle(sessionResource: URI, title: string): void { this._sessionModels.get(sessionResource)?.setCustomTitle(title); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index c5d858de46c67..a85a969c0b2f0 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -2214,7 +2214,7 @@ export class ChatModel extends Disposable implements IChatModel { const needsInput = this.requestNeedsInput.read(r); const shouldStayAlive = inProgress || !!needsInput; if (shouldStayAlive && !selfRef.value) { - selfRef.value = chatService.getActiveSessionReference(this._sessionResource); + selfRef.value = chatService.acquireExistingSession(this._sessionResource); } else if (!shouldStayAlive && selfRef.value) { selfRef.clear(); } @@ -2238,7 +2238,7 @@ export class ChatModel extends Disposable implements IChatModel { this._register(autorun(r => { const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified); if (hasModified && !selfRef.value) { - selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource); + selfRef.value = this.chatService.acquireExistingSession(this._sessionResource); } else if (!hasModified && selfRef.value) { selfRef.clear(); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts index 588be79a82851..24bd5cd826325 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable, IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js'; +import { Disposable, IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js'; import { ObservableMap } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; @@ -27,7 +27,7 @@ export interface ChatModelStoreDelegate { willDisposeModel: (model: ChatModel) => Promise; } -export class ChatModelStore extends Disposable implements IDisposable { +export class ChatModelStore extends Disposable { private readonly _refCollection: ReferenceCollection; private readonly _models = new ObservableMap(); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index 2cd794010cb0f..8113041814474 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -98,7 +98,7 @@ class MockChatService implements IChatService { return []; } - startSession(_location: ChatAgentLocation, _options?: IChatSessionStartOptions): any { + startNewLocalSession(_location: ChatAgentLocation, _options?: IChatSessionStartOptions): any { throw new Error('Method not implemented.'); } @@ -110,7 +110,7 @@ class MockChatService implements IChatService { return undefined; } - getOrRestoreSession(_sessionResource: URI): Promise { + acquireOrRestoreSession(_sessionResource: URI): Promise { throw new Error('Method not implemented.'); } @@ -118,19 +118,19 @@ class MockChatService implements IChatService { return undefined; } - loadSessionFromContent(_data: any): any { + loadSessionFromData(_data: any): any { throw new Error('Method not implemented.'); } - loadSessionForResource(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise { + acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - getActiveSessionReference(_sessionResource: URI): any { + acquireExistingSession(_sessionResource: URI): any { return undefined; } - setTitle(_sessionResource: URI, _title: string): void { } + setSessionTitle(_sessionResource: URI, _title: string): void { } appendProgress(_request: IChatRequestModel, _progress: any): void { } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts index 0243dcfdd0d97..adf87b80a5781 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts @@ -147,7 +147,7 @@ suite('ChatEditingService', function () { test('create session', async function () { assert.ok(editingService); - const modelRef = chatService.startSession(ChatAgentLocation.EditorInline); + const modelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); const model = modelRef.object as ChatModel; const session = editingService.createEditingSession(model, true); @@ -168,7 +168,7 @@ suite('ChatEditingService', function () { const uri = URI.from({ scheme: 'test', path: 'HelloWorld' }); - const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const modelRef = store.add(chatService.startNewLocalSession(ChatAgentLocation.Chat)); const model = modelRef.object as ChatModel; const session = model.editingSession; if (!session) { @@ -225,7 +225,7 @@ suite('ChatEditingService', function () { const uri = URI.from({ scheme: 'test', path: 'abc\n' }); - const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const modelRef = store.add(chatService.startNewLocalSession(ChatAgentLocation.Chat)); const model = modelRef.object as ChatModel; const session = model.editingSession; assertType(session, 'session not created'); @@ -259,7 +259,7 @@ suite('ChatEditingService', function () { const uri = URI.from({ scheme: 'test', path: 'abc\n' }); - const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const modelRef = store.add(chatService.startNewLocalSession(ChatAgentLocation.Chat)); const model = modelRef.object as ChatModel; const session = model.editingSession; assertType(session, 'session not created'); @@ -293,7 +293,7 @@ suite('ChatEditingService', function () { const uri = URI.from({ scheme: 'test', path: 'abc\n' }); - const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const modelRef = store.add(chatService.startNewLocalSession(ChatAgentLocation.Chat)); const model = modelRef.object as ChatModel; const session = model.editingSession; assertType(session, 'session not created'); @@ -329,7 +329,7 @@ suite('ChatEditingService', function () { const modified = store.add(await textModelService.createModelReference(uri)).object.textEditorModel; - const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const modelRef = store.add(chatService.startNewLocalSession(ChatAgentLocation.Chat)); const model = modelRef.object as ChatModel; const session = model.editingSession; assertType(session, 'session not created'); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 15a5add31964f..ca738af3fb20c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -49,6 +49,7 @@ import { MockChatService } from './mockChatService.js'; import { MockChatVariablesService } from '../mockChatVariables.js'; import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js'; import { MockPromptsService } from '../promptSyntax/service/mockPromptsService.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { @@ -144,12 +145,12 @@ suite('ChatService', () => { } function startSessionModel(service: IChatService, location: ChatAgentLocation = ChatAgentLocation.Chat): IChatModelReference { - const ref = testDisposables.add(service.startSession(location)); + const ref = testDisposables.add(service.startNewLocalSession(location)); return ref; } async function getOrRestoreModel(service: IChatService, resource: URI): Promise { - const ref = await service.getOrRestoreSession(resource); + const ref = await service.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None); if (!ref) { return undefined; } @@ -218,11 +219,11 @@ suite('ChatService', () => { test('retrieveSession', async () => { const testService = createChatService(); // Don't add refs to testDisposables so we can control disposal - const session1Ref = testService.startSession(ChatAgentLocation.Chat); + const session1Ref = testService.startNewLocalSession(ChatAgentLocation.Chat); const session1 = session1Ref.object as ChatModel; session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0); - const session2Ref = testService.startSession(ChatAgentLocation.Chat); + const session2Ref = testService.startNewLocalSession(ChatAgentLocation.Chat); const session2 = session2Ref.object as ChatModel; session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0); @@ -370,7 +371,7 @@ suite('ChatService', () => { const testService2 = createChatService(); - const chatModel2Ref = testService2.loadSessionFromContent(serializedChatData); + const chatModel2Ref = testService2.loadSessionFromData(serializedChatData); assert(chatModel2Ref); testDisposables.add(chatModel2Ref); const chatModel2 = chatModel2Ref.object; @@ -401,7 +402,7 @@ suite('ChatService', () => { const testService2 = createChatService(); - const chatModel2Ref = testService2.loadSessionFromContent(serializedChatData); + const chatModel2Ref = testService2.loadSessionFromData(serializedChatData); assert(chatModel2Ref); testDisposables.add(chatModel2Ref); const chatModel2 = chatModel2Ref.object; @@ -411,7 +412,7 @@ suite('ChatService', () => { test('onDidDisposeSession', async () => { const testService = createChatService(); - const modelRef = testService.startSession(ChatAgentLocation.Chat); + const modelRef = testService.startNewLocalSession(ChatAgentLocation.Chat); const model = modelRef.object; let disposed = false; diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index 5d662e05199bb..8fb1d803deea2 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -37,7 +37,7 @@ export class MockChatService implements IChatService { getProviderInfos(): IChatProviderInfo[] { throw new Error('Method not implemented.'); } - startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { + startNewLocalSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { throw new Error('Method not implemented.'); } addSession(session: IChatModel): void { @@ -50,22 +50,22 @@ export class MockChatService implements IChatService { getLatestRequest(): IChatRequestModel | undefined { return undefined; } - async getOrRestoreSession(sessionResource: URI): Promise { + async acquireOrRestoreSession(sessionResource: URI): Promise { throw new Error('Method not implemented.'); } getSessionTitle(sessionResource: URI): string | undefined { throw new Error('Method not implemented.'); } - loadSessionFromContent(data: ISerializableChatData): IChatModelReference | undefined { + loadSessionFromData(data: ISerializableChatData): IChatModelReference { throw new Error('Method not implemented.'); } - loadSessionForResource(resource: URI, position: ChatAgentLocation, token: CancellationToken): Promise { + acquireOrLoadSession(resource: URI, position: ChatAgentLocation, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined { + acquireExistingSession(sessionResource: URI): IChatModelReference | undefined { return undefined; } - setTitle(sessionResource: URI, title: string): void { + setSessionTitle(sessionResource: URI, title: string): void { throw new Error('Method not implemented.'); } appendProgress(request: IChatRequestModel, progress: IChatProgress): void { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 6415bfcfe49de..7efaea3ee5d90 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -687,7 +687,7 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito const chatService = accessor.get(IChatService); const uri = editor.getModel().uri; - const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline); + const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); const chatModel = chatModelRef.object as ChatModel; chatModel.startEditingSession(true); @@ -739,7 +739,7 @@ export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, const chatService = accessor.get(IChatService); const notebookService = accessor.get(INotebookService); const isNotebook = notebookService.hasSupportedNotebooks(uri); - const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline); + const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); const chatModel = chatModelRef.object as ChatModel; chatModel.startEditingSession(true); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 4f16fdc8bbb7e..1e54c82cb2ffd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -71,7 +71,7 @@ export async function askInPanelChat(accessor: ServicesAccessor, request: IChatR return; } - const newModelRef = chatService.startSession(ChatAgentLocation.Chat); + const newModelRef = chatService.startNewLocalSession(ChatAgentLocation.Chat); const newModel = newModelRef.object; newModel.inputModel.setState({ ...state }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 185b56de36971..52ea6e4631636 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -80,7 +80,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._onWillStartSession.fire(editor); - const chatModelRef = this._chatService.startSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); + const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); const chatModel = chatModelRef.object; chatModel.startEditingSession(false); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index d08f85abbe234..946f14bcbcd06 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -329,7 +329,7 @@ export class TerminalChatWidget extends Disposable { private async _createSession(): Promise { this._sessionCtor = createCancelablePromise(async token => { if (!this._model.value) { - const modelRef = this._chatService.startSession(ChatAgentLocation.Terminal); + const modelRef = this._chatService.startNewLocalSession(ChatAgentLocation.Terminal); this._model.value = modelRef; const model = modelRef.object; this._inlineChatWidget.setChatModel(model); diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 34b84089d180d..a46a9622da6a3 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -323,8 +323,8 @@ export class AgentSessionsWelcomePage extends EditorPane { position: ChatSessionPosition.Sidebar, displayName: '' }); - const ref = await this.chatService.loadSessionForResource(newResource, ChatAgentLocation.Chat, CancellationToken.None); - this.chatModelRef = ref ?? this.chatService.startSession(ChatAgentLocation.Chat); + const ref = await this.chatService.acquireOrLoadSession(newResource, ChatAgentLocation.Chat, CancellationToken.None); + this.chatModelRef = ref ?? this.chatService.startNewLocalSession(ChatAgentLocation.Chat); this.contentDisposables.add(this.chatModelRef); if (this.chatModelRef.object) { this.chatWidget.setModel(this.chatModelRef.object); @@ -404,7 +404,7 @@ export class AgentSessionsWelcomePage extends EditorPane { // Start a chat session so the widget has a viewModel // This is necessary for actions like mode switching to work properly - this.chatModelRef = this.chatService.startSession(ChatAgentLocation.Chat); + this.chatModelRef = this.chatService.startNewLocalSession(ChatAgentLocation.Chat); this.contentDisposables.add(this.chatModelRef); if (this.chatModelRef.object) { this.chatWidget.setModel(this.chatModelRef.object); From 74e79e64f976c430ce1c6887c23fdf3861cb3454 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:32:46 -0800 Subject: [PATCH 08/28] Update src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/common/chatService/chatServiceImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 7b937fb65fb8b..cb8c99abc8387 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -445,7 +445,7 @@ export class ChatService extends Disposable implements IChatService { } startNewLocalSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { - this.trace('startSession'); + this.trace('startNewLocalSession'); const sessionResource = LocalChatSessionUri.forSession(generateUuid()); return this._sessionModels.acquireOrCreate({ initialData: undefined, From 746774994933c275c09ec208ea8fb4cd28afe014 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 20 Feb 2026 13:47:17 -0800 Subject: [PATCH 09/28] refactor: rename terminal title properties to titleTemplate for consistency --- src/vs/platform/terminal/common/terminal.ts | 8 ++++---- .../api/browser/mainThreadTerminalService.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostTerminalService.ts | 14 +++++++------- .../contrib/terminal/browser/terminalInstance.ts | 8 ++++---- .../terminal/browser/terminalProfileQuickpick.ts | 8 ++++---- .../terminal/browser/terminalProfileService.ts | 4 ++-- .../contrib/terminal/browser/terminalService.ts | 4 ++-- .../workbench/contrib/terminal/common/terminal.ts | 6 +++--- .../terminal/test/browser/terminalInstance.test.ts | 4 ++-- src/vscode-dts/vscode.proposed.terminalTitle.d.ts | 8 ++++---- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index e061c47650b5d..dc7c596c346ca 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -677,7 +677,7 @@ export interface IShellLaunchConfig { * `terminal.integrated.tabs.title` setting. When set, this overrides the config-based * title template for this terminal instance. */ - title?: string; + titleTemplate?: string; } export interface ITerminalTabAction { @@ -693,7 +693,7 @@ export interface ICreateContributedTerminalProfileOptions { color?: string; location?: TerminalLocation | { viewColumn: number; preserveState?: boolean } | { splitActiveTerminal: boolean }; cwd?: string | URI; - title?: string; + titleTemplate?: string; } export enum TerminalLocation { @@ -721,7 +721,7 @@ export interface IShellLaunchConfigDto { isFeatureTerminal?: boolean; tabActions?: ITerminalTabAction[]; shellIntegrationEnvironmentReporting?: boolean; - title?: string; + titleTemplate?: string; } /** @@ -971,7 +971,7 @@ export interface ITerminalProfileContribution { id: string; icon?: URI | { light: URI; dark: URI } | string; color?: string; - tabTitle?: string; + titleTemplate?: string; } export interface IExtensionTerminalProfile extends ITerminalProfileContribution { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index a83feafd3ea65..ccf282f9cc8d7 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -174,7 +174,7 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT useShellEnvironment: launchConfig.useShellEnvironment, isTransient: launchConfig.isTransient, shellIntegrationNonce: launchConfig.shellIntegrationNonce, - title: launchConfig.title, + titleTemplate: launchConfig.titleTemplate, }; const terminal = Promises.withAsyncBody(async r => { const terminal = await this._terminalService.createTerminal({ @@ -417,7 +417,7 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser, tabActions: terminalInstance.shellLaunchConfig.tabActions, - title: terminalInstance.shellLaunchConfig.title + titleTemplate: terminalInstance.shellLaunchConfig.titleTemplate }; this._proxy.$acceptTerminalOpened(terminalInstance.instanceId, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7e5b2e972b842..5ddc2106e844e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -625,7 +625,7 @@ export interface TerminalLaunchConfig { location?: TerminalLocation | { viewColumn: number; preserveFocus?: boolean } | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean }; isTransient?: boolean; shellIntegrationNonce?: string; - title?: string; + titleTemplate?: string; } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index f0469e819296e..4dcec3f6681d7 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -193,12 +193,12 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(options.location, internalOptions?.resolvedExtHostIdentifier), isTransient: options.isTransient ?? undefined, shellIntegrationNonce: options.shellIntegrationNonce ?? undefined, - title: options.title ?? undefined, + titleTemplate: options.titleTemplate ?? undefined, }); } - public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string, title?: string): Promise { + public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string, titleTemplate?: string): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } @@ -210,7 +210,7 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(location, parentTerminal), isTransient: true, shellIntegrationNonce: shellIntegrationNonce ?? undefined, - title: title ?? undefined, + titleTemplate: titleTemplate ?? undefined, }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { @@ -515,7 +515,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.title).then(id => { + terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.titleTemplate).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -637,7 +637,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), env: shellLaunchConfigDto.env, hideFromUser: shellLaunchConfigDto.hideFromUser, - title: shellLaunchConfigDto.title + titleTemplate: shellLaunchConfigDto.titleTemplate }; const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); @@ -857,8 +857,8 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`No terminal profile options provided for id "${id}"`); } - const profileOptions = options.title && !profile.options.title - ? { ...profile.options, title: options.title } + const profileOptions = options.titleTemplate && !profile.options.titleTemplate + ? { ...profile.options, titleTemplate: options.titleTemplate } : profile.options; if (hasKey(profileOptions, { pty: true })) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 9e405b875d188..f78a087fbd854 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -518,7 +518,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. - if (this.shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.title) { + if (this.shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.titleTemplate) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } @@ -1471,7 +1471,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } })); } - if (this._shellLaunchConfig.name && !this._shellLaunchConfig.title) { + if (this._shellLaunchConfig.name && !this._shellLaunchConfig.titleTemplate) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Listen to xterm.js' sequence title change event, trigger this async to ensure @@ -1485,7 +1485,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); // When a title template is provided, use the name as the initial process name // so it can be referenced via ${process} in the template - if (this._shellLaunchConfig.title && this._shellLaunchConfig.name) { + if (this._shellLaunchConfig.titleTemplate && this._shellLaunchConfig.name) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Process); } else { this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); @@ -2639,7 +2639,7 @@ export class TerminalLabelComputer extends Disposable { } refreshLabel(instance: Pick, reset?: boolean): void { - const titleTemplate = instance.shellLaunchConfig.title ?? this._terminalConfigurationService.config.tabs.title; + const titleTemplate = instance.shellLaunchConfig.titleTemplate ?? this._terminalConfigurationService.config.tabs.title; this._title = this.computeLabel(instance, titleTemplate, TerminalLabelType.Title, reset); this._description = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.description, TerminalLabelType.Description); if (this._title !== instance.title || this._description !== instance.description || reset) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index 569f84b4357ac..28318f5403dda 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -81,7 +81,7 @@ export class TerminalProfileQuickpick { extensionIdentifier: string; id: string; title: string; - tabTitle?: string; + titleTemplate?: string; options: { icon: IExtensionTerminalProfile['icon']; color: IExtensionTerminalProfile['color']; @@ -95,8 +95,8 @@ export class TerminalProfileQuickpick { color: result.profile.color, } }; - if (result.profile.tabTitle !== undefined) { - config.tabTitle = result.profile.tabTitle; + if (result.profile.titleTemplate !== undefined) { + config.titleTemplate = result.profile.titleTemplate; } return { config, @@ -191,7 +191,7 @@ export class TerminalProfileQuickpick { icon: contributed.icon, id: contributed.id, color: contributed.color, - tabTitle: contributed.tabTitle + titleTemplate: contributed.titleTemplate }, profileName: contributed.title, iconClasses diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 7415ad5794f2d..598715341dedb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -234,7 +234,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi id: args.id, title: args.title, color: args.options.color, - tabTitle: args.tabTitle + titleTemplate: args.titleTemplate }; (profilesConfig as { [key: string]: ITerminalProfileObject })[args.title] = newProfile; @@ -273,5 +273,5 @@ function contributedProfilesEqual(one: IExtensionTerminalProfile, other: IExtens one.icon === other.icon && one.id === other.id && one.title === other.title && - one.tabTitle === other.tabTitle; + one.titleTemplate === other.titleTemplate; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index ddfae73b74323..6107058afad2d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -257,7 +257,7 @@ export class TerminalService extends Disposable implements ITerminalService { icon: result.config.options?.icon, color: result.config.options?.color, location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation, - title: result.config.tabTitle, + titleTemplate: result.config.titleTemplate, }); return; } else if (result.config && hasKey(result.config, { profileName: true })) { @@ -1033,7 +1033,7 @@ export class TerminalService extends Disposable implements ITerminalService { color: contributedProfile.color, location, cwd: shellLaunchConfig.cwd, - title: contributedProfile.tabTitle, + titleTemplate: contributedProfile.titleTemplate, }); const instanceHost = resolvedLocation === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; // TODO@meganrogge: This returns undefined in the remote & web smoke tests but the function diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index f442f17698def..c5951189a8000 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -67,7 +67,7 @@ export interface ITerminalProfileResolverService { export const ShellIntegrationExitCode = 633; export interface IRegisterContributedProfileArgs { - extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; tabTitle?: string; + extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; titleTemplate?: string; } export const ITerminalProfileService = createDecorator('terminalProfileService'); @@ -693,8 +693,8 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor { strictEqual(terminalLabelComputer.title, 'my-title'); strictEqual(terminalLabelComputer.description, 'folder'); }); - test('should use shellLaunchConfig.title as template when set', () => { + test('should use shellLaunchConfig.titleTemplate as template when set', () => { const terminalLabelComputer = createLabelComputer({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${cwd}' } } } }); - terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'my-sequence', processName: 'zsh', shellLaunchConfig: { title: '${sequence}' } })); + terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'my-sequence', processName: 'zsh', shellLaunchConfig: { titleTemplate: '${sequence}' } })); strictEqual(terminalLabelComputer.title, 'my-sequence'); strictEqual(terminalLabelComputer.description, 'cwd'); }); diff --git a/src/vscode-dts/vscode.proposed.terminalTitle.d.ts b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts index 584b2d29cf4d7..7833a644a49b8 100644 --- a/src/vscode-dts/vscode.proposed.terminalTitle.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts @@ -15,10 +15,10 @@ declare module 'vscode' { * behavior (which uses the `name` as a static title) and instead uses the template for * dynamic title resolution. * - * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * For example, setting `titleTemplate` to `"${sequence}"` allows the terminal's escape sequence * title to be used as the tab title. */ - title?: string; + titleTemplate?: string; } export interface ExtensionTerminalOptions { @@ -29,9 +29,9 @@ declare module 'vscode' { * behavior (which uses the `name` as a static title) and instead uses the template for * dynamic title resolution. * - * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * For example, setting `titleTemplate` to `"${sequence}"` allows the terminal's escape sequence * title to be used as the tab title. */ - title?: string; + titleTemplate?: string; } } From 1643bdada516a99e7ebc1b9ad4e4c6cf91b8ffbc Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 20 Feb 2026 14:03:27 -0800 Subject: [PATCH 10/28] proposed api check. --- src/vs/workbench/api/common/extHost.api.impl.ts | 3 +++ .../api/common/extHostTerminalService.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f26f121a87d75..1ed78327aa5a9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -941,6 +941,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { + if ('titleTemplate' in nameOrOptions && nameOrOptions.titleTemplate !== undefined) { + checkProposedApiEnabled(extension, 'terminalTitle'); + } if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 4dcec3f6681d7..52becdb964960 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -29,6 +29,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ISerializedTerminalInstanceContext } from '../../contrib/terminal/common/terminal.js'; import { isWindows } from '../../../base/common/platform.js'; import { hasKey } from '../../../base/common/types.js'; +import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -425,7 +426,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private readonly _bufferer: TerminalDataBufferer; private readonly _linkProviders: Set = new Set(); private readonly _completionProviders: Map> = new Map(); - private readonly _profileProviders: Map = new Map(); + private readonly _profileProviders: Map = new Map(); private readonly _quickFixProviders: Map = new Map(); private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); @@ -752,7 +753,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I if (this._profileProviders.has(id)) { throw new Error(`Terminal profile provider "${id}" already registered`); } - this._profileProviders.set(id, provider); + this._profileProviders.set(id, { provider, extension }); this._proxy.$registerProfileProvider(id, extension.identifier.value); return new VSCodeDisposable(() => { this._profileProviders.delete(id); @@ -845,7 +846,11 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public async $createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise { const token = new CancellationTokenSource().token; - let profile = await this._profileProviders.get(id)?.provideTerminalProfile(token); + const profileProviderData = this._profileProviders.get(id); + if (!profileProviderData) { + throw new Error(`No terminal profile provider registered for id "${id}"`); + } + let profile = await profileProviderData.provider.provideTerminalProfile(token); if (token.isCancellationRequested) { return; } @@ -857,6 +862,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`No terminal profile options provided for id "${id}"`); } + if (profile.options.titleTemplate !== undefined) { + checkProposedApiEnabled(profileProviderData.extension, 'terminalTitle'); + } + const profileOptions = options.titleTemplate && !profile.options.titleTemplate ? { ...profile.options, titleTemplate: options.titleTemplate } : profile.options; From 95a14d7284273c0481c098a355455e0b1693b95e Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:42:40 -0800 Subject: [PATCH 11/28] Fix welcome sessions list scrolling and archived session flicker (#296637) Add overrideExclude to the AgentSessionsFilter in the welcome view to exclude archived sessions from the tree data source, matching the height calculation in layoutSessionsControl which already filters them out. --- .../contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 34b84089d180d..c68affbed1e85 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -554,6 +554,7 @@ export class AgentSessionsWelcomePage extends EditorPane { }), filter: this.sessionsControlDisposables.add(this.instantiationService.createInstance(AgentSessionsFilter, { limitResults: () => MAX_SESSIONS, + overrideExclude: (session) => session.isArchived() ? true : undefined, })), getHoverPosition: () => HoverPosition.BELOW, trackActiveEditorSession: () => false, From f54a681ec73cc10fbef183d504b2bac20b0a5581 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 20 Feb 2026 14:55:18 -0800 Subject: [PATCH 12/28] address commits --- .../chat/browser/actions/chatPluginActions.ts | 20 +- .../contrib/chat/browser/chat.contribution.ts | 11 +- .../input/editor/chatInputCompletions.ts | 27 ++- .../chat/common/plugins/agentPluginService.ts | 7 +- .../common/plugins/agentPluginServiceImpl.ts | 220 +++++++++--------- .../plugins/pluginMarketplaceService.ts | 4 +- .../service/promptsServiceImpl.ts | 12 +- .../computeAutomaticInstructions.test.ts | 8 + .../service/promptsService.test.ts | 8 + 9 files changed, 177 insertions(+), 140 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts index d8a25479ebc98..5eb0f3e2f0b3d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatPluginActions.ts @@ -80,7 +80,6 @@ class ManagePluginsAction extends Action2 { const pluginMarketplaceService = accessor.get(IPluginMarketplaceService); const allPlugins = agentPluginService.allPlugins.get(); - const disabledUris = agentPluginService.disabledPluginUris.get(); const hasWorkspace = workspaceContextService.getWorkspace().folders.length > 0; // Group plugins by parent directory label @@ -105,7 +104,7 @@ class ManagePluginsAction extends Action2 { kind: ManagePluginItemKind.Plugin, label: pluginName, plugin, - picked: !disabledUris.has(plugin.uri), + picked: plugin.enabled.get(), }; if (item.picked) { preselectedPluginItems.push(item); @@ -157,11 +156,10 @@ class ManagePluginsAction extends Action2 { return; } - const currentPaths = configurationService.getValue(ChatConfiguration.PluginPaths) ?? []; - const normalizedPaths = currentPaths.filter((path): path is string => typeof path === 'string' && path.length > 0); + const currentPaths = configurationService.getValue>(ChatConfiguration.PluginPaths) ?? {}; const nextPath = folderUri.fsPath; - if (!normalizedPaths.includes(nextPath)) { - await configurationService.updateValue(ChatConfiguration.PluginPaths, [...normalizedPaths, nextPath], ConfigurationTarget.USER_LOCAL); + if (!Object.prototype.hasOwnProperty.call(currentPaths, nextPath)) { + await configurationService.updateValue(ChatConfiguration.PluginPaths, { ...currentPaths, [nextPath]: true }, ConfigurationTarget.USER_LOCAL); } return; } @@ -176,13 +174,13 @@ class ManagePluginsAction extends Action2 { const enabledUris = new ResourceSet(result.selectedPluginItems.map(i => i.plugin.uri)); for (const plugin of allPlugins) { - const wasDisabled = disabledUris.has(plugin.uri); + const wasEnabled = plugin.enabled.get(); const isNowEnabled = enabledUris.has(plugin.uri); - if (wasDisabled && isNowEnabled) { - agentPluginService.setPluginEnabled(plugin.uri, true); - } else if (!wasDisabled && !isNowEnabled) { - agentPluginService.setPluginEnabled(plugin.uri, false); + if (!wasEnabled && isNowEnabled) { + plugin.setEnabled(true); + } else if (wasEnabled && !isNowEnabled) { + plugin.setEnabled(false); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index ec53e32d5f044..3593c3cde3e9f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -624,12 +624,11 @@ configurationRegistry.registerConfiguration({ } }, [ChatConfiguration.PluginPaths]: { - type: 'array', - items: { - type: 'string', - }, - description: nls.localize('chat.plugins.paths', "Additional local plugin directories to discover. Each path should point directly to a plugin folder."), - default: [], + type: 'object', + additionalProperties: { type: 'boolean' }, + restricted: true, + markdownDescription: nls.localize('chat.plugins.paths', "Plugin directories to discover. Each key is a path that points directly to a plugin folder, and the value enables (`true`) or disables (`false`) it. Paths can be absolute or relative to the workspace root."), + default: {}, scope: ConfigurationScope.MACHINE, tags: ['experimental'], }, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index ded66c6508973..e86cda4d36cda 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -60,6 +60,17 @@ import { IChatWidget, IChatWidgetService } from '../../../chat.js'; import { resizeImage } from '../../../chatImageUtils.js'; import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; +/** + * Regex matching a slash command word (e.g. `/foo`). Uses `\p{L}` for Unicode + * letter matching, consistent with `isValidSlashCommandName`. + */ +const SlashCommandWord = /\/[\p{L}0-9_.:-]*/gu; + +/** + * Regex matching an agent-or-slash command word (e.g. `@agent` or `/cmd`). + */ +const AgentOrSlashCommandWord = /(@|\/)[\p{L}0-9_.:-]*/gu; + class SlashCommandCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @@ -83,7 +94,7 @@ class SlashCommandCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /\/[a-z0-9_.:-]*/gi); + const range = computeCompletionRanges(model, position, SlashCommandWord); if (!range) { return null; } @@ -175,7 +186,7 @@ class SlashCommandCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /\/[a-z0-9_.:-]*/gi); + const range = computeCompletionRanges(model, position, SlashCommandWord); if (!range) { return null; } @@ -234,7 +245,7 @@ class SlashCommandCompletions extends Disposable { } // regex is the opposite of `mcpPromptReplaceSpecialChars` found in `mcpTypes.ts` - const range = computeCompletionRanges(model, position, /\/[a-z0-9_.-]*/g); + const range = computeCompletionRanges(model, position, /\/[\p{L}0-9_.-]*/gu); if (!range) { return null; } @@ -291,7 +302,7 @@ class AgentCompletions extends Disposable { return; } - const range = computeCompletionRanges(model, position, /\/[a-z0-9_.:-]*/gi); + const range = computeCompletionRanges(model, position, SlashCommandWord); if (!range) { return; } @@ -332,7 +343,7 @@ class AgentCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /(@|\/)[a-z0-9_.:-]*/gi); + const range = computeCompletionRanges(model, position, AgentOrSlashCommandWord); if (!range) { return null; } @@ -432,7 +443,7 @@ class AgentCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /(@|\/)[a-z0-9_.:-]*/gi); + const range = computeCompletionRanges(model, position, AgentOrSlashCommandWord); if (!range) { return null; } @@ -499,7 +510,7 @@ class AgentCompletions extends Disposable { return null; } - const range = computeCompletionRanges(model, position, /(@|\/)[a-z0-9_.:-]*/gi); + const range = computeCompletionRanges(model, position, AgentOrSlashCommandWord); if (!range) { return; } @@ -552,7 +563,7 @@ class AgentCompletions extends Disposable { for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { // Could allow text after 'position' - if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/[a-z0-9_.:-]*)?$/i)) { + if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/[\p{L}0-9_.:-]*)?$/u)) { // No text allowed between agent and subcommand return; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts index 553da59465314..c2101984e3bb0 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginService.ts @@ -21,15 +21,11 @@ export interface IAgentPluginHook { export interface IAgentPluginCommand { readonly uri: URI; readonly name: string; - readonly description?: string; - readonly content: string; } export interface IAgentPluginSkill { readonly uri: URI; readonly name: string; - readonly description?: string; - readonly content: string; } export interface IAgentPluginMcpServerDefinition { @@ -39,6 +35,8 @@ export interface IAgentPluginMcpServerDefinition { export interface IAgentPlugin { readonly uri: URI; + readonly enabled: IObservable; + setEnabled(enabled: boolean): void; readonly hooks: IObservable; readonly commands: IObservable; readonly skills: IObservable; @@ -49,7 +47,6 @@ export interface IAgentPluginService { readonly _serviceBrand: undefined; readonly plugins: IObservable; readonly allPlugins: IObservable; - readonly disabledPluginUris: IObservable>; setPluginEnabled(pluginUri: URI, enabled: boolean): void; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index a709199c8b224..e458d7cce607d 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -7,66 +7,39 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { autorun, derived, IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { + posix, + win32 +} from '../../../../../base/common/path.js'; import { basename, extname, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IConfigurationService, ConfigurationTarget, getConfigValueInTarget } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { IMcpServerConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../../../../platform/mcp/common/mcpPlatformTypes.js'; -import { ObservableMemento, observableMemento } from '../../../../../platform/observable/common/observableMemento.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { ChatConfiguration } from '../constants.js'; -import { PromptFileParser } from '../promptSyntax/promptFileParser.js'; import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginCommand, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js'; -const STORAGE_KEY = 'workbench.chat.plugins.disabled'; const COMMAND_FILE_SUFFIX = '.md'; -const disabledPluginUrisMemento = observableMemento>({ - key: STORAGE_KEY, - defaultValue: new ResourceSet(), - fromStorage: value => { - try { - const parsed = JSON.parse(value); - if (!Array.isArray(parsed)) { - return new ResourceSet(); - } - - const uris = parsed - .filter((entry): entry is string => typeof entry === 'string') - .map(entry => URI.parse(entry)); - - return new ResourceSet(uris); - } catch { - return new ResourceSet(); - } - }, - toStorage: value => JSON.stringify([...value].map(uri => uri.toString()).sort((a, b) => a.localeCompare(b))) -}); - export class AgentPluginService extends Disposable implements IAgentPluginService { declare readonly _serviceBrand: undefined; public readonly allPlugins: IObservable; - private readonly _disabledPluginUrisMemento: ObservableMemento>; - - public readonly disabledPluginUris: IObservable>; public readonly plugins: IObservable; constructor( @IInstantiationService instantiationService: IInstantiationService, - @IStorageService storageService: IStorageService, ) { super(); - this._disabledPluginUrisMemento = this._register(disabledPluginUrisMemento(StorageScope.PROFILE, StorageTarget.MACHINE, storageService)); - - this.disabledPluginUris = this._disabledPluginUrisMemento; const discoveries: IAgentPluginDiscovery[] = []; for (const descriptor of agentPluginDiscoveryRegistry.getAll()) { @@ -81,23 +54,15 @@ export class AgentPluginService extends Disposable implements IAgentPluginServic this.plugins = derived(reader => { const all = this.allPlugins.read(reader); - const disabled = this.disabledPluginUris.read(reader); - if (disabled.size === 0) { - return all; - } - - return all.filter(p => !disabled.has(p.uri)); + return all.filter(p => p.enabled.read(reader)); }); } public setPluginEnabled(pluginUri: URI, enabled: boolean): void { - const current = new ResourceSet([...this._disabledPluginUrisMemento.get()]); - if (enabled) { - current.delete(pluginUri); - } else { - current.add(pluginUri); + const plugin = this.allPlugins.get().find(p => p.uri.toString() === pluginUri.toString()); + if (plugin) { + plugin.setEnabled(enabled); } - this._disabledPluginUrisMemento.set(current, undefined); } private _dedupeAndSort(plugins: readonly IAgentPlugin[]): readonly IAgentPlugin[] { @@ -118,10 +83,12 @@ export class AgentPluginService extends Disposable implements IAgentPluginServic } } +type PluginEntry = IAgentPlugin & { enabled: ISettableObservable }; + export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgentPluginDiscovery { - private readonly _pluginPaths: IObservable; - private readonly _pluginEntries = new Map(); + private readonly _pluginPathsConfig: IObservable>; + private readonly _pluginEntries = new Map(); private readonly _plugins = observableValue('discoveredAgentPlugins', []); public readonly plugins: IObservable = this._plugins; @@ -129,17 +96,19 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent private _discoverVersion = 0; constructor( - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @ILogService private readonly _logService: ILogService, ) { super(); - this._pluginPaths = observableConfigValue(ChatConfiguration.PluginPaths, [], configurationService); + this._pluginPathsConfig = observableConfigValue>(ChatConfiguration.PluginPaths, {}, _configurationService); } public start(): void { const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); this._register(autorun(reader => { - this._pluginPaths.read(reader); + this._pluginPathsConfig.read(reader); scheduler.schedule(); })); scheduler.schedule(); @@ -158,28 +127,33 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent private async _discoverPlugins(): Promise { const plugins: IAgentPlugin[] = []; const seenPluginUris = new Set(); + const config = this._pluginPathsConfig.get(); - for (const path of this._pluginPaths.get()) { - if (typeof path !== 'string' || !path.trim()) { + for (const [path, enabled] of Object.entries(config)) { + if (!path.trim()) { continue; } - const resource = URI.file(path); - let stat; - try { - stat = await this._fileService.resolve(resource); - } catch { - continue; - } - - if (!stat.isDirectory) { - continue; - } - - const key = stat.resource.toString(); - if (!seenPluginUris.has(key)) { - seenPluginUris.add(key); - plugins.push(this._toPlugin(stat.resource)); + const resources = this._resolvePluginPath(path.trim()); + for (const resource of resources) { + let stat; + try { + stat = await this._fileService.resolve(resource); + } catch { + this._logService.debug(`[ConfiguredAgentPluginDiscovery] Could not resolve plugin path: ${resource.toString()}`); + continue; + } + + if (!stat.isDirectory) { + this._logService.debug(`[ConfiguredAgentPluginDiscovery] Plugin path is not a directory: ${resource.toString()}`); + continue; + } + + const key = stat.resource.toString(); + if (!seenPluginUris.has(key)) { + seenPluginUris.add(key); + plugins.push(this._toPlugin(stat.resource, path, enabled)); + } } } @@ -189,24 +163,71 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent return plugins; } - private _toPlugin(uri: URI): IAgentPlugin { + /** + * Resolves a plugin path to one or more resource URIs. Absolute paths are + * used directly; relative paths are resolved against each workspace folder. + */ + private _resolvePluginPath(path: string): URI[] { + if (win32.isAbsolute(path) || posix.isAbsolute(path)) { + return [URI.file(path)]; + } + + return this._workspaceContextService.getWorkspace().folders.map( + folder => joinPath(folder.uri, path) + ); + } + + /** + * Updates the enabled state of a plugin path in the configuration, + * writing to the most specific config target where the key is defined. + */ + private _updatePluginPathEnabled(configKey: string, value: boolean): void { + const inspected = this._configurationService.inspect>(ChatConfiguration.PluginPaths); + + // Walk from most specific to least specific to find where this key is defined + const targets = [ + ConfigurationTarget.WORKSPACE_FOLDER, + ConfigurationTarget.WORKSPACE, + ConfigurationTarget.USER_LOCAL, + ConfigurationTarget.USER_REMOTE, + ConfigurationTarget.USER, + ConfigurationTarget.APPLICATION, + ]; + + for (const target of targets) { + const mapping = getConfigValueInTarget(inspected, target); + if (mapping && Object.prototype.hasOwnProperty.call(mapping, configKey)) { + this._configurationService.updateValue( + ChatConfiguration.PluginPaths, + { ...mapping, [configKey]: value }, + target, + ); + return; + } + } + + // Key not found in any target; write to USER_LOCAL as default + const current = getConfigValueInTarget(inspected, ConfigurationTarget.USER_LOCAL) ?? {}; + this._configurationService.updateValue( + ChatConfiguration.PluginPaths, + { ...current, [configKey]: value }, + ConfigurationTarget.USER_LOCAL, + ); + } + + private _toPlugin(uri: URI, configKey: string, initialEnabled: boolean): IAgentPlugin { const key = uri.toString(); const existing = this._pluginEntries.get(key); if (existing) { + existing.plugin.enabled.set(initialEnabled, undefined); return existing.plugin; } - const store = this._register(new DisposableStore()); + const store = new DisposableStore(); const commands = observableValue('agentPluginCommands', []); const skills = observableValue('agentPluginSkills', []); const mcpServerDefinitions = observableValue('agentPluginMcpServerDefinitions', []); - const plugin: IAgentPlugin = { - uri, - hooks: observableValue('agentPluginHooks', []), - commands, - skills, - mcpServerDefinitions, - }; + const enabled = observableValue('agentPluginEnabled', initialEnabled); const commandsDir = joinPath(uri, 'commands'); const skillsDir = joinPath(uri, 'skills'); @@ -239,7 +260,20 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent skillsScheduler.schedule(); mcpScheduler.schedule(); - this._pluginEntries.set(key, { plugin, store }); + const plugin: PluginEntry = { + uri, + enabled, + setEnabled: (value: boolean) => { + this._updatePluginPathEnabled(configKey, value); + }, + hooks: observableValue('agentPluginHooks', []), + commands, + skills, + mcpServerDefinitions, + }; + + this._pluginEntries.set(key, { store, plugin }); + return plugin; } @@ -392,28 +426,17 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent return []; } - const parser = new PromptFileParser(); const skills: IAgentPluginSkill[] = []; for (const child of stat.children) { if (!child.isFile || extname(child.resource).toLowerCase() !== COMMAND_FILE_SUFFIX) { continue; } - let fileContents; - try { - fileContents = await this._fileService.readFile(child.resource); - } catch { - continue; - } - - const parsed = parser.parse(child.resource, fileContents.value.toString()); - const name = parsed.header?.name?.trim() || basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); + const name = basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); skills.push({ uri: child.resource, name, - description: parsed.header?.description, - content: parsed.body?.getContent()?.trim() ?? '', }); } @@ -434,28 +457,17 @@ export class ConfiguredAgentPluginDiscovery extends Disposable implements IAgent return []; } - const parser = new PromptFileParser(); const commands: IAgentPluginCommand[] = []; for (const child of stat.children) { if (!child.isFile || extname(child.resource).toLowerCase() !== COMMAND_FILE_SUFFIX) { continue; } - let fileContents; - try { - fileContents = await this._fileService.readFile(child.resource); - } catch { - continue; - } - - const parsed = parser.parse(child.resource, fileContents.value.toString()); - const name = parsed.header?.name?.trim() || basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); + const name = basename(child.resource).slice(0, -COMMAND_FILE_SUFFIX.length); commands.push({ uri: child.resource, name, - description: parsed.header?.description, - content: parsed.body?.getContent()?.trim() ?? '', }); } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index 67382d1d1e24f..ecc7fdb86f394 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -72,14 +72,16 @@ export class PluginMarketplaceService implements IPluginMarketplaceService { try { const context = await this._requestService.request({ type: 'GET', url }, token); if (context.res.statusCode !== 200) { + this._logService.debug(`[PluginMarketplaceService] ${url} returned status ${context.res.statusCode}, skipping`); continue; } const json = await asJson(context); if (!json?.plugins || !Array.isArray(json.plugins)) { + this._logService.debug(`[PluginMarketplaceService] ${url} did not contain a valid plugins array, skipping`); continue; } return json.plugins - .filter((p): p is { name: string; description: string; version: string; source: string } => + .filter((p): p is { name: string; description?: string; version?: string; source?: string } => typeof p.name === 'string' && !!p.name ) .map(p => { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index daabef3b34f16..e2c34070d07f0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -142,6 +142,7 @@ export class PromptsService extends Disposable implements IPromptsService { private readonly _contributedWhenKeys = new Set(); private readonly _contributedWhenClauses = new Map(); private readonly _onDidContributedWhenChange = this._register(new Emitter()); + private readonly _onDidPluginPromptFilesChange = this._register(new Emitter()); private _pluginPromptFilesByType = new Map(); constructor( @@ -190,12 +191,13 @@ export class PromptsService extends Disposable implements IPromptsService { this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.prompt), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), - this._onDidContributedWhenChange.event), + this._onDidContributedWhenChange.event, + this._onDidPluginPromptFilesChange.event), )); this.cachedSkills = this._register(new CachedPromise( (token) => this.computeAgentSkills(token), - () => Event.any(this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), this._onDidContributedWhenChange.event) + () => Event.any(this.getFileLocatorEvent(PromptsType.skill), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.skill), this._onDidContributedWhenChange.event, this._onDidPluginPromptFilesChange.event) )); this.cachedHooks = this._register(new CachedPromise( @@ -222,7 +224,7 @@ export class PromptsService extends Disposable implements IPromptsService { private watchPluginPromptFilesForType( type: PromptsType, - getItems: (plugin: IAgentPlugin, reader: IReader) => readonly { uri: URI; name: string; description?: string }[], + getItems: (plugin: IAgentPlugin, reader: IReader) => readonly { uri: URI; name: string }[], ) { return autorun(reader => { const plugins = this.agentPluginService.plugins.read(reader); @@ -234,14 +236,14 @@ export class PromptsService extends Disposable implements IPromptsService { storage: PromptsStorage.local, type, name: getCanonicalPluginCommandId(plugin, item.name), - description: item.description, }); } } nextFiles.sort((a, b) => `${a.name ?? ''}|${a.uri.toString()}`.localeCompare(`${b.name ?? ''}|${b.uri.toString()}`)); this._pluginPromptFilesByType.set(type, nextFiles); - this.invalidatePromptFileCache(type); + this.cachedFileLocations[type] = undefined; + this._onDidPluginPromptFilesChange.fire(); }); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index abde6d1f8453d..2b0c19af80183 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -46,6 +46,8 @@ import { match } from '../../../../../../base/common/glob.js'; import { ChatModeKind } from '../../../common/constants.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; +import { observableValue } from '../../../../../../base/common/observable.js'; suite('ComputeAutomaticInstructions', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -179,6 +181,12 @@ suite('ComputeAutomaticInstructions', () => { instaService.stub(IContextKeyService, new MockContextKeyService()); + instaService.stub(IAgentPluginService, { + plugins: observableValue('testPlugins', []), + allPlugins: observableValue('testAllPlugins', []), + setPluginEnabled: () => { }, + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index e3c508205ac3f..1efdafbe4de61 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -10,6 +10,7 @@ import { Emitter, Event } from '../../../../../../../base/common/event.js'; import { match } from '../../../../../../../base/common/glob.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; import { Schemas } from '../../../../../../../base/common/network.js'; +import { observableValue } from '../../../../../../../base/common/observable.js'; import { relativePath } from '../../../../../../../base/common/resources.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; @@ -52,6 +53,7 @@ import { ChatModeKind } from '../../../../common/constants.js'; import { HookType } from '../../../../common/promptSyntax/hookSchema.js'; import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { IAgentPluginService } from '../../../../common/plugins/agentPluginService.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -162,6 +164,12 @@ suite('PromptsService', () => { instaService.stub(IContextKeyService, new MockContextKeyService()); + instaService.stub(IAgentPluginService, { + plugins: observableValue('testPlugins', []), + allPlugins: observableValue('testAllPlugins', []), + setPluginEnabled: () => { }, + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); From 4f8f63604ec515148fb1ad9aaf7076ed96c582f9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Feb 2026 17:00:29 -0600 Subject: [PATCH 13/28] ensure chat tip actions trigger chat setup (#296536) * fixes #296487 * fix: handle unexpected errors in chat tip action --- .../chatContentParts/chatTipContentPart.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts index f8be5ed5ebbee..f715e54ab038f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts @@ -9,7 +9,9 @@ import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../../base/common/event.js'; +import { onUnexpectedError } from '../../../../../../base/common/errors.js'; import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { localize, localize2 } from '../../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; @@ -18,9 +20,12 @@ import { IContextKey, IContextKeyService } from '../../../../../../platform/cont import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IMarkdownRenderer, openLinkFromMarkdown } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { CHAT_SETUP_ACTION_ID } from '../../actions/chatActions.js'; import { IChatTip, IChatTipService } from '../../chatTipService.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; const $ = dom.$; @@ -43,6 +48,9 @@ export class ChatTipContentPart extends Disposable { @IMenuService private readonly _menuService: IMenuService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IOpenerService private readonly _openerService: IOpenerService, + @ICommandService private readonly _commandService: ICommandService, + @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, ) { super(); @@ -108,7 +116,9 @@ export class ChatTipContentPart extends Disposable { this._toolbar.clear(); this.domNode.appendChild(renderIcon(Codicon.lightbulb)); - const markdownContent = this._renderer.render(tip.content); + const markdownContent = this._renderer.render(tip.content, { + actionHandler: (link, md) => { this._handleTipAction(link, md).catch(onUnexpectedError); } + }); this._renderedContent.value = markdownContent; this.domNode.appendChild(markdownContent.element); @@ -128,6 +138,26 @@ export class ChatTipContentPart extends Disposable { : textContent; this.domNode.setAttribute('aria-label', ariaLabel); } + + private async _handleTipAction(link: string, mdStr: IMarkdownString): Promise { + if (link.startsWith('command:') && this._shouldTriggerSetup()) { + const setupSucceeded = await this._commandService.executeCommand(CHAT_SETUP_ACTION_ID); + if (!setupSucceeded) { + return; + } + } + + await openLinkFromMarkdown(this._openerService, link, mdStr.isTrusted); + } + + private _shouldTriggerSetup(): boolean { + const sentiment = this._chatEntitlementService.sentiment; + if (!sentiment?.installed) { + return true; + } + + return this._chatEntitlementService.entitlement === ChatEntitlement.Unknown; + } } //#region Tip toolbar actions From b21a3cc7a373843d318c280bfa4c690099d51abe Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sat, 21 Feb 2026 00:08:10 +0100 Subject: [PATCH 14/28] GitService - add barrier for setting the delegate (#296631) --- .../workbench/contrib/git/browser/gitService.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/git/browser/gitService.ts b/src/vs/workbench/contrib/git/browser/gitService.ts index 9dfb27c8fad6f..2406d972a3bde 100644 --- a/src/vs/workbench/contrib/git/browser/gitService.ts +++ b/src/vs/workbench/contrib/git/browser/gitService.ts @@ -10,25 +10,34 @@ import { URI } from '../../../../base/common/uri.js'; import { IGitService, IGitExtensionDelegate, GitRef, GitRefQuery, IGitRepository, GitRepositoryState } from '../common/gitService.js'; import { ISettableObservable, observableValueOpts } from '../../../../base/common/observable.js'; import { structuralEquals } from '../../../../base/common/equals.js'; +import { AutoOpenBarrier } from '../../../../base/common/async.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class GitService extends Disposable implements IGitService { declare readonly _serviceBrand: undefined; private _delegate: IGitExtensionDelegate | undefined; + private _delegateBarrier = new AutoOpenBarrier(10_000); get repositories(): Iterable { return this._delegate?.repositories ?? []; } + constructor(@ILogService private readonly logService: ILogService) { + super(); + } + setDelegate(delegate: IGitExtensionDelegate): IDisposable { // The delegate can only be set once, since the vscode.git // extension can only run in one extension host process per // window. if (this._delegate) { - throw new BugIndicatingError('GitService delegate is already set.'); + this.logService.error('[GitService][setDelegate] GitExtension delegate is already set.'); + throw new BugIndicatingError('GitExtension delegate is already set.'); } this._delegate = delegate; + this._delegateBarrier.open(); return toDisposable(() => { this._delegate = undefined; @@ -36,7 +45,13 @@ export class GitService extends Disposable implements IGitService { } async openRepository(uri: URI): Promise { + // We need to wait for the delegate to be set before we can open a repository. + // At the moment we are waiting for 10 seconds before we automatically open the + // barrier. + await this._delegateBarrier.wait(); + if (!this._delegate) { + this.logService.warn('[GitService][openRepository] GitExtension delegate is not set after 10 seconds. Cannot open repository.'); return undefined; } From 797e1c84ae009ecd9ea1a9299227c2e8989e0be5 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 20 Feb 2026 17:13:44 -0600 Subject: [PATCH 15/28] add kb to navigate to next/previous question (#296633) --- .../browser/actions/chatAccessibilityHelp.ts | 2 + .../chat/browser/actions/chatActions.ts | 48 ++++++++++++++++++ src/vs/workbench/contrib/chat/browser/chat.ts | 10 ++++ .../chatQuestionCarouselPart.ts | 49 +++++++++++++++++-- .../contrib/chat/browser/widget/chatWidget.ts | 16 ++++++ .../browser/widget/input/chatInputPart.ts | 10 ++++ .../chat/common/actions/chatContextKeys.ts | 1 + 7 files changed, 132 insertions(+), 4 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..b21a1e21048d5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -89,6 +89,8 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age content.push(localize('chat.focusMostRecentTerminal', 'To focus the last chat terminal that ran a tool, invoke the Focus Most Recent Chat Terminal command{0}.', ``)); content.push(localize('chat.focusMostRecentTerminalOutput', 'To focus the output from the last chat terminal tool, invoke the Focus Most Recent Chat Terminal Output command{0}.', ``)); content.push(localize('chat.focusQuestionCarousel', 'When a chat question appears, toggle focus between the question and the chat input{0}.', '')); + content.push(localize('chat.previousQuestionCarouselQuestion', 'When a chat question is focused, move to the previous question{0}.', '')); + content.push(localize('chat.nextQuestionCarouselQuestion', 'When a chat question is focused, move to the next question{0}.', '')); content.push(localize('chat.focusTip', 'When a tip appears, toggle focus between the tip and the chat input{0}.', '')); } if (type === 'editsView' || type === 'agentView') { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 5843e1ee006f7..a5ed9ee7a9e58 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -910,6 +910,54 @@ export function registerChatActions() { } }); + registerAction2(class PreviousQuestionCarouselQuestionAction extends Action2 { + static readonly ID = 'workbench.action.chat.previousQuestion'; + + constructor() { + super({ + id: PreviousQuestionCarouselQuestionAction.ID, + title: localize2('interactiveSession.previousQuestion.label', "Chat: Previous Question"), + category: CHAT_CATEGORY, + f1: true, + precondition: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasQuestionCarousel), + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyCode.KeyP, + when: ContextKeyExpr.and(ChatContextKeys.inChatQuestionCarousel, ChatContextKeys.Editing.hasQuestionCarousel), + }] + }); + } + + run(accessor: ServicesAccessor): void { + const widgetService = accessor.get(IChatWidgetService); + widgetService.lastFocusedWidget?.navigateToPreviousQuestion(); + } + }); + + registerAction2(class NextQuestionCarouselQuestionAction extends Action2 { + static readonly ID = 'workbench.action.chat.nextQuestion'; + + constructor() { + super({ + id: NextQuestionCarouselQuestionAction.ID, + title: localize2('interactiveSession.nextQuestion.label', "Chat: Next Question"), + category: CHAT_CATEGORY, + f1: true, + precondition: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasQuestionCarousel), + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyCode.KeyN, + when: ContextKeyExpr.and(ChatContextKeys.inChatQuestionCarousel, ChatContextKeys.Editing.hasQuestionCarousel), + }] + }); + } + + run(accessor: ServicesAccessor): void { + const widgetService = accessor.get(IChatWidgetService); + widgetService.lastFocusedWidget?.navigateToNextQuestion(); + } + }); + registerAction2(class FocusTipAction extends Action2 { static readonly ID = 'workbench.action.chat.focusTip'; diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index c29b6a4843b05..ee24a09ab8fd1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -402,6 +402,16 @@ export interface IChatWidget { * @returns Whether the operation succeeded (i.e., the focus was toggled). */ toggleQuestionCarouselFocus(): boolean; + /** + * Navigates to the previous question in the question carousel. + * @returns Whether the operation succeeded (i.e., a previous question exists). + */ + navigateToPreviousQuestion(): boolean; + /** + * Navigates to the next question in the question carousel. + * @returns Whether the operation succeeded (i.e., a next question exists). + */ + navigateToNextQuestion(): boolean; /** * Toggles focus between the tip widget and the chat input. * Returns false if no tip is visible. diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 20aa1bdcd56b8..15170e05679de 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -25,8 +25,14 @@ import { ChatTreeItem } from '../../chat.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import './media/chatQuestionCarousel.css'; +const PREVIOUS_QUESTION_ACTION_ID = 'workbench.action.chat.previousQuestion'; +const NEXT_QUESTION_ACTION_ID = 'workbench.action.chat.nextQuestion'; + export interface IChatQuestionCarouselOptions { onSubmit: (answers: Map | undefined) => void; shouldAutoFocus?: boolean; @@ -65,6 +71,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent * that should be disposed when transitioning to summary view. */ private readonly _interactiveUIStore: MutableDisposable = this._register(new MutableDisposable()); + private readonly _inChatQuestionCarouselContextKey: IContextKey; constructor( public readonly carousel: IChatQuestionCarousel, @@ -73,10 +80,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, @IHoverService private readonly _hoverService: IHoverService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); this.domNode = dom.$('.chat-question-carousel-container'); + this._inChatQuestionCarouselContextKey = ChatContextKeys.inChatQuestionCarousel.bindTo(this._contextKeyService); + const focusTracker = this._register(dom.trackFocus(this.domNode)); + this._register(focusTracker.onDidFocus(() => this._inChatQuestionCarouselContextKey.set(true))); + this._register(focusTracker.onDidBlur(() => this._inChatQuestionCarouselContextKey.set(false))); + this._register({ dispose: () => this._inChatQuestionCarouselContextKey.reset() }); // Set up accessibility attributes for the carousel container this.domNode.tabIndex = 0; @@ -137,11 +151,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const arrowsContainer = dom.$('.chat-question-nav-arrows'); const previousLabel = localize('previous', 'Previous'); + const previousLabelWithKeybinding = this.getLabelWithKeybinding(previousLabel, PREVIOUS_QUESTION_ACTION_ID); const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev'); prevButton.label = `$(${Codicon.chevronLeft.id})`; - prevButton.element.setAttribute('aria-label', previousLabel); - interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabel })); + prevButton.element.setAttribute('aria-label', previousLabelWithKeybinding); + interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabelWithKeybinding })); this._prevButton = prevButton; const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); @@ -430,6 +445,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return dom.isAncestorOfActiveElement(this.domNode); } + public navigateToPreviousQuestion(): boolean { + if (this._currentIndex <= 0) { + return false; + } + + this.navigate(-1); + return true; + } + + public navigateToNextQuestion(): boolean { + if (this._currentIndex >= this.carousel.questions.length - 1) { + return false; + } + + this.navigate(1); + return true; + } + private renderCurrentQuestion(focusContainerForScreenReader: boolean = false): void { if (!this._questionContainer || !this._prevButton || !this._nextButton) { return; @@ -515,6 +548,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1; const submitLabel = localize('submit', 'Submit'); const nextLabel = localize('next', 'Next'); + const nextLabelWithKeybinding = this.getLabelWithKeybinding(nextLabel, NEXT_QUESTION_ACTION_ID); if (isLastQuestion) { this._nextButton!.label = submitLabel; this._nextButton!.element.setAttribute('aria-label', submitLabel); @@ -523,10 +557,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: submitLabel }); } else { this._nextButton!.label = `$(${Codicon.chevronRight.id})`; - this._nextButton!.element.setAttribute('aria-label', nextLabel); + this._nextButton!.element.setAttribute('aria-label', nextLabelWithKeybinding); // Keep secondary style for next this._nextButton!.element.classList.remove('chat-question-nav-submit'); - this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabel }); + this._nextButtonHover.value = this._hoverService.setupDelayedHover(this._nextButton!.element, { content: nextLabelWithKeybinding }); } // Update aria-label to reflect the current question @@ -541,6 +575,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._onDidChangeHeight.fire(); } + private getLabelWithKeybinding(label: string, actionId: string): string { + const keybindingLabel = this._keybindingService.lookupKeybinding(actionId, this._contextKeyService)?.getLabel(); + return keybindingLabel + ? localize('chat.questionCarousel.labelWithKeybinding', '{0} ({1})', label, keybindingLabel) + : label; + } + private renderInput(container: HTMLElement, question: IChatQuestion): void { switch (question.type) { case 'text': diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 27bef3f19c06b..6c26cd328dc8a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -800,6 +800,22 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.input.focusQuestionCarousel(); } + navigateToPreviousQuestion(): boolean { + if (!this.input.questionCarousel) { + return false; + } + + return this.input.navigateToPreviousQuestion(); + } + + navigateToNextQuestion(): boolean { + if (!this.input.questionCarousel) { + return false; + } + + return this.input.navigateToNextQuestion(); + } + toggleTipFocus(): boolean { if (this._gettingStartedTipPartRef?.hasFocus()) { this.focusInput(); 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 32a286920ce08..07a2242918ee4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2600,6 +2600,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return carousel?.hasFocus() ?? false; } + navigateToPreviousQuestion(): boolean { + const carousel = this._chatQuestionCarouselWidget.value; + return carousel?.navigateToPreviousQuestion() ?? false; + } + + navigateToNextQuestion(): boolean { + const carousel = this._chatQuestionCarouselWidget.value; + return carousel?.navigateToNextQuestion() ?? false; + } + setWorkingSetCollapsed(collapsed: boolean): void { this._workingSetCollapsed.set(collapsed, undefined); } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index a4c7f5316aac2..fd23452fa9bd3 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -39,6 +39,7 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); + export const inChatQuestionCarousel = new RawContextKey('inChatQuestionCarousel', false, { type: 'boolean', description: localize('inChatQuestionCarousel', "True when focus is in the chat question carousel.") }); export const inChatEditor = new RawContextKey('inChatEditor', false, { type: 'boolean', description: localize('inChatEditor', "Whether focus is in a chat editor.") }); export const inChatTodoList = new RawContextKey('inChatTodoList', false, { type: 'boolean', description: localize('inChatTodoList', "True when focus is in the chat todo list.") }); export const inChatTip = new RawContextKey('inChatTip', false, { type: 'boolean', description: localize('inChatTip', "True when focus is in a chat tip.") }); From 53a9e016879b27f9fc8217467893b9336a0d7dd8 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sat, 21 Feb 2026 00:36:25 +0100 Subject: [PATCH 16/28] remove creating session in beginning (#296638) --- .../contrib/chat/browser/newSession.ts | 8 ++---- .../browser/sessionsManagementService.ts | 28 ++----------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newSession.ts b/src/vs/sessions/contrib/chat/browser/newSession.ts index 266a2c2e7f9f3..fa9ea77c5b817 100644 --- a/src/vs/sessions/contrib/chat/browser/newSession.ts +++ b/src/vs/sessions/contrib/chat/browser/newSession.ts @@ -11,7 +11,6 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IsolationMode } from './sessionTargetPicker.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { IActiveSessionItem } from '../../sessions/browser/sessionsManagementService.js'; import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; @@ -25,7 +24,6 @@ export type NewSessionChangeType = 'repoUri' | 'isolationMode' | 'branch' | 'opt export interface INewSession extends IDisposable { readonly resource: URI; readonly target: AgentSessionProviders; - readonly activeSessionItem: IActiveSessionItem; readonly repoUri: URI | undefined; readonly isolationMode: IsolationMode; readonly branch: string | undefined; @@ -67,7 +65,6 @@ export class LocalNewSession extends Disposable implements INewSession { readonly target = AgentSessionProviders.Background; readonly selectedOptions = new Map(); - get resource(): URI { return this.activeSessionItem.resource; } get repoUri(): URI | undefined { return this._repoUri; } get isolationMode(): IsolationMode { return this._isolationMode; } get branch(): string | undefined { return this._branch; } @@ -76,7 +73,7 @@ export class LocalNewSession extends Disposable implements INewSession { get attachedContext(): IChatRequestVariableEntry[] | undefined { return this._attachedContext; } constructor( - readonly activeSessionItem: IActiveSessionItem, + readonly resource: URI, defaultRepoUri: URI | undefined, private readonly chatSessionsService: IChatSessionsService, private readonly logService: ILogService, @@ -155,7 +152,6 @@ export class RemoteNewSession extends Disposable implements INewSession { readonly selectedOptions = new Map(); - get resource(): URI { return this.activeSessionItem.resource; } get repoUri(): URI | undefined { return this._repoUri; } get isolationMode(): IsolationMode { return this._isolationMode; } get branch(): string | undefined { return undefined; } @@ -164,7 +160,7 @@ export class RemoteNewSession extends Disposable implements INewSession { get attachedContext(): IChatRequestVariableEntry[] | undefined { return this._attachedContext; } constructor( - readonly activeSessionItem: IActiveSessionItem, + readonly resource: URI, readonly target: AgentSessionProviders, private readonly chatSessionsService: IChatSessionsService, private readonly logService: ILogService, diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index d3a7e96111910..a70646f6b0e49 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -75,11 +75,6 @@ export interface ISessionsManagementService { */ openNewSession(): void; - /** - * Create a new session and set it as active, without opening a chat view. - */ - createNewPendingSession(pendingSessionResource: URI): Promise; - /** * Create a pending session object for the given target type. * Local sessions collect options locally; remote sessions notify the extension. @@ -244,31 +239,12 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } - async createNewPendingSession(pendingSessionResource: URI): Promise { - const chatsSession = await this.chatSessionsService.getOrCreateChatSession(pendingSessionResource, CancellationToken.None); - const chatSessionItem: IChatSessionItem = { - resource: chatsSession.sessionResource, - label: '', - timing: { - created: Date.now(), - lastRequestStarted: undefined, - lastRequestEnded: undefined, - } - }; - const repository = this.getRepositoryFromSessionOption(chatsSession.sessionResource); - const activeSessionItem = { ...chatSessionItem, repository, worktree: undefined }; - this._activeSession.set(activeSessionItem, undefined); - return activeSessionItem; - } - async createNewSessionForTarget(target: AgentSessionProviders, sessionResource: URI, defaultRepoUri?: URI): Promise { - const activeSessionItem = await this.createNewPendingSession(sessionResource); - let newSession: INewSession; if (target === AgentSessionProviders.Background || target === AgentSessionProviders.Local) { - newSession = new LocalNewSession(activeSessionItem, defaultRepoUri, this.chatSessionsService, this.logService); + newSession = new LocalNewSession(sessionResource, defaultRepoUri, this.chatSessionsService, this.logService); } else { - newSession = new RemoteNewSession(activeSessionItem, target, this.chatSessionsService, this.logService); + newSession = new RemoteNewSession(sessionResource, target, this.chatSessionsService, this.logService); } this._newSessions.set(newSession.resource.toString(), newSession); return newSession; From 175a5fd9b708faee9f2961ded4ce56a3ac931258 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:45:02 -0800 Subject: [PATCH 17/28] Show attachments toolbar for implicit context in Ask mode and selections (#296630) * Show attachments toolbar for implicit context in Ask mode and selections * Fix keyboard focus for implicit context attachments --- .../attachments/implicitContextAttachment.ts | 8 ++-- .../browser/widget/input/chatInputPart.ts | 42 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 0683d8b19d339..9eeac397e31e3 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -88,6 +88,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { private renderMainContext(context: ChatImplicitContext, isSelection?: boolean) { const contextNode = dom.$('.chat-attached-context-attachment.show-file-icons.implicit'); this.domNode.appendChild(contextNode); + contextNode.tabIndex = 0; contextNode.classList.toggle('disabled', !context.enabled); const file: URI | undefined = context.uri; @@ -158,7 +159,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { markdownTooltip = context.value.tooltip; title = this.renderString(label, context.name, context.icon, context.value.resourceUri, markdownTooltip, localize('openFile', "Current file context")); } else { - title = this.renderResource(context.value, context.isSelection, context.enabled, label); + title = this.renderResource(context.value, context.isSelection, context.enabled, label, contextNode); } if (markdownTooltip || title) { @@ -204,7 +205,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { return title; } - private renderResource(attachmentValue: Location | URI | undefined, isSelection: boolean, enabled: boolean, label: IResourceLabel): string { + private renderResource(attachmentValue: Location | URI | undefined, isSelection: boolean, enabled: boolean, label: IResourceLabel, contextNode: HTMLElement): string { const file = URI.isUri(attachmentValue) ? attachmentValue : attachmentValue!.uri; const range = URI.isUri(attachmentValue) || !isSelection ? undefined : attachmentValue!.range; @@ -227,8 +228,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { range, title }); - this.domNode.ariaLabel = ariaLabel; - this.domNode.tabIndex = 0; + contextNode.ariaLabel = ariaLabel; return title; } 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 07a2242918ee4..969eeff0b555c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -38,6 +38,7 @@ import { CodeEditorWidget } from '../../../../../../editor/browser/widget/codeEd import { EditorLayoutInfo, EditorOption, EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js'; import { IDimension } from '../../../../../../editor/common/core/2d/dimension.js'; import { IPosition } from '../../../../../../editor/common/core/position.js'; +import { IRange, Range } from '../../../../../../editor/common/core/range.js'; import { isLocation } from '../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { IModelService } from '../../../../../../editor/common/services/model.js'; @@ -80,7 +81,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { InlineChatConfigKeys } from '../../../../inlineChat/common/inlineChat.js'; import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; -import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; @@ -101,6 +102,7 @@ import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentWidgetRegistry.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; +import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; import { IChatWidget, IChatWidgetViewModelChangeEvent, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; import { resizeImage } from '../../chatImageUtils.js'; @@ -2377,8 +2379,42 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const attachments = [...this.attachmentModel.attachments.entries()]; const hasAttachments = Boolean(attachments.length); - dom.setVisibility(Boolean(this.options.renderInputToolbarBelowInput || hasAttachments), this.attachmentsContainer); - dom.setVisibility(hasAttachments, this.attachedContextContainer); + + // Render implicit context (active editor in Ask mode, or selection) + let hasImplicitContext = false; + const hasVisibleImplicitContext = this._implicitContext?.values.some(v => v.enabled || v.isSelection) ?? false; + if (this._implicitContext && hasVisibleImplicitContext) { + const isAttachmentAlreadyAttached = (targetUri: URI | undefined, targetRange: IRange | undefined, targetHandle: number | undefined): boolean => { + return this._attachmentModel.attachments.some(a => { + const aUri = URI.isUri(a.value) ? a.value : isLocation(a.value) ? a.value.uri : undefined; + const aRange = isLocation(a.value) ? a.value.range : undefined; + if (targetHandle !== undefined && isStringVariableEntry(a) && a.handle === targetHandle) { + return true; + } + if (targetUri && aUri && isEqual(targetUri, aUri)) { + if (targetRange && aRange) { + return Range.equalsRange(targetRange, aRange); + } + return !targetRange && !aRange; + } + return false; + }); + }; + const implicitContextWidget = this.instantiationService.createInstance( + ImplicitContextAttachmentWidget, + () => this._widget, + isAttachmentAlreadyAttached, + this._implicitContext, + this._contextResourceLabels, + this._attachmentModel, + container, + ); + store.add(implicitContextWidget); + hasImplicitContext = implicitContextWidget.hasRenderedContexts; + } + + dom.setVisibility(Boolean(this.options.renderInputToolbarBelowInput || hasAttachments || hasImplicitContext), this.attachmentsContainer); + dom.setVisibility(hasAttachments || hasImplicitContext, this.attachedContextContainer); if (!attachments.length) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; this._indexOfLastOpenedContext = -1; From 91b2ea73d9c5f00ff1ad9e5e0fc277ddd5de1240 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:52:48 -0800 Subject: [PATCH 18/28] Model picker search input styling and separator improvements (#296645) * Refine chat model picker menu behavior * Model picker search input styling and separator improvements * Remove filterPosition option, always place filter on top * Remove filterInputClassName option and font-size override * Fix separator always shown above Manage Models and update tests --- .../actionWidget/browser/actionList.ts | 36 ++++++++-------- .../actionWidget/browser/actionWidget.css | 3 +- .../browser/actionWidgetDropdown.ts | 43 ++++++++++++++----- .../browser/widget/input/chatModelPicker.ts | 11 ++--- .../widget/input/chatModelPicker.test.ts | 4 +- 5 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 58c13c1931135..d212e4cd14a0c 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -350,6 +350,13 @@ export interface IActionListOptions { * Minimum width for the action list. */ readonly minWidth?: number; + + + + /** + * When true and filtering is enabled, focuses the filter input when the list opens. + */ + readonly focusFilterOnOpen?: boolean; } export class ActionList extends Disposable { @@ -553,6 +560,9 @@ export class ActionList extends Disposable { if (isFiltering) { continue; } + if (item.section && this._collapsedSections.has(item.section)) { + continue; + } visible.push(item); continue; } @@ -637,14 +647,7 @@ export class ActionList extends Disposable { return this._filterContainer; } - /** - * Returns the resolved filter placement based on the dropdown direction. - * When shown above the anchor, filter is at the bottom (closest to anchor); - * when shown below, filter is at the top. - */ - get filterPlacement(): 'top' | 'bottom' { - return this._showAbove ? 'bottom' : 'top'; - } + get filterInput(): HTMLInputElement | undefined { return this._filterInput; @@ -655,6 +658,10 @@ export class ActionList extends Disposable { } focus(): void { + if (this._filterInput && this._options?.focusFilterOnOpen) { + this._filterInput.focus(); + return; + } this._list.domFocus(); this._focusCheckedOrFirst(); } @@ -835,18 +842,9 @@ export class ActionList extends Disposable { this._list.layout(listHeight, this._cachedMaxWidth); this.domNode.style.height = `${listHeight}px`; - // Place filter container on the correct side based on dropdown direction. - // When shown above, filter goes below the list (closest to anchor). - // When shown below, filter goes above the list (closest to anchor). + // Place filter container on the preferred side. if (this._filterContainer && this._filterContainer.parentElement) { - const parent = this._filterContainer.parentElement; - if (this._showAbove) { - // Move filter after the list - parent.appendChild(this._filterContainer); - } else { - // Move filter before the list - parent.insertBefore(this._filterContainer, this.domNode); - } + this._filterContainer.parentElement.insertBefore(this._filterContainer, this.domNode); } return this._cachedMaxWidth; diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index db07ddba8e2a4..41db8e8d87ed1 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -240,7 +240,7 @@ /* Filter input */ .action-widget .action-list-filter { - padding: 4px; + padding: 2px 2px 4px 2px } .action-widget .action-list-filter:first-child { @@ -259,7 +259,6 @@ border-radius: 3px; background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); - font-size: 12px; outline: none; } diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index b7b61da059f8e..2b2022fb43572 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -82,7 +82,7 @@ export class ActionWidgetDropdown extends BaseDropdown { return; } - let actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? []; + const actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? []; const actions = this._options.actions ?? this._options.actionProvider?.getActions() ?? []; // Track the currently selected option before opening @@ -159,9 +159,13 @@ export class ActionWidgetDropdown extends BaseDropdown { const previouslyFocusedElement = getActiveElement(); + const auxiliaryActionIds = new Set(actionBarActions.map(action => action.id)); + const actionWidgetDelegate: IActionListDelegate = { onSelect: (action, preview) => { - selectedOption = action; + if (!auxiliaryActionIds.has(action.id)) { + selectedOption = action; + } this.actionWidgetService.hide(); action.run(); }, @@ -173,13 +177,30 @@ export class ActionWidgetDropdown extends BaseDropdown { } }; - actionBarActions = actionBarActions.map(action => ({ - ...action, - run: async (...args: unknown[]) => { - this.actionWidgetService.hide(); - return action.run(...args); + if (actionBarActions.length) { + if (actionWidgetItems.length) { + actionWidgetItems.push({ + label: '', + kind: ActionListItemKind.Separator, + canPreview: false, + disabled: false, + hideIcon: false, + }); + } + + for (const action of actionBarActions) { + actionWidgetItems.push({ + item: action, + tooltip: action.tooltip, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(Codicon.blank.id) }, + disabled: !action.enabled, + hideIcon: false, + label: action.label, + }); } - })); + } const accessibilityProvider: Partial>> = { isChecked(element) { @@ -188,7 +209,9 @@ export class ActionWidgetDropdown extends BaseDropdown { getRole: (e) => { switch (e.kind) { case ActionListItemKind.Action: - return 'menuitemcheckbox'; + // Auxiliary actions are not checkable options, so use 'menuitem' to + // avoid screen readers announcing them as unchecked checkboxes. + return e.item && auxiliaryActionIds.has(e.item.id) ? 'menuitem' : 'menuitemcheckbox'; case ActionListItemKind.Separator: return 'separator'; default: @@ -205,7 +228,7 @@ export class ActionWidgetDropdown extends BaseDropdown { actionWidgetDelegate, this._options.getAnchor?.() ?? this.element, undefined, - actionBarActions, + [], accessibilityProvider, this._options.listOptions ); 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 bd38dd8e78750..4f61562ed0f3e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -253,9 +253,6 @@ export function buildModelPickerItems( return aName.localeCompare(bName); }); - if (items.length > 0) { - items.push({ kind: ActionListItemKind.Separator }); - } for (const item of promotedItems) { if (item.kind === 'available') { items.push(createModelItem(createModelAction(item.model, selectedModelId, onSelect), item.model)); @@ -322,9 +319,7 @@ export function buildModelPickerItems( chatEntitlementService.entitlement === ChatEntitlement.Enterprise || chatEntitlementService.isInternal ) { - if (!otherModels.length) { - items.push({ kind: ActionListItemKind.Separator }); - } + items.push({ kind: ActionListItemKind.Separator, section: otherModels.length ? ModelPickerSection.Other : undefined }); items.push({ item: { id: 'manageModels', @@ -333,12 +328,11 @@ export function buildModelPickerItems( class: undefined, tooltip: localize('chat.manageModels.tooltip', "Manage Language Models"), label: localize('chat.manageModels', "Manage Models..."), - icon: Codicon.settingsGear, run: () => { commandService.executeCommand(MANAGE_CHAT_COMMAND_ID); } }, kind: ActionListItemKind.Action, label: localize('chat.manageModels', "Manage Models..."), - group: { title: '', icon: Codicon.settingsGear }, + group: { title: '', icon: Codicon.blank }, hideIcon: false, section: otherModels.length ? ModelPickerSection.Other : undefined, showAlways: true, @@ -532,6 +526,7 @@ export class ModelPickerWidget extends Disposable { const listOptions = { showFilter: models.length >= 10, filterPlaceholder: localize('chat.modelPicker.search', "Search models"), + focusFilterOnOpen: true, collapsedByDefault: new Set([ModelPickerSection.Other]), minWidth: 300, }; 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 8f2f67e90b6ab..3cadcb3723610 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 @@ -286,8 +286,8 @@ suite('buildModelPickerItems', () => { }); // With no selected, no recent, and no featured, both models should be in Other const seps = items.filter(i => i.kind === ActionListItemKind.Separator); - // One separator before Other Models section - assert.strictEqual(seps.length, 1); + // One separator before Other Models section, one before Manage Models + assert.strictEqual(seps.length, 2); const actions = getActionItems(items); assert.strictEqual(actions[0].label, 'Auto'); // Next should be "Other Models" toggle From bf1992d6750de3b1dd2aeb460d880e9d459c2c6a Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 20 Feb 2026 16:19:22 -0800 Subject: [PATCH 19/28] Fix claude agents not showing up in local chat dropdown (#296647) --- .../chat/browser/widget/input/modePickerActionItem.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 8cc57f3238fd4..db4e90383a02f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -227,19 +227,14 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { filteredCustomModes, mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); - const modeSupportsVSCode = (mode: IChatMode) => { - const target = mode.target.get(); - return target === Target.Undefined || target === Target.VSCode; - }; - - const customBuiltinModeActions = customModes.builtin?.filter(modeSupportsVSCode)?.map(mode => { + const customBuiltinModeActions = customModes.builtin?.map(mode => { const action = makeActionFromCustomMode(mode, currentMode); action.category = agentModeDisabledViaPolicy ? policyDisabledCategory : builtInCategory; return action; }) ?? []; customBuiltinModeActions.sort((a, b) => a.label.localeCompare(b.label)); - const customModeActions = customModes.custom?.filter(modeSupportsVSCode)?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; + const customModeActions = customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; customModeActions.sort((a, b) => a.label.localeCompare(b.label)); const orderedModes = coalesce([ From 23a013aaee48b55e95c47184809a94dfff4d2f16 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:44:52 -0800 Subject: [PATCH 20/28] Better grouping of js/ts settings --- .../typescript-language-features/package.json | 2636 +++++++++-------- .../package.nls.json | 11 +- 2 files changed, 1336 insertions(+), 1311 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 28565c0e75125..45ca4ed301dc8 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -155,241 +155,23 @@ "configuration": [ { "type": "object", - "order": 20, "properties": { "typescript.tsdk": { "type": "string", "markdownDescription": "%typescript.tsdk.desc%", - "scope": "window" - }, - "typescript.disableAutomaticTypeAcquisition": { - "type": "boolean", - "default": false, - "markdownDescription": "%typescript.disableAutomaticTypeAcquisition%", "scope": "window", - "tags": [ - "usesOnlineServices" - ] - }, - "typescript.enablePromptUseWorkspaceTsdk": { - "type": "boolean", - "default": false, - "description": "%typescript.enablePromptUseWorkspaceTsdk%", - "scope": "window" + "order": 1 }, "typescript.experimental.useTsgo": { "type": "boolean", "default": false, "markdownDescription": "%typescript.useTsgo%", "scope": "window", + "order": 2, "tags": [ "experimental" ] }, - "js/ts.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.referencesCodeLens.enabled%", - "scope": "language-overridable", - "tags": [ - "JavaScript", - "TypeScript" - ] - }, - "javascript.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.referencesCodeLens.enabled%", - "markdownDeprecationMessage": "%configuration.referencesCodeLens.enabled.unifiedDeprecationMessage%", - "scope": "window" - }, - "typescript.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.referencesCodeLens.enabled%", - "markdownDeprecationMessage": "%configuration.referencesCodeLens.enabled.unifiedDeprecationMessage%", - "scope": "window" - }, - "js/ts.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", - "scope": "language-overridable", - "tags": [ - "JavaScript", - "TypeScript" - ] - }, - "javascript.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", - "markdownDeprecationMessage": "%configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage%", - "scope": "window" - }, - "typescript.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", - "markdownDeprecationMessage": "%configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage%", - "scope": "window" - }, - "js/ts.implementationsCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.implementationsCodeLens.enabled%", - "scope": "language-overridable", - "tags": [ - "TypeScript" - ] - }, - "typescript.implementationsCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.implementationsCodeLens.enabled%", - "markdownDeprecationMessage": "%configuration.implementationsCodeLens.enabled.unifiedDeprecationMessage%", - "scope": "window" - }, - "js/ts.implementationsCodeLens.showOnInterfaceMethods": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", - "scope": "language-overridable", - "tags": [ - "TypeScript" - ] - }, - "typescript.implementationsCodeLens.showOnInterfaceMethods": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", - "markdownDeprecationMessage": "%configuration.implementationsCodeLens.showOnInterfaceMethods.unifiedDeprecationMessage%", - "scope": "window" - }, - "js/ts.implementationsCodeLens.showOnAllClassMethods": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implementationsCodeLens.showOnAllClassMethods%", - "scope": "language-overridable", - "tags": [ - "TypeScript" - ] - }, - "typescript.implementationsCodeLens.showOnAllClassMethods": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implementationsCodeLens.showOnAllClassMethods%", - "markdownDeprecationMessage": "%configuration.implementationsCodeLens.showOnAllClassMethods.unifiedDeprecationMessage%", - "scope": "window" - }, - "typescript.reportStyleChecksAsWarnings": { - "type": "boolean", - "default": true, - "description": "%typescript.reportStyleChecksAsWarnings%", - "scope": "window" - }, - "typescript.validate.enable": { - "type": "boolean", - "default": true, - "description": "%typescript.validate.enable%", - "scope": "window" - }, - "javascript.validate.enable": { - "type": "boolean", - "default": true, - "description": "%javascript.validate.enable%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.module": { - "type": "string", - "markdownDescription": "%configuration.implicitProjectConfig.module%", - "default": "ESNext", - "enum": [ - "CommonJS", - "AMD", - "System", - "UMD", - "ES6", - "ES2015", - "ES2020", - "ESNext", - "None", - "ES2022", - "Node12", - "NodeNext" - ], - "scope": "window" - }, - "js/ts.implicitProjectConfig.target": { - "type": "string", - "default": "ES2024", - "markdownDescription": "%configuration.implicitProjectConfig.target%", - "enum": [ - "ES3", - "ES5", - "ES6", - "ES2015", - "ES2016", - "ES2017", - "ES2018", - "ES2019", - "ES2020", - "ES2021", - "ES2022", - "ES2023", - "ES2024", - "ESNext" - ], - "scope": "window" - }, - "js/ts.implicitProjectConfig.checkJs": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.experimentalDecorators": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strictNullChecks": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strictNullChecks%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strictFunctionTypes": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strictFunctionTypes%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strict": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strict%", - "scope": "window" - }, - "typescript.tsc.autoDetect": { - "type": "string", - "default": "on", - "enum": [ - "on", - "off", - "build", - "watch" - ], - "markdownEnumDescriptions": [ - "%typescript.tsc.autoDetect.on%", - "%typescript.tsc.autoDetect.off%", - "%typescript.tsc.autoDetect.build%", - "%typescript.tsc.autoDetect.watch%" - ], - "description": "%typescript.tsc.autoDetect%", - "scope": "window" - }, "typescript.locale": { "type": "string", "default": "auto", @@ -420,1611 +202,1846 @@ "中文(繁體)" ], "markdownDescription": "%typescript.locale%", - "scope": "window" - }, - "js/ts.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggestionActions.enabled%", - "scope": "language-overridable", - "tags": [ - "JavaScript", - "TypeScript" - ] - }, - "javascript.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%javascript.suggestionActions.enabled%", - "markdownDeprecationMessage": "%configuration.suggestionActions.enabled.unifiedDeprecationMessage%", - "scope": "resource" - }, - "typescript.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggestionActions.enabled%", - "markdownDeprecationMessage": "%configuration.suggestionActions.enabled.unifiedDeprecationMessage%", - "scope": "resource" + "scope": "window", + "order": 3 }, - "js/ts.updateImportsOnFileMove.enabled": { + "typescript.tsc.autoDetect": { "type": "string", + "default": "on", "enum": [ - "prompt", - "always", - "never" + "on", + "off", + "build", + "watch" ], "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" + "%typescript.tsc.autoDetect.on%", + "%typescript.tsc.autoDetect.off%", + "%typescript.tsc.autoDetect.build%", + "%typescript.tsc.autoDetect.watch%" ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "scope": "resource", + "description": "%typescript.tsc.autoDetect%", + "scope": "window", + "order": 4 + } + } + }, + { + "type": "object", + "title": "%configuration.preferences%", + "properties": { + "js/ts.preferences.quoteStyle": { + "type": "string", + "enum": [ + "auto", + "single", + "double" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", + "markdownEnumDescriptions": [ + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" + ], + "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "typescript.updateImportsOnFileMove.enabled": { + "javascript.preferences.quoteStyle": { "type": "string", "enum": [ - "prompt", - "always", - "never" + "auto", + "single", + "double" ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "markdownDeprecationMessage": "%configuration.updateImportsOnFileMove.enabled.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDeprecationMessage": "%configuration.preferences.quoteStyle.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "javascript.updateImportsOnFileMove.enabled": { + "typescript.preferences.quoteStyle": { "type": "string", "enum": [ - "prompt", - "always", - "never" + "auto", + "single", + "double" ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "markdownDeprecationMessage": "%configuration.updateImportsOnFileMove.enabled.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDeprecationMessage": "%configuration.preferences.quoteStyle.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.autoClosingTags.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", + "js/ts.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "typescript.autoClosingTags": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", - "markdownDeprecationMessage": "%configuration.autoClosingTags.enabled.unifiedDeprecationMessage%", + "javascript.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", + "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifier.unifiedDeprecationMessage%", "scope": "language-overridable" }, - "javascript.autoClosingTags": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", - "markdownDeprecationMessage": "%configuration.autoClosingTags.enabled.unifiedDeprecationMessage%", + "typescript.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", + "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifier.unifiedDeprecationMessage%", "scope": "language-overridable" }, - "js/ts.workspaceSymbols.scope": { + "js/ts.preferences.importModuleSpecifierEnding": { "type": "string", "enum": [ - "allOpenProjects", - "currentProject" + "auto", + "minimal", + "index", + "js" ], - "enumDescriptions": [ - "%typescript.workspaceSymbols.scope.allOpenProjects%", - "%typescript.workspaceSymbols.scope.currentProject%" + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" ], - "default": "allOpenProjects", - "markdownDescription": "%typescript.workspaceSymbols.scope%", - "scope": "window", + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "language-overridable", "tags": [ + "JavaScript", "TypeScript" ] }, - "typescript.workspaceSymbols.scope": { + "javascript.preferences.importModuleSpecifierEnding": { "type": "string", "enum": [ - "allOpenProjects", - "currentProject" + "auto", + "minimal", + "index", + "js" ], - "enumDescriptions": [ - "%typescript.workspaceSymbols.scope.allOpenProjects%", - "%typescript.workspaceSymbols.scope.currentProject%" + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" ], - "default": "allOpenProjects", - "markdownDescription": "%typescript.workspaceSymbols.scope%", - "markdownDeprecationMessage": "%configuration.workspaceSymbols.scope.unifiedDeprecationMessage%", - "scope": "window" + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifierEnding.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "scope": "window", + "typescript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifierEnding.unifiedDeprecationMessage%", + "scope": "language-overridable" + }, + "js/ts.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%configuration.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "typescript.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "markdownDeprecationMessage": "%configuration.preferGoToSourceDefinition.unifiedDeprecationMessage%", - "scope": "window" - }, - "javascript.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "markdownDeprecationMessage": "%configuration.preferGoToSourceDefinition.unifiedDeprecationMessage%", - "scope": "window" + "javascript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%javascript.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "markdownDeprecationMessage": "%configuration.preferences.jsxAttributeCompletionStyle.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.workspaceSymbols.excludeLibrarySymbols": { - "type": "boolean", - "default": true, - "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", + "typescript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "markdownDeprecationMessage": "%configuration.preferences.jsxAttributeCompletionStyle.unifiedDeprecationMessage%", + "scope": "language-overridable" + }, + "js/ts.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "enumDescriptions": [ + "%typescript.preferences.includePackageJsonAutoImports.auto%", + "%typescript.preferences.includePackageJsonAutoImports.on%", + "%typescript.preferences.includePackageJsonAutoImports.off%" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", "scope": "window", "tags": [ "TypeScript" ] }, - "typescript.workspaceSymbols.excludeLibrarySymbols": { - "type": "boolean", - "default": true, - "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", - "markdownDeprecationMessage": "%configuration.workspaceSymbols.excludeLibrarySymbols.unifiedDeprecationMessage%", + "typescript.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "enumDescriptions": [ + "%typescript.preferences.includePackageJsonAutoImports.auto%", + "%typescript.preferences.includePackageJsonAutoImports.on%", + "%typescript.preferences.includePackageJsonAutoImports.off%" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", + "markdownDeprecationMessage": "%configuration.preferences.includePackageJsonAutoImports.unifiedDeprecationMessage%", "scope": "window" }, - "js/ts.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%", + "js/ts.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "scope": "resource", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%", - "markdownDeprecationMessage": "%configuration.updateImportsOnPaste.enabled.unifiedDeprecationMessage%" - }, - "typescript.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%", - "markdownDeprecationMessage": "%configuration.updateImportsOnPaste.enabled.unifiedDeprecationMessage%" - }, - "js/ts.hover.maximumLength": { - "type": "number", - "default": 500, - "description": "%configuration.hover.maximumLength%", + "javascript.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "markdownDeprecationMessage": "%configuration.preferences.autoImportFileExcludePatterns.unifiedDeprecationMessage%", "scope": "resource" - } - } - }, - { - "type": "object", - "title": "%configuration.suggest%", - "order": 21, - "properties": { - "js/ts.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "scope": "language-overridable", - "tags": [ - "JavaScript", - "TypeScript" - ] - }, - "javascript.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "markdownDeprecationMessage": "%configuration.suggest.enabled.unifiedDeprecationMessage%", - "scope": "language-overridable" }, - "typescript.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "markdownDeprecationMessage": "%configuration.suggest.enabled.unifiedDeprecationMessage%", - "scope": "language-overridable" + "typescript.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "markdownDeprecationMessage": "%configuration.preferences.autoImportFileExcludePatterns.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "scope": "language-overridable", + "js/ts.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "scope": "resource", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "markdownDeprecationMessage": "%configuration.suggest.autoImports.unifiedDeprecationMessage%", - "scope": "resource" - }, - "typescript.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "markdownDeprecationMessage": "%configuration.suggest.autoImports.unifiedDeprecationMessage%", + "javascript.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "markdownDeprecationMessage": "%configuration.preferences.autoImportSpecifierExcludeRegexes.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.suggest.names": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.names%", - "scope": "language-overridable", - "tags": [ - "JavaScript" - ] - }, - "javascript.suggest.names": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.names%", - "markdownDeprecationMessage": "%configuration.suggest.names.unifiedDeprecationMessage%", + "typescript.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "markdownDeprecationMessage": "%configuration.preferences.autoImportSpecifierExcludeRegexes.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.suggest.completeFunctionCalls": { + "js/ts.preferences.preferTypeOnlyAutoImports": { "type": "boolean", "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "scope": "language-overridable", + "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", + "scope": "resource", "tags": [ - "JavaScript", "TypeScript" ] }, - "javascript.suggest.completeFunctionCalls": { - "type": "boolean", - "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "markdownDeprecationMessage": "%configuration.suggest.completeFunctionCalls.unifiedDeprecationMessage%", - "scope": "resource" - }, - "typescript.suggest.completeFunctionCalls": { + "typescript.preferences.preferTypeOnlyAutoImports": { "type": "boolean", "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "markdownDeprecationMessage": "%configuration.suggest.completeFunctionCalls.unifiedDeprecationMessage%", + "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", + "markdownDeprecationMessage": "%configuration.preferences.preferTypeOnlyAutoImports.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.suggest.paths": { + "js/ts.preferences.useAliasesForRenames": { "type": "boolean", "default": true, - "description": "%configuration.suggest.paths%", + "description": "%typescript.preferences.useAliasesForRenames%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.paths": { + "javascript.preferences.useAliasesForRenames": { "type": "boolean", "default": true, - "description": "%configuration.suggest.paths%", - "markdownDeprecationMessage": "%configuration.suggest.paths.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%typescript.preferences.useAliasesForRenames%", + "markdownDeprecationMessage": "%configuration.preferences.useAliasesForRenames.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "typescript.suggest.paths": { + "typescript.preferences.useAliasesForRenames": { "type": "boolean", "default": true, - "description": "%configuration.suggest.paths%", - "markdownDeprecationMessage": "%configuration.suggest.paths.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%typescript.preferences.useAliasesForRenames%", + "markdownDeprecationMessage": "%configuration.preferences.useAliasesForRenames.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.suggest.completeJSDocs": { + "js/ts.preferences.renameMatchingJsxTags": { "type": "boolean", "default": true, - "description": "%configuration.suggest.completeJSDocs%", + "description": "%typescript.preferences.renameMatchingJsxTags%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.completeJSDocs": { + "javascript.preferences.renameMatchingJsxTags": { "type": "boolean", "default": true, - "description": "%configuration.suggest.completeJSDocs%", - "markdownDeprecationMessage": "%configuration.suggest.completeJSDocs.unifiedDeprecationMessage%", + "description": "%typescript.preferences.renameMatchingJsxTags%", + "markdownDeprecationMessage": "%configuration.preferences.renameMatchingJsxTags.unifiedDeprecationMessage%", "scope": "language-overridable" }, - "typescript.suggest.completeJSDocs": { + "typescript.preferences.renameMatchingJsxTags": { "type": "boolean", "default": true, - "description": "%configuration.suggest.completeJSDocs%", - "markdownDeprecationMessage": "%configuration.suggest.completeJSDocs.unifiedDeprecationMessage%", + "description": "%typescript.preferences.renameMatchingJsxTags%", + "markdownDeprecationMessage": "%configuration.preferences.renameMatchingJsxTags.unifiedDeprecationMessage%", "scope": "language-overridable" }, - "js/ts.suggest.jsdoc.generateReturns": { + "js/ts.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" + } + }, + "tags": [ + "JavaScript", + "TypeScript" + ] + }, + "javascript.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "markdownDeprecationMessage": "%configuration.preferences.organizeImports.unifiedDeprecationMessage%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" + } + } + }, + "typescript.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "markdownDeprecationMessage": "%configuration.preferences.organizeImports.unifiedDeprecationMessage%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" + } + } + } + } + }, + { + "type": "object", + "title": "%configuration.format%", + "properties": { + "js/ts.format.enabled": { "type": "boolean", "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "description": "%format.enable%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.jsdoc.generateReturns": { + "javascript.format.enable": { "type": "boolean", "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", - "markdownDeprecationMessage": "%configuration.suggest.jsdoc.generateReturns.unifiedDeprecationMessage%", - "scope": "language-overridable" + "description": "%javascript.format.enable%", + "markdownDeprecationMessage": "%configuration.format.enable.unifiedDeprecationMessage%", + "scope": "window" }, - "typescript.suggest.jsdoc.generateReturns": { + "typescript.format.enable": { "type": "boolean", "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", - "markdownDeprecationMessage": "%configuration.suggest.jsdoc.generateReturns.unifiedDeprecationMessage%", - "scope": "language-overridable" + "description": "%typescript.format.enable%", + "markdownDeprecationMessage": "%configuration.format.enable.unifiedDeprecationMessage%", + "scope": "window" }, - "js/ts.suggest.includeAutomaticOptionalChainCompletions": { + "js/ts.format.insertSpaceAfterCommaDelimiter": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "description": "%format.insertSpaceAfterCommaDelimiter%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, + "javascript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterCommaDelimiter%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterCommaDelimiter.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterCommaDelimiter%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterCommaDelimiter.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, + "javascript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterConstructor.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterConstructor.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterSemicolonInForStatements%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.includeAutomaticOptionalChainCompletions": { + "javascript.format.insertSpaceAfterSemicolonInForStatements": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", - "markdownDeprecationMessage": "%configuration.suggest.includeAutomaticOptionalChainCompletions.unifiedDeprecationMessage%", + "description": "%format.insertSpaceAfterSemicolonInForStatements%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterSemicolonInForStatements.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.suggest.includeAutomaticOptionalChainCompletions": { + "typescript.format.insertSpaceAfterSemicolonInForStatements": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", - "markdownDeprecationMessage": "%configuration.suggest.includeAutomaticOptionalChainCompletions.unifiedDeprecationMessage%", + "description": "%format.insertSpaceAfterSemicolonInForStatements%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterSemicolonInForStatements.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.suggest.includeCompletionsForImportStatements": { + "js/ts.format.insertSpaceBeforeAndAfterBinaryOperators": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.includeCompletionsForImportStatements": { + "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", - "markdownDeprecationMessage": "%configuration.suggest.includeCompletionsForImportStatements.unifiedDeprecationMessage%", + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeAndAfterBinaryOperators.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.suggest.includeCompletionsForImportStatements": { + "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", - "markdownDeprecationMessage": "%configuration.suggest.includeCompletionsForImportStatements.unifiedDeprecationMessage%", + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeAndAfterBinaryOperators.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.suggest.classMemberSnippets.enabled": { + "js/ts.format.insertSpaceAfterKeywordsInControlFlowStatements": { "type": "boolean", "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.suggest.classMemberSnippets.enabled": { + "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { "type": "boolean", "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", - "markdownDeprecationMessage": "%configuration.suggest.classMemberSnippets.enabled.unifiedDeprecationMessage%", + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterKeywordsInControlFlowStatements.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.suggest.classMemberSnippets.enabled": { + "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { "type": "boolean", "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", - "markdownDeprecationMessage": "%configuration.suggest.classMemberSnippets.enabled.unifiedDeprecationMessage%", + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterKeywordsInControlFlowStatements.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.suggest.objectLiteralMethodSnippets.enabled": { + "js/ts.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { "type": "boolean", "default": true, - "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", "scope": "language-overridable", "tags": [ + "JavaScript", "TypeScript" ] }, - "typescript.suggest.objectLiteralMethodSnippets.enabled": { + "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { "type": "boolean", "default": true, - "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", - "markdownDeprecationMessage": "%configuration.suggest.objectLiteralMethodSnippets.enabled.unifiedDeprecationMessage%", + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions.unifiedDeprecationMessage%", "scope": "resource" - } - } - }, - { - "type": "object", - "title": "%configuration.preferences%", - "order": 21, - "properties": { - "js/ts.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], + }, + "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], - "markdownDeprecationMessage": "%configuration.preferences.quoteStyle.unifiedDeprecationMessage%", - "scope": "language-overridable" + "javascript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeFunctionParenthesis.unifiedDeprecationMessage%", + "scope": "resource" }, - "typescript.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], - "markdownDeprecationMessage": "%configuration.preferences.quoteStyle.unifiedDeprecationMessage%", - "scope": "language-overridable" + "typescript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeFunctionParenthesis.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", + "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", - "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifier.unifiedDeprecationMessage%", - "scope": "language-overridable" + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis.unifiedDeprecationMessage%", + "scope": "resource" }, - "typescript.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", - "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifier.unifiedDeprecationMessage%", - "scope": "language-overridable" + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", + "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", "scope": "language-overridable", - "tags": [ - "JavaScript", - "TypeScript" - ] - }, - "javascript.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", - "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifierEnding.unifiedDeprecationMessage%", - "scope": "language-overridable" + "tags": [ + "JavaScript", + "TypeScript" + ] }, - "typescript.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", - "markdownDeprecationMessage": "%configuration.preferences.importModuleSpecifierEnding.unifiedDeprecationMessage%", - "scope": "language-overridable" + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%configuration.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%javascript.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", - "markdownDeprecationMessage": "%configuration.preferences.jsxAttributeCompletionStyle.unifiedDeprecationMessage%", - "scope": "language-overridable" + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces.unifiedDeprecationMessage%", + "scope": "resource" }, - "typescript.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", - "markdownDeprecationMessage": "%configuration.preferences.jsxAttributeCompletionStyle.unifiedDeprecationMessage%", - "scope": "language-overridable" + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.preferences.includePackageJsonAutoImports": { - "type": "string", - "enum": [ - "auto", - "on", - "off" - ], - "enumDescriptions": [ - "%typescript.preferences.includePackageJsonAutoImports.auto%", - "%typescript.preferences.includePackageJsonAutoImports.on%", - "%typescript.preferences.includePackageJsonAutoImports.off%" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", - "scope": "window", + "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "language-overridable", "tags": [ + "JavaScript", "TypeScript" ] }, - "typescript.preferences.includePackageJsonAutoImports": { - "type": "string", - "enum": [ - "auto", - "on", - "off" - ], - "enumDescriptions": [ - "%typescript.preferences.includePackageJsonAutoImports.auto%", - "%typescript.preferences.includePackageJsonAutoImports.on%", - "%typescript.preferences.includePackageJsonAutoImports.off%" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", - "markdownDeprecationMessage": "%configuration.preferences.includePackageJsonAutoImports.unifiedDeprecationMessage%", - "scope": "window" + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" - }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "scope": "resource", + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" - }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "markdownDeprecationMessage": "%configuration.preferences.autoImportFileExcludePatterns.unifiedDeprecationMessage%", + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" - }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "markdownDeprecationMessage": "%configuration.preferences.autoImportFileExcludePatterns.unifiedDeprecationMessage%", + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" - }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "scope": "resource", + "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" - }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "markdownDeprecationMessage": "%configuration.preferences.autoImportSpecifierExcludeRegexes.unifiedDeprecationMessage%", + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" - }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "markdownDeprecationMessage": "%configuration.preferences.autoImportSpecifierExcludeRegexes.unifiedDeprecationMessage%", + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.preferences.preferTypeOnlyAutoImports": { + "js/ts.format.insertSpaceAfterTypeAssertion": { "type": "boolean", "default": false, - "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", - "scope": "resource", + "description": "%format.insertSpaceAfterTypeAssertion%", + "scope": "language-overridable", "tags": [ "TypeScript" ] }, - "typescript.preferences.preferTypeOnlyAutoImports": { + "typescript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterTypeAssertion%", + "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterTypeAssertion.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, + "javascript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForFunctions.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.format.placeOpenBraceOnNewLineForFunctions": { "type": "boolean", "default": false, - "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", - "markdownDeprecationMessage": "%configuration.preferences.preferTypeOnlyAutoImports.unifiedDeprecationMessage%", + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForFunctions.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.preferences.useAliasesForRenames": { + "js/ts.format.placeOpenBraceOnNewLineForControlBlocks": { "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.useAliasesForRenames": { + "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "markdownDeprecationMessage": "%configuration.preferences.useAliasesForRenames.unifiedDeprecationMessage%", - "scope": "language-overridable" + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForControlBlocks.unifiedDeprecationMessage%", + "scope": "resource" }, - "typescript.preferences.useAliasesForRenames": { + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "markdownDeprecationMessage": "%configuration.preferences.useAliasesForRenames.unifiedDeprecationMessage%", - "scope": "language-overridable" + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForControlBlocks.unifiedDeprecationMessage%", + "scope": "resource" }, - "js/ts.preferences.renameMatchingJsxTags": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", + "js/ts.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", "scope": "language-overridable", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ], "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.renameMatchingJsxTags": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", - "markdownDeprecationMessage": "%configuration.preferences.renameMatchingJsxTags.unifiedDeprecationMessage%", - "scope": "language-overridable" + "javascript.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", + "markdownDeprecationMessage": "%configuration.format.semicolons.unifiedDeprecationMessage%", + "scope": "resource", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ] }, - "typescript.preferences.renameMatchingJsxTags": { + "typescript.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", + "markdownDeprecationMessage": "%configuration.format.semicolons.unifiedDeprecationMessage%", + "scope": "resource", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ] + }, + "js/ts.format.indentSwitchCase": { "type": "boolean", "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", - "markdownDeprecationMessage": "%configuration.preferences.renameMatchingJsxTags.unifiedDeprecationMessage%", - "scope": "language-overridable" - }, - "js/ts.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" - }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" - }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" - }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" - }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" - } - }, + "description": "%format.indentSwitchCase%", + "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "markdownDeprecationMessage": "%configuration.preferences.organizeImports.unifiedDeprecationMessage%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" - }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" - }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" - }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" - }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" - } - } + "javascript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "markdownDeprecationMessage": "%configuration.format.indentSwitchCase.unifiedDeprecationMessage%", + "scope": "resource" }, - "typescript.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "markdownDeprecationMessage": "%configuration.preferences.organizeImports.unifiedDeprecationMessage%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" - }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" - }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" - }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" - }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" - } - } + "typescript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "markdownDeprecationMessage": "%configuration.format.indentSwitchCase.unifiedDeprecationMessage%", + "scope": "resource" } } }, { "type": "object", - "title": "%configuration.format%", - "order": 23, + "title": "%configuration.validation%", "properties": { - "js/ts.format.enabled": { + "typescript.validate.enable": { "type": "boolean", "default": true, - "description": "%format.enable%", + "description": "%typescript.validate.enable%", + "scope": "window" + }, + "javascript.validate.enable": { + "type": "boolean", + "default": true, + "description": "%javascript.validate.enable%", + "scope": "window" + }, + "typescript.reportStyleChecksAsWarnings": { + "type": "boolean", + "default": true, + "description": "%typescript.reportStyleChecksAsWarnings%", + "scope": "window" + }, + "js/ts.suggestionActions.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggestionActions.enabled%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.enable": { + "javascript.suggestionActions.enabled": { "type": "boolean", "default": true, - "description": "%javascript.format.enable%", - "markdownDeprecationMessage": "%configuration.format.enable.unifiedDeprecationMessage%", + "description": "%javascript.suggestionActions.enabled%", + "markdownDeprecationMessage": "%configuration.suggestionActions.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.suggestionActions.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggestionActions.enabled%", + "markdownDeprecationMessage": "%configuration.suggestionActions.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.tsserver.experimental.enableProjectDiagnostics": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.experimental.enableProjectDiagnostics%", + "scope": "window", + "tags": [ + "experimental" + ] + } + } + }, + { + "type": "object", + "title": "%configuration.implicitProjectConfig%", + "properties": { + "js/ts.implicitProjectConfig.module": { + "type": "string", + "markdownDescription": "%configuration.implicitProjectConfig.module%", + "default": "ESNext", + "enum": [ + "CommonJS", + "AMD", + "System", + "UMD", + "ES6", + "ES2015", + "ES2020", + "ESNext", + "None", + "ES2022", + "Node12", + "NodeNext" + ], "scope": "window" }, - "typescript.format.enable": { + "js/ts.implicitProjectConfig.target": { + "type": "string", + "default": "ES2024", + "markdownDescription": "%configuration.implicitProjectConfig.target%", + "enum": [ + "ES3", + "ES5", + "ES6", + "ES2015", + "ES2016", + "ES2017", + "ES2018", + "ES2019", + "ES2020", + "ES2021", + "ES2022", + "ES2023", + "ES2024", + "ESNext" + ], + "scope": "window" + }, + "js/ts.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.strictNullChecks": { "type": "boolean", "default": true, - "description": "%typescript.format.enable%", - "markdownDeprecationMessage": "%configuration.format.enable.unifiedDeprecationMessage%", + "markdownDescription": "%configuration.implicitProjectConfig.strictNullChecks%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.strictFunctionTypes": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.implicitProjectConfig.strictFunctionTypes%", "scope": "window" }, - "js/ts.format.insertSpaceAfterCommaDelimiter": { + "js/ts.implicitProjectConfig.strict": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.implicitProjectConfig.strict%", + "scope": "window" + } + } + }, + { + "type": "object", + "title": "%configuration.languageFeatures%", + "properties": { + "js/ts.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, + "typescript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "markdownDeprecationMessage": "%configuration.updateImportsOnFileMove.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "javascript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "markdownDeprecationMessage": "%configuration.updateImportsOnFileMove.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.autoClosingTags.enabled": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", + "description": "%typescript.autoClosingTags%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterCommaDelimiter": { + "typescript.autoClosingTags": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterCommaDelimiter.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%typescript.autoClosingTags%", + "markdownDeprecationMessage": "%configuration.autoClosingTags.enabled.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "typescript.format.insertSpaceAfterCommaDelimiter": { + "javascript.autoClosingTags": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterCommaDelimiter.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%typescript.autoClosingTags%", + "markdownDeprecationMessage": "%configuration.autoClosingTags.enabled.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.format.insertSpaceAfterConstructor": { + "js/ts.workspaceSymbols.scope": { + "type": "string", + "enum": [ + "allOpenProjects", + "currentProject" + ], + "enumDescriptions": [ + "%typescript.workspaceSymbols.scope.allOpenProjects%", + "%typescript.workspaceSymbols.scope.currentProject%" + ], + "default": "allOpenProjects", + "markdownDescription": "%typescript.workspaceSymbols.scope%", + "scope": "window", + "tags": [ + "TypeScript" + ] + }, + "typescript.workspaceSymbols.scope": { + "type": "string", + "enum": [ + "allOpenProjects", + "currentProject" + ], + "enumDescriptions": [ + "%typescript.workspaceSymbols.scope.allOpenProjects%", + "%typescript.workspaceSymbols.scope.currentProject%" + ], + "default": "allOpenProjects", + "markdownDescription": "%typescript.workspaceSymbols.scope%", + "markdownDeprecationMessage": "%configuration.workspaceSymbols.scope.unifiedDeprecationMessage%", + "scope": "window" + }, + "js/ts.preferGoToSourceDefinition": { "type": "boolean", "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "scope": "language-overridable", + "description": "%configuration.preferGoToSourceDefinition%", + "scope": "window", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterConstructor": { + "typescript.preferGoToSourceDefinition": { "type": "boolean", "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterConstructor.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%configuration.preferGoToSourceDefinition%", + "markdownDeprecationMessage": "%configuration.preferGoToSourceDefinition.unifiedDeprecationMessage%", + "scope": "window" }, - "typescript.format.insertSpaceAfterConstructor": { + "javascript.preferGoToSourceDefinition": { "type": "boolean", "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterConstructor.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%configuration.preferGoToSourceDefinition%", + "markdownDeprecationMessage": "%configuration.preferGoToSourceDefinition.unifiedDeprecationMessage%", + "scope": "window" }, - "js/ts.format.insertSpaceAfterSemicolonInForStatements": { + "js/ts.workspaceSymbols.excludeLibrarySymbols": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "scope": "language-overridable", + "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", + "scope": "window", + "tags": [ + "TypeScript" + ] + }, + "typescript.workspaceSymbols.excludeLibrarySymbols": { + "type": "boolean", + "default": true, + "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", + "markdownDeprecationMessage": "%configuration.workspaceSymbols.excludeLibrarySymbols.unifiedDeprecationMessage%", + "scope": "window" + }, + "js/ts.updateImportsOnPaste.enabled": { + "scope": "window", + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterSemicolonInForStatements": { + "javascript.updateImportsOnPaste.enabled": { + "scope": "window", "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterSemicolonInForStatements.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDescription": "%configuration.updateImportsOnPaste%", + "markdownDeprecationMessage": "%configuration.updateImportsOnPaste.enabled.unifiedDeprecationMessage%" }, - "typescript.format.insertSpaceAfterSemicolonInForStatements": { + "typescript.updateImportsOnPaste.enabled": { + "scope": "window", "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterSemicolonInForStatements.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDescription": "%configuration.updateImportsOnPaste%", + "markdownDeprecationMessage": "%configuration.updateImportsOnPaste.enabled.unifiedDeprecationMessage%" }, - "js/ts.format.insertSpaceBeforeAndAfterBinaryOperators": { + "js/ts.hover.maximumLength": { + "type": "number", + "default": 500, + "description": "%configuration.hover.maximumLength%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.suggest%", + "properties": { + "js/ts.suggest.enabled": { "type": "boolean", "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "description": "%typescript.suggest.enabled%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "javascript.suggest.enabled": { "type": "boolean", "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeAndAfterBinaryOperators.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%typescript.suggest.enabled%", + "markdownDeprecationMessage": "%configuration.suggest.enabled.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "typescript.suggest.enabled": { "type": "boolean", "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeAndAfterBinaryOperators.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%typescript.suggest.enabled%", + "markdownDeprecationMessage": "%configuration.suggest.enabled.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "js/ts.suggest.autoImports": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "description": "%configuration.suggest.autoImports%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "javascript.suggest.autoImports": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterKeywordsInControlFlowStatements.unifiedDeprecationMessage%", + "description": "%configuration.suggest.autoImports%", + "markdownDeprecationMessage": "%configuration.suggest.autoImports.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "typescript.suggest.autoImports": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterKeywordsInControlFlowStatements.unifiedDeprecationMessage%", + "description": "%configuration.suggest.autoImports%", + "markdownDeprecationMessage": "%configuration.suggest.autoImports.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "js/ts.suggest.names": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "markdownDescription": "%configuration.suggest.names%", "scope": "language-overridable", "tags": [ - "JavaScript", - "TypeScript" + "JavaScript" ] }, - "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions.unifiedDeprecationMessage%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "javascript.suggest.names": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions.unifiedDeprecationMessage%", + "markdownDescription": "%configuration.suggest.names%", + "markdownDeprecationMessage": "%configuration.suggest.names.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceBeforeFunctionParenthesis": { + "js/ts.suggest.completeFunctionCalls": { "type": "boolean", "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "description": "%configuration.suggest.completeFunctionCalls%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceBeforeFunctionParenthesis": { + "javascript.suggest.completeFunctionCalls": { "type": "boolean", "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeFunctionParenthesis.unifiedDeprecationMessage%", + "description": "%configuration.suggest.completeFunctionCalls%", + "markdownDeprecationMessage": "%configuration.suggest.completeFunctionCalls.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.format.insertSpaceBeforeFunctionParenthesis": { + "typescript.suggest.completeFunctionCalls": { "type": "boolean", "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceBeforeFunctionParenthesis.unifiedDeprecationMessage%", + "description": "%configuration.suggest.completeFunctionCalls%", + "markdownDeprecationMessage": "%configuration.suggest.completeFunctionCalls.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "js/ts.suggest.paths": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "default": true, + "description": "%configuration.suggest.paths%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "javascript.suggest.paths": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.paths%", + "markdownDeprecationMessage": "%configuration.suggest.paths.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "typescript.suggest.paths": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.paths%", + "markdownDeprecationMessage": "%configuration.suggest.paths.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "js/ts.suggest.completeJSDocs": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "default": true, + "description": "%configuration.suggest.completeJSDocs%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "javascript.suggest.completeJSDocs": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets.unifiedDeprecationMessage%", - "scope": "resource" + "default": true, + "description": "%configuration.suggest.completeJSDocs%", + "markdownDeprecationMessage": "%configuration.suggest.completeJSDocs.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "typescript.suggest.completeJSDocs": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets.unifiedDeprecationMessage%", - "scope": "resource" + "default": true, + "description": "%configuration.suggest.completeJSDocs%", + "markdownDeprecationMessage": "%configuration.suggest.completeJSDocs.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "js/ts.suggest.jsdoc.generateReturns": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "javascript.suggest.jsdoc.generateReturns": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "markdownDeprecationMessage": "%configuration.suggest.jsdoc.generateReturns.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "typescript.suggest.jsdoc.generateReturns": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "markdownDeprecationMessage": "%configuration.suggest.jsdoc.generateReturns.unifiedDeprecationMessage%", + "scope": "language-overridable" }, - "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "js/ts.suggest.includeAutomaticOptionalChainCompletions": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "javascript.suggest.includeAutomaticOptionalChainCompletions": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces.unifiedDeprecationMessage%", + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "markdownDeprecationMessage": "%configuration.suggest.includeAutomaticOptionalChainCompletions.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "typescript.suggest.includeAutomaticOptionalChainCompletions": { "type": "boolean", "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces.unifiedDeprecationMessage%", + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "markdownDeprecationMessage": "%configuration.suggest.includeAutomaticOptionalChainCompletions.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "js/ts.suggest.includeCompletionsForImportStatements": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "javascript.suggest.includeCompletionsForImportStatements": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "markdownDeprecationMessage": "%configuration.suggest.includeCompletionsForImportStatements.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "typescript.suggest.includeCompletionsForImportStatements": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "markdownDeprecationMessage": "%configuration.suggest.includeCompletionsForImportStatements.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "js/ts.suggest.classMemberSnippets.enabled": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "javascript.suggest.classMemberSnippets.enabled": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", + "markdownDeprecationMessage": "%configuration.suggest.classMemberSnippets.enabled.unifiedDeprecationMessage%", "scope": "resource" }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "typescript.suggest.classMemberSnippets.enabled": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", + "markdownDeprecationMessage": "%configuration.suggest.classMemberSnippets.enabled.unifiedDeprecationMessage%", "scope": "resource" }, - "js/ts.format.insertSpaceAfterTypeAssertion": { + "js/ts.suggest.objectLiteralMethodSnippets.enabled": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterTypeAssertion%", + "default": true, + "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", "scope": "language-overridable", "tags": [ "TypeScript" ] }, - "typescript.format.insertSpaceAfterTypeAssertion": { + "typescript.suggest.objectLiteralMethodSnippets.enabled": { "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterTypeAssertion%", - "markdownDeprecationMessage": "%configuration.format.insertSpaceAfterTypeAssertion.unifiedDeprecationMessage%", + "default": true, + "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", + "markdownDeprecationMessage": "%configuration.suggest.objectLiteralMethodSnippets.enabled.unifiedDeprecationMessage%", "scope": "resource" - }, - "js/ts.format.placeOpenBraceOnNewLineForFunctions": { + } + } + }, + { + "type": "object", + "title": "%configuration.codeLens%", + "properties": { + "js/ts.referencesCodeLens.enabled": { "type": "boolean", "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "description": "%configuration.referencesCodeLens.enabled%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.placeOpenBraceOnNewLineForFunctions": { + "javascript.referencesCodeLens.enabled": { "type": "boolean", "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", - "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForFunctions.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%configuration.referencesCodeLens.enabled%", + "markdownDeprecationMessage": "%configuration.referencesCodeLens.enabled.unifiedDeprecationMessage%", + "scope": "window" }, - "typescript.format.placeOpenBraceOnNewLineForFunctions": { + "typescript.referencesCodeLens.enabled": { "type": "boolean", "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", - "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForFunctions.unifiedDeprecationMessage%", - "scope": "resource" + "description": "%configuration.referencesCodeLens.enabled%", + "markdownDeprecationMessage": "%configuration.referencesCodeLens.enabled.unifiedDeprecationMessage%", + "scope": "window" }, - "js/ts.format.placeOpenBraceOnNewLineForControlBlocks": { + "js/ts.referencesCodeLens.showOnAllFunctions": { "type": "boolean", "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", "scope": "language-overridable", "tags": [ "JavaScript", "TypeScript" ] }, - "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { + "javascript.referencesCodeLens.showOnAllFunctions": { "type": "boolean", "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", - "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForControlBlocks.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", + "markdownDeprecationMessage": "%configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage%", + "scope": "window" }, - "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { + "typescript.referencesCodeLens.showOnAllFunctions": { "type": "boolean", "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", - "markdownDeprecationMessage": "%configuration.format.placeOpenBraceOnNewLineForControlBlocks.unifiedDeprecationMessage%", - "scope": "resource" + "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", + "markdownDeprecationMessage": "%configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage%", + "scope": "window" }, - "js/ts.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", + "js/ts.implementationsCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%configuration.implementationsCodeLens.enabled%", "scope": "language-overridable", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ], "tags": [ - "JavaScript", "TypeScript" ] }, - "javascript.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", - "markdownDeprecationMessage": "%configuration.format.semicolons.unifiedDeprecationMessage%", - "scope": "resource", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ] - }, - "typescript.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", - "markdownDeprecationMessage": "%configuration.format.semicolons.unifiedDeprecationMessage%", - "scope": "resource", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ] + "typescript.implementationsCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%configuration.implementationsCodeLens.enabled%", + "markdownDeprecationMessage": "%configuration.implementationsCodeLens.enabled.unifiedDeprecationMessage%", + "scope": "window" }, - "js/ts.format.indentSwitchCase": { + "js/ts.implementationsCodeLens.showOnInterfaceMethods": { "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", + "default": false, + "markdownDescription": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", "scope": "language-overridable", "tags": [ - "JavaScript", "TypeScript" ] }, - "javascript.format.indentSwitchCase": { + "typescript.implementationsCodeLens.showOnInterfaceMethods": { "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", - "markdownDeprecationMessage": "%configuration.format.indentSwitchCase.unifiedDeprecationMessage%", - "scope": "resource" + "default": false, + "markdownDescription": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", + "markdownDeprecationMessage": "%configuration.implementationsCodeLens.showOnInterfaceMethods.unifiedDeprecationMessage%", + "scope": "window" }, - "typescript.format.indentSwitchCase": { + "js/ts.implementationsCodeLens.showOnAllClassMethods": { "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", - "markdownDeprecationMessage": "%configuration.format.indentSwitchCase.unifiedDeprecationMessage%", - "scope": "resource" + "default": false, + "markdownDescription": "%configuration.implementationsCodeLens.showOnAllClassMethods%", + "scope": "language-overridable", + "tags": [ + "TypeScript" + ] + }, + "typescript.implementationsCodeLens.showOnAllClassMethods": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implementationsCodeLens.showOnAllClassMethods%", + "markdownDeprecationMessage": "%configuration.implementationsCodeLens.showOnAllClassMethods.unifiedDeprecationMessage%", + "scope": "window" } } }, { "type": "object", "title": "%configuration.inlayHints%", - "order": 24, "properties": { "js/ts.inlayHints.parameterNames.enabled": { "type": "string", @@ -2244,9 +2261,23 @@ }, { "type": "object", - "title": "%configuration.server%", - "order": 25, + "title": "%configuration.serverAdvanced%", "properties": { + "typescript.enablePromptUseWorkspaceTsdk": { + "type": "boolean", + "default": false, + "description": "%typescript.enablePromptUseWorkspaceTsdk%", + "scope": "window" + }, + "typescript.disableAutomaticTypeAcquisition": { + "type": "boolean", + "default": false, + "markdownDescription": "%typescript.disableAutomaticTypeAcquisition%", + "scope": "window", + "tags": [ + "usesOnlineServices" + ] + }, "typescript.tsserver.nodePath": { "type": "string", "description": "%configuration.tsserver.nodePath%", @@ -2303,15 +2334,6 @@ "markdownDescription": "%configuration.tsserver.maxTsServerMemory%", "scope": "window" }, - "typescript.tsserver.experimental.enableProjectDiagnostics": { - "type": "boolean", - "default": false, - "description": "%configuration.tsserver.experimental.enableProjectDiagnostics%", - "scope": "window", - "tags": [ - "experimental" - ] - }, "typescript.tsserver.watchOptions": { "description": "%configuration.tsserver.watchOptions%", "scope": "window", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 799c5fedf2f65..b27a4f512b94c 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -4,12 +4,15 @@ "workspaceTrust": "The extension requires workspace trust when the workspace version is used because it executes code specified by the workspace.", "virtualWorkspaces": "In virtual workspaces, resolving and finding references across files is not supported.", "reloadProjects.title": "Reload Project", - "configuration.typescript": "TypeScript", - "configuration.preferences": "Preferences", + "configuration.codeLens": "CodeLens", "configuration.format": "Formatting", - "configuration.suggest": "Suggestions", + "configuration.languageFeatures": "Language Features", + "configuration.implicitProjectConfig": "Implicit Project Config", "configuration.inlayHints": "Inlay Hints", - "configuration.server": "TS Server", + "configuration.preferences": "Preferences", + "configuration.serverAdvanced": "TS Server Advanced Settings", + "configuration.suggest": "Suggestions", + "configuration.validation": "Validation", "configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.", "configuration.suggest.completeFunctionCalls.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.suggest.completeFunctionCalls#` instead.", "configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.", From cf4d60558a424af3b305095702a94926a4e95307 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:49:49 -0800 Subject: [PATCH 21/28] Fix missing `%` --- extensions/typescript-language-features/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 45ca4ed301dc8..88dca19cfba32 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -650,7 +650,7 @@ ], "markdownEnumDescriptions": [ "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive%", "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" ], "default": "auto" @@ -733,7 +733,7 @@ ], "markdownEnumDescriptions": [ "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive%", "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" ], "default": "auto" From cca45c80c669854bcc9664997cd29e23f138e1c0 Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:51:34 -0800 Subject: [PATCH 22/28] Remove checkmarks from chat-used-context-label, add hover chevron (#296621) * Remove checkmarks from chat-used-context-label, add hover chevron * thinking check fixes, confirmation widget fixes * fix some tests and margin * remove checkmarks from progress containers * remove all checks and support in hooks and all other used context lists * Add aria-hidden to decorative hover chevrons --------- Co-authored-by: justschen --- .../chatCollapsibleContentPart.ts | 20 ++++++- .../chatToolInputOutputContentPart.ts | 22 ++++++-- .../media/chatCodeBlockPill.css | 6 ++- .../media/chatConfirmationWidget.css | 25 +++++---- .../media/chatHookContentPart.css | 10 ++-- .../media/chatThinkingContent.css | 53 +++++++++++++++---- .../chat/browser/widget/media/chat.css | 28 ++++++++-- .../chatSubagentContentPart.test.ts | 2 +- 8 files changed, 127 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts index 01559966fdfd6..32c69e1170e77 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatCollapsibleContentPart.ts @@ -81,6 +81,10 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I this._domNode = $('.chat-used-context', undefined, buttonElement); collapseButton.label = referencesLabel; + // Add hover chevron indicator on the right (decorative, hide from screen readers) + const hoverChevron = $('span.chat-collapsible-hover-chevron.codicon.codicon-chevron-right', { 'aria-hidden': 'true' }); + collapseButton.element.appendChild(hoverChevron); + if (this.hoverMessage) { this._register(this.hoverService.setupDelayedHover(collapseButton.iconElement, { content: this.hoverMessage, @@ -98,7 +102,21 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I this._register(autorun(r => { const expanded = this._isExpanded.read(r); - collapseButton.icon = this._overrideIcon.read(r) ?? (expanded ? Codicon.chevronDown : Codicon.chevronRight); + const overrideIcon = this._overrideIcon.read(r); + const isErrorIcon = overrideIcon?.id === Codicon.error.id || overrideIcon?.id === Codicon.warning.id; + + if (isErrorIcon && overrideIcon) { + collapseButton.icon = overrideIcon; + collapseButton.iconElement.style.display = ''; + } else { + collapseButton.icon = Codicon.blank; + collapseButton.iconElement.style.display = 'none'; + } + + // Update hover chevron direction + hoverChevron.classList.toggle('codicon-chevron-right', !expanded); + hoverChevron.classList.toggle('codicon-chevron-down', expanded); + this._domNode?.classList.toggle('chat-used-context-collapsed', !expanded); this.updateAriaLabel(collapseButton.element, typeof referencesLabel === 'string' ? referencesLabel : referencesLabel.value, expanded); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts index 79ba3979615aa..9950215466d99 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatToolInputOutputContentPart.ts @@ -123,6 +123,11 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { btn.element.classList.add('chat-confirmation-widget-title', 'monaco-text-button'); btn.labelElement.append(titleEl.root); + // Add hover chevron indicator on the right (decorative, hide from screen readers) + const hoverChevron = dom.$('span.chat-collapsible-hover-chevron.codicon.codicon-chevron-right'); + hoverChevron.setAttribute('aria-hidden', 'true'); + btn.element.appendChild(hoverChevron); + const check = dom.h(isError ? ThemeIcon.asCSSSelector(Codicon.error) : output @@ -137,16 +142,23 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { })); } + // Only show leading icon for errors + if (isError) { + btn.icon = Codicon.error; + } else { + btn.icon = Codicon.blank; + btn.iconElement.style.display = 'none'; + } + const expanded = this._expanded = observableValue(this, initiallyExpanded); this._register(autorun(r => { const value = expanded.read(r); - btn.icon = isError - ? Codicon.error - : output - ? Codicon.check - : ThemeIcon.modify(Codicon.loading, 'spin'); elements.root.classList.toggle('collapsed', !value); + // Update hover chevron direction + hoverChevron.classList.toggle('codicon-chevron-right', !value); + hoverChevron.classList.toggle('codicon-chevron-down', value); + // Lazy initialization: render content only when expanded for the first time if (value && !this._contentInitialized) { this._contentInitialized = true; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css index ab3056de41f66..da31535a946ec 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatCodeBlockPill.css @@ -11,7 +11,7 @@ display: flex; align-items: center; gap: 5px; - margin: 0 0 6px 4px; + margin: 0 0 6px 0px; font-size: var(--vscode-chat-font-size-body-s); color: var(--vscode-descriptionForeground); @@ -33,6 +33,10 @@ } } + .codicon.codicon-check { + display: none; + } + .status-label { color: var(--vscode-descriptionForeground); white-space: nowrap; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css index 1a3d83505b78b..c54b75574b128 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css @@ -83,13 +83,6 @@ } .chat-confirmation-widget .chat-confirmation-widget-title.monaco-button { - &:hover { - background: var(--vscode-toolbar-hoverBackground); - } - - &:active { - background: var(--vscode-toolbar-activeBackground); - } .monaco-button-mdlabel { display: flex; @@ -319,7 +312,7 @@ } .chat-confirmation-widget-container .chat-confirmation-widget .chat-confirmation-widget-title { - padding: 2px 6px 2px 2px; + padding: 2px 6px 2px 0px; &.monaco-button { @@ -332,8 +325,20 @@ font-size: 12px; } - &:hover { - background: var(--vscode-list-hoverBackground); + &:hover { + p, .rendered-markdown { + color: var(--vscode-foreground); + } + + .chat-collapsible-hover-chevron { + opacity: 1; + } + } + } + + .chat-confirmation-widget:not(.collapsed) .chat-confirmation-widget-title { + p, .rendered-markdown { + color: var(--vscode-foreground); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css index 3a30dc1e68a22..179a2b06dc892 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css @@ -7,12 +7,6 @@ color: var(--vscode-notificationsWarningIcon-foreground) !important; } -.chat-thinking-box .chat-used-context.chat-hook-outcome-blocked, -.chat-thinking-box .chat-used-context.chat-hook-outcome-warning { - padding: 4px 12px 4px 22px; - margin-bottom: 0; -} - .chat-thinking-box .chat-used-context.chat-hook-outcome-blocked > .chat-used-context-label .codicon, .chat-thinking-box .chat-used-context.chat-hook-outcome-warning > .chat-used-context-label .codicon { display: none; @@ -42,3 +36,7 @@ margin-top: 2px; padding-top: 6px; } + +.chat-used-context.chat-hook-outcome-warning .chat-used-context-label .monaco-button { + gap: 4px; +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index 3a2d4c3cef96b..817fe3945b42f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -98,22 +98,53 @@ .chat-tool-invocation-part { padding: 4px 12px 4px 18px; position: relative; + } + .chat-thinking-tool-wrapper { .chat-used-context { - margin-bottom: 0px; - margin-left: 2px; - padding-left: 2px; - } + margin-bottom: 0px; + margin-left: 4px; + padding-left: 2px; + } - .progress-container, - .chat-confirmation-widget-container { - margin: 0 0 2px 6px; - } + .chat-used-context { + .monaco-button.monaco-text-button { + color: var(--vscode-descriptionForeground); - .codicon:not(.chat-thinking-icon) { - display: none; - } + :hover { + color: var(--vscode-foreground); + } + + .monaco-button:hover .chat-collapsible-hover-chevron { + opacity: 1; + } + } + } + .chat-used-context:not(.chat-used-context-collapsed) { + .monaco-button.monaco-text-button { + color: var(--vscode-foreground); + } + } + + .progress-container, + .chat-confirmation-widget-container { + margin: 0 0 2px 6px; + } + + .codicon:not(.chat-thinking-icon) { + display: none; + } + + .chat-collapsible-hover-chevron.codicon { + display: inline-flex; + } + + .chat-used-context.chat-hook-outcome-blocked, + .chat-used-context.chat-hook-outcome-warning { + padding: 4px 12px 4px 20px; + margin-bottom: 0; + } } .chat-thinking-item.markdown-content { diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index b10b74d14fff9..675c91bdfcee7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2154,7 +2154,7 @@ have to be updated for changes to the rules above, or to support more deeply nes width: fit-content; border: none; border-radius: 4px; - gap: 4px; + gap: 0; text-align: initial; justify-content: initial; } @@ -2170,6 +2170,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-used-context-label .monaco-button { padding: 2px 6px 2px 2px; + margin-left: -2px; font-size: var(--vscode-chat-font-size-body-s); line-height: unset; } @@ -2178,10 +2179,9 @@ have to be updated for changes to the rules above, or to support more deeply nes background-color: var(--vscode-toolbar-hoverBackground); } +.interactive-session .chat-used-context:not(.chat-used-context-collapsed) .chat-used-context-label .monaco-button, .interactive-session .chat-used-context-label .monaco-button:hover { - background-color: var(--vscode-list-hoverBackground); color: var(--vscode-foreground); - } .interactive-session .chat-file-changes-label .monaco-text-button:focus, @@ -2200,11 +2200,27 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-icon-foreground) !important; } +/* Hover chevron indicator for collapsible parts */ +.chat-collapsible-hover-chevron { + font-size: 12px; + opacity: 0; + transition: opacity 0.1s ease-in-out; + color: var(--vscode-descriptionForeground); +} + +.chat-collapsible-hover-chevron.codicon-chevron-down { + opacity: 1; +} + +.interactive-session .chat-used-context-label .monaco-button:hover .chat-collapsible-hover-chevron { + opacity: 1; +} + .interactive-item-container .progress-container { display: flex; align-items: center; gap: 4px; - margin: 0 0 6px 2px; + margin: 0 0 6px 0; font-size: 13px; /* Tool calls transition from a progress to a collapsible list part, which needs to have this top padding. @@ -2219,6 +2235,10 @@ have to be updated for changes to the rules above, or to support more deeply nes } } + > .codicon.codicon-check { + display: none; + } + .codicon { /* Very aggressive list styles try to apply focus colors to every codicon in a list row. */ color: var(--vscode-icon-foreground) !important; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts index 1f3400c6fc029..ad7c78f8966dc 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -274,7 +274,7 @@ suite('ChatSubagentContentPart', () => { } function getCollapseButtonLabel(button: HTMLElement): HTMLElement | undefined { - const label = button.lastElementChild; + const label = button.querySelector('.monaco-button-mdlabel'); return isHTMLElement(label) ? label : undefined; } From 4968804d65496a837615a30f9529ee891ee8b6f5 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:01:16 -0800 Subject: [PATCH 23/28] Allow returning a title in the Content Provider (#296659) So that the title of a chat can be changed. --- .../api/browser/mainThreadChatSessions.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatSessions.ts | 1 + .../browser/mainThreadChatSessions.test.ts | 40 +++++++++++++++++++ .../common/chatService/chatServiceImpl.ts | 4 ++ .../chat/common/chatSessionsService.ts | 2 + .../vscode.proposed.chatSessionsProvider.d.ts | 9 +++++ 7 files changed, 59 insertions(+) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index a683f75dfb8f3..496430de9647f 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -40,6 +40,7 @@ export class ObservableChatSession extends Disposable implements IChatSession { readonly sessionResource: URI; readonly providerHandle: number; readonly history: Array; + title?: string; private _options?: Record; public get options(): Record | undefined { return this._options; @@ -111,6 +112,7 @@ export class ObservableChatSession extends Disposable implements IChatSession { ); this._options = sessionContent.options; + this.title = sessionContent.title; this.history.length = 0; this.history.push(...sessionContent.history.map((turn: IChatSessionHistoryItemDto) => { if (turn.type === 'request') { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5504fae3fa645..7164a0217ca5f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3410,6 +3410,7 @@ export interface ChatSessionOptionUpdateDto2 { export interface ChatSessionDto { id: string; resource: UriComponents; + title?: string; history: Array; hasActiveResponseCallback: boolean; hasRequestHandler: boolean; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index da032619287c2..f884d985ddacf 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -546,6 +546,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return { id: sessionId + '', resource: URI.revive(sessionResource), + title: session.title, hasActiveResponseCallback: !!session.activeResponseCallback, hasRequestHandler: !!session.requestHandler, supportsInterruption: !!capabilities?.supportsInterruptions, diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 78018645d7354..2e31054aa64b7 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -75,12 +75,14 @@ suite('ObservableChatSession', function () { function createSessionContent(options: { id?: string; + title?: string; history?: any[]; hasActiveResponseCallback?: boolean; hasRequestHandler?: boolean; } = {}) { return { id: options.id || 'test-id', + title: options.title, history: options.history || [], hasActiveResponseCallback: options.hasActiveResponseCallback || false, hasRequestHandler: options.hasRequestHandler || false @@ -161,6 +163,22 @@ suite('ObservableChatSession', function () { assert.ok(session.requestHandler); }); + test('initialization sets title from session content', async function () { + const sessionContent = createSessionContent({ + title: 'My Custom Title', + }); + + const session = disposables.add(await createInitializedSession(sessionContent)); + assert.strictEqual(session.title, 'My Custom Title'); + }); + + test('title is undefined when not provided in session content', async function () { + const sessionContent = createSessionContent(); + + const session = disposables.add(await createInitializedSession(sessionContent)); + assert.strictEqual(session.title, undefined); + }); + test('initialization is idempotent and returns same promise', async function () { const sessionId = 'test-id'; const resource = LocalChatSessionUri.forSession(sessionId); @@ -441,6 +459,28 @@ suite('MainThreadChatSessions', function () { mainThread.$unregisterChatSessionContentProvider(1); }); + test('provideChatSessionContent propagates title', async function () { + const sessionScheme = 'test-session-type'; + mainThread.$registerChatSessionContentProvider(1, sessionScheme); + + const sessionContent = { + id: 'test-session', + title: 'My Session Title', + history: [], + hasActiveResponseCallback: false, + hasRequestHandler: false + }; + + const resource = URI.parse(`${sessionScheme}:/test-session`); + + (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); + const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None); + + assert.strictEqual(session.title, 'My Session Title'); + + mainThread.$unregisterChatSessionContentProvider(1); + }); + test('$handleProgressChunk routes to correct session', async function () { const sessionScheme = 'test-session-type'; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index cb8c99abc8387..267488ad3f4e1 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -612,6 +612,10 @@ export class ChatService extends Disposable implements IChatService { isUntitled: sessionResource.path.startsWith('/untitled-') //TODO(jospicer) }); + if (providedSession.title) { + modelRef.object.setCustomTitle(providedSession.title); + } + const model = modelRef.object; const disposables = new DisposableStore(); disposables.add(modelRef.object.onDidDispose(() => { diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 4c8f12a339f35..ec178df2ab9c1 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -158,6 +158,8 @@ export interface IChatSession extends IDisposable { readonly sessionResource: URI; + readonly title?: string; + readonly history: readonly IChatSessionHistoryItem[]; /** diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 5f93a7fe90990..3219f0b26390e 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -346,6 +346,15 @@ declare module 'vscode' { } export interface ChatSession { + /** + * An optional title for the chat session. + * + * When provided, this title is used as the display name for the session + * (e.g. in the editor tab). When not provided, the title defaults to + * the first user message in the session history. + */ + readonly title?: string; + /** * The full history of the session * From 51e07b8403bfa9bed69e221c3ea8ad6b2fe9bc44 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:50:09 -0800 Subject: [PATCH 24/28] bump the distro (#296672) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4dda3bbce18e..73b8625209ed0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "bd187e4508a244500eb533c56e5cccb6801a699c", + "distro": "9d472fb245bfdeb5eca66384d5bf6a9881fe4965", "author": { "name": "Microsoft Corporation" }, From 655ce6f07e36108a1e5b1722952baff5927b5191 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:54:24 -0800 Subject: [PATCH 25/28] Adopt unified js/ts setting for diagnostic settings For #292934 Also renames validate.enable to `validate.enabled` to align with other settings --- .../typescript-language-features/package.json | 22 +++++++++++ .../package.nls.json | 5 ++- .../src/languageProvider.ts | 9 ++--- .../src/tsServer/bufferSyncSupport.ts | 38 +++++++------------ .../src/typeScriptServiceClientHost.ts | 5 +-- .../src/utils/configuration.ts | 2 +- 6 files changed, 46 insertions(+), 35 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 88dca19cfba32..156003c26cda3 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1366,22 +1366,44 @@ "type": "object", "title": "%configuration.validation%", "properties": { + "js/ts.validate.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.validate.enable%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "typescript.validate.enable": { "type": "boolean", "default": true, "description": "%typescript.validate.enable%", + "markdownDeprecationMessage": "%configuration.validate.enable.unifiedDeprecationMessage%", "scope": "window" }, "javascript.validate.enable": { "type": "boolean", "default": true, "description": "%javascript.validate.enable%", + "markdownDeprecationMessage": "%configuration.validate.enable.unifiedDeprecationMessage%", "scope": "window" }, + "js/ts.reportStyleChecksAsWarnings": { + "type": "boolean", + "default": true, + "description": "%typescript.reportStyleChecksAsWarnings%", + "scope": "window", + "tags": [ + "TypeScript" + ] + }, "typescript.reportStyleChecksAsWarnings": { "type": "boolean", "default": true, "description": "%typescript.reportStyleChecksAsWarnings%", + "markdownDeprecationMessage": "%configuration.reportStyleChecksAsWarnings.unifiedDeprecationMessage%", "scope": "window" }, "js/ts.suggestionActions.enabled": { diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index b27a4f512b94c..b776a5b3cef9d 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -29,6 +29,9 @@ "typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).", "typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.", "typescript.validate.enable": "Enable/disable TypeScript validation.", + "javascript.validate.enable": "Enable/disable JavaScript validation.", + "configuration.validate.enable": "Enable/disable JavaScript and TypeScript validation.", + "configuration.validate.enable.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.validate.enable#` instead.", "typescript.format.enable": "Enable/disable default TypeScript formatter.", "javascript.format.enable": "Enable/disable default JavaScript formatter.", "format.insertSpaceAfterCommaDelimiter": "Defines space handling after a comma delimiter.", @@ -72,7 +75,6 @@ "configuration.format.placeOpenBraceOnNewLineForControlBlocks.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.placeOpenBraceOnNewLineForControlBlocks#` instead.", "configuration.format.semicolons.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.semicolons#` instead.", "configuration.format.indentSwitchCase.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.format.indentSwitchCase#` instead.", - "javascript.validate.enable": "Enable/disable JavaScript validation.", "javascript.goToProjectConfig.title": "Go to Project Configuration (jsconfig / tsconfig)", "typescript.goToProjectConfig.title": "Go to Project Configuration (tsconfig)", "configuration.referencesCodeLens.enabled": "Enable/disable references CodeLens in JavaScript and TypeScript files. This CodeLens shows the number of references for classes and exported functions and allows you to peek or navigate to them.", @@ -89,6 +91,7 @@ "typescript.restartTsServer": "Restart TS Server", "typescript.selectTypeScriptVersion.title": "Select TypeScript Version...", "typescript.reportStyleChecksAsWarnings": "Report style checks as warnings.", + "configuration.reportStyleChecksAsWarnings.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.reportStyleChecksAsWarnings#` instead.", "typescript.npm": "Specifies the path to the npm executable used for [Automatic Type Acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition).", "typescript.check.npmIsInstalled": "Check if npm is installed for [Automatic Type Acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition).", "configuration.suggest.names": "Enable/disable including unique names from the file in JavaScript suggestions. Note that name suggestions are always disabled in JavaScript code that is semantically checked using `@ts-check` or `checkJs`.", diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index cd383a46ec545..230e79a6f7fb0 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -22,9 +22,6 @@ import { readUnifiedConfig } from './utils/configuration'; import { isWeb, isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; -const validateSetting = 'validate.enable'; -const suggestionSetting = 'suggestionActions.enabled'; - export default class LanguageProvider extends Disposable { constructor( @@ -95,9 +92,9 @@ export default class LanguageProvider extends Disposable { } private configurationChanged(): void { - const config = vscode.workspace.getConfiguration(this.id, null); - this.updateValidate(config.get(validateSetting, true)); - this.updateSuggestionDiagnostics(readUnifiedConfig(suggestionSetting, true, { scope: null, fallbackSection: this.id })); + 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 { diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index b60a319e6099a..0e32abdccedc3 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -10,6 +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 { Delayer, setImmediate } from '../utils/async'; import { nulToken } from '../utils/cancellation'; import { Disposable } from '../utils/dispose'; @@ -161,7 +162,7 @@ class SyncedBuffer { private state = BufferState.Initial; constructor( - private readonly document: vscode.TextDocument, + public readonly document: vscode.TextDocument, public readonly filepath: string, private readonly client: ITypeScriptServiceClient, private readonly synchronizer: BufferSynchronizer, @@ -462,9 +463,6 @@ export default class BufferSyncSupport extends Disposable { private readonly client: ITypeScriptServiceClient; - private _validateJavaScript = true; - private _validateTypeScript = true; - private readonly modeIds: Set; private readonly syncedBuffers: SyncedBufferMap; private readonly pendingDiagnostics: PendingDiagnostics; @@ -513,8 +511,14 @@ export default class BufferSyncSupport extends Disposable { } })); - this.updateConfiguration(); - vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this, this._disposables); + this._register(vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(`${unifiedConfigSection}.validate.enabled`) + || e.affectsConfiguration('typescript.validate.enabled') + || e.affectsConfiguration('javascript.validate.enabled') + ) { + this.requestAllDiagnostics(); + } + })); } private readonly _onDelete = this._register(new vscode.EventEmitter()); @@ -756,14 +760,6 @@ export default class BufferSyncSupport extends Disposable { this.pendingDiagnostics.clear(); } - private updateConfiguration() { - const jsConfig = vscode.workspace.getConfiguration('javascript', null); - const tsConfig = vscode.workspace.getConfiguration('typescript', null); - - this._validateJavaScript = jsConfig.get('validate.enable', true); - this._validateTypeScript = tsConfig.get('validate.enable', true); - } - private shouldValidate(buffer: SyncedBuffer): boolean { if (fileSchemes.isOfScheme(buffer.resource, fileSchemes.chatCodeBlock)) { return false; @@ -773,15 +769,9 @@ export default class BufferSyncSupport extends Disposable { return false; } - switch (buffer.languageId) { - case languageModeIds.javascript: - case languageModeIds.javascriptreact: - return this._validateJavaScript; - - case languageModeIds.typescript: - case languageModeIds.typescriptreact: - default: - return this._validateTypeScript; - } + const fallbackSection = (buffer.languageId === languageModeIds.javascript || buffer.languageId === languageModeIds.javascriptreact) + ? 'javascript' + : 'typescript'; + return readUnifiedConfig('validate.enabled', true, { scope: buffer.document, fallbackSection, fallbackSubSectionNameOverride: 'validate.enable' }); } } diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index c44bfc3ac3fd2..86ac03ec225b5 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -14,6 +14,7 @@ import { ServiceConfigurationProvider } from './configuration/configuration'; import { DiagnosticLanguage, LanguageDescription } from './configuration/languageDescription'; import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; import { DiagnosticKind } from './languageFeatures/diagnostics'; +import { readUnifiedConfig } from './utils/configuration'; import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import LanguageProvider from './languageProvider'; import { LogLevelMonitor } from './logging/logLevelMonitor'; @@ -193,9 +194,7 @@ export default class TypeScriptServiceClientHost extends Disposable { } private configurationChanged(): void { - const typescriptConfig = vscode.workspace.getConfiguration('typescript'); - - this.reportStyleCheckAsWarnings = typescriptConfig.get('reportStyleChecksAsWarnings', true); + this.reportStyleCheckAsWarnings = readUnifiedConfig('reportStyleChecksAsWarnings', true, { scope: null, fallbackSection: 'typescript' }); } private async findLanguage(resource: vscode.Uri): Promise { diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index e0038500cf4ba..2b9bca95d6ad9 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -export type UnifiedConfigurationScope = vscode.TextDocument | null | undefined; +export type UnifiedConfigurationScope = vscode.ConfigurationScope | null | undefined; export const unifiedConfigSection = 'js/ts'; From 851ed34c131ed87f63c3cc5a4db1711e93784b2b Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:55:27 -0800 Subject: [PATCH 26/28] Fix ref --- extensions/typescript-language-features/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index b776a5b3cef9d..21d974fcaf2dd 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -31,7 +31,7 @@ "typescript.validate.enable": "Enable/disable TypeScript validation.", "javascript.validate.enable": "Enable/disable JavaScript validation.", "configuration.validate.enable": "Enable/disable JavaScript and TypeScript validation.", - "configuration.validate.enable.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.validate.enable#` instead.", + "configuration.validate.enable.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.validate.enabled#` instead.", "typescript.format.enable": "Enable/disable default TypeScript formatter.", "javascript.format.enable": "Enable/disable default JavaScript formatter.", "format.insertSpaceAfterCommaDelimiter": "Defines space handling after a comma delimiter.", From 2a0ce9055cc83943191a2f816562aaaad7d686a4 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:03:03 -0800 Subject: [PATCH 27/28] Update extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/tsServer/bufferSyncSupport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 0e32abdccedc3..89ccb66646ba5 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -513,8 +513,8 @@ export default class BufferSyncSupport extends Disposable { this._register(vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(`${unifiedConfigSection}.validate.enabled`) - || e.affectsConfiguration('typescript.validate.enabled') - || e.affectsConfiguration('javascript.validate.enabled') + || e.affectsConfiguration('typescript.validate.enable') + || e.affectsConfiguration('javascript.validate.enable') ) { this.requestAllDiagnostics(); } From 405897eb1103a7141acc9113defc0aaa1287a12a Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:10:46 -0800 Subject: [PATCH 28/28] customizations editor (#296680) * ai customizations editor * tidy * tidy * tidy --- .../aiCustomizationManagement/browser/SPEC.md | 179 ----- .../aiCustomizationTreeView/browser/SPEC.md | 116 ---- .../browser/aiCustomizationOverviewView.ts | 14 +- .../aiCustomizationTreeView.contribution.ts | 2 - .../browser/aiCustomizationTreeViewViews.ts | 8 +- .../aiCustomizationWorkspaceService.ts | 62 ++ .../contrib/chat/browser/chat.contribution.ts | 3 + .../customizationsToolbar.contribution.ts | 8 +- src/vs/sessions/sessions.desktop.main.ts | 1 - .../chat/browser/actions/chatActions.ts | 24 + .../aiCustomization/aiCustomizationIcons.ts} | 6 +- .../aiCustomizationListWidget.ts | 186 +++--- .../aiCustomizationManagement.contribution.ts | 53 +- .../aiCustomizationManagement.ts | 32 +- .../aiCustomizationManagementEditor.ts | 626 ++++++++---------- .../aiCustomizationManagementEditorInput.ts | 10 +- .../aiCustomizationWorkspaceService.ts | 75 +++ .../customizationCreatorService.ts | 87 +-- .../browser/aiCustomization}/mcpListWidget.ts | 42 +- .../media/aiCustomizationManagement.css | 4 + .../contrib/chat/browser/chat.contribution.ts | 2 + .../common/aiCustomizationWorkspaceService.ts | 63 ++ 22 files changed, 741 insertions(+), 862 deletions(-) delete mode 100644 src/vs/sessions/contrib/aiCustomizationManagement/browser/SPEC.md delete mode 100644 src/vs/sessions/contrib/aiCustomizationTreeView/browser/SPEC.md rename src/vs/sessions/contrib/{aiCustomizationManagement => aiCustomizationTreeView}/browser/aiCustomizationOverviewView.ts (91%) create mode 100644 src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts rename src/vs/{sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.ts => workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts} (92%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/aiCustomizationListWidget.ts (84%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/aiCustomizationManagement.contribution.ts (80%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/aiCustomizationManagement.ts (74%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/aiCustomizationManagementEditor.ts (56%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/aiCustomizationManagementEditorInput.ts (86%) create mode 100644 src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/customizationCreatorService.ts (72%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/mcpListWidget.ts (89%) rename src/vs/{sessions/contrib/aiCustomizationManagement/browser => workbench/contrib/chat/browser/aiCustomization}/media/aiCustomizationManagement.css (99%) create mode 100644 src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/SPEC.md b/src/vs/sessions/contrib/aiCustomizationManagement/browser/SPEC.md deleted file mode 100644 index edfbe10fbc22a..0000000000000 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/SPEC.md +++ /dev/null @@ -1,179 +0,0 @@ -# AI Customization Management Editor Specification - -## Overview - -The AI Customization Management Editor is a global management surface for AI customizations. It provides sectioned navigation and a content area that switches between prompt lists, MCP servers, models, and an embedded editor. - -**Location:** `src/vs/sessions/contrib/aiCustomizationManagement/browser/` - -**Purpose:** Centralized discovery and management across worktree, user, and extension sources, optimized for agent sessions. - -## Architecture - -### Component Hierarchy - -``` -AICustomizationManagementEditor (EditorPane) -├── SplitView (Horizontal orientation) -│ ├── Sidebar Panel (Left) -│ │ └── WorkbenchList (sections) -│ └── Content Panel (Right) -│ ├── PromptsContent (AICustomizationListWidget) -│ ├── MCP Content (McpListWidget) -│ ├── Models Content (ChatModelsWidget) -│ └── Embedded Editor (CodeEditorWidget) -``` - -### File Structure - -``` -aiCustomizationManagement/browser/ -├── aiCustomizationManagement.ts # IDs + context keys -├── aiCustomizationManagement.contribution.ts # Commands + context menus -├── aiCustomizationManagementEditor.ts # SplitView list/editor -├── aiCustomizationManagementEditorInput.ts # Singleton input -├── aiCustomizationListWidget.ts # Search + grouped list -├── customizationCreatorService.ts # AI-guided creation flow -├── mcpListWidget.ts # MCP servers list -├── aiCustomizationOverviewView.ts # Overview view -└── media/ - └── aiCustomizationManagement.css -``` - -## Key Components - -### AICustomizationManagementEditorInput - -**Pattern:** Singleton editor input with dynamic tab title (section label). - -### AICustomizationManagementEditor - -**Responsibilities:** -- Manages section navigation and content swapping. -- Hosts embedded editor view for prompt files. -- Persists selected section and sidebar width. - -**Sections:** -- Agents, Skills, Instructions, Prompts, Hooks, MCP Servers, Models. - -**Embedded Editor:** -- Uses `CodeEditorWidget` for full editor UX. -- Auto-commits worktree files on exit via agent session command. - -**Overview View:** -- A compact view (`AICustomizationOverviewView`) shows counts and deep-links to sections. - -**Creation flows:** -- Manual create (worktree/user) with snippet templates. -- AI-guided create opens a new chat with hidden system instructions. - -### AICustomizationListWidget - -**Responsibilities:** -- Search + grouped list of prompt files by storage (Worktree/User/Extensions). -- Collapsible group headers. -- Storage badges and git status badges. - - Empty state UI with icon, title, and description. - - Section footer with description + docs link. - -**Search behavior:** -- Fuzzy matches across name, description, and filename. -- Debounced (200ms) filtering. - -**Active session scoping:** -- The active worktree comes from `IActiveSessionService` and is the source of truth for scoping. -- Prompt discovery is scoped by the agentic prompt service override using the active session root. -- Views refresh counts/filters when the active session changes. - -**Context menu actions:** -- Open, Run Prompt (prompts), Reveal in OS, Delete. -- Copy full path / relative path actions. - -**Add button behavior:** -- Primary action targets worktree when available, otherwise user. -- Dropdown offers User creation and AI-generated creation. -- Hooks use the built-in Configure Hooks flow and do not offer user-scoped creation. - -### McpListWidget - -**Responsibilities:** -- Lists MCP servers with status and actions. -- Provides add server flow and docs link. - - Search input with debounced filtering and an empty state. - -### Models Widget - -**Responsibilities:** -- Hosts the chat models management widget with a footer link. - -## Registration & Commands - -- Editor pane registered under `AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID`. -- Command `aiCustomization.openManagementEditor` opens the singleton editor. -- Command visibility and actions are gated by `ChatContextKeys.enabled`. - -## State and Context - -- Selected section and sidebar width are persisted to profile storage. -- Context keys: - - `aiCustomizationManagementEditorFocused` - - `aiCustomizationManagementSection` - -## User Workflows - -### Open Management Editor - -1. Run "Open AI Customizations" from the command palette. -2. Editor opens with the last selected section. - -### Create Items - -1. Use the Add button in the list header. -2. Choose worktree or user location (if available). -3. Optionally use "Generate" to start AI-guided creation. - -This is the only UI surface for creating new customizations. - -### Edit Items - -1. Click an item to open the embedded editor. -2. Use back to return to list; worktree files auto-commit. - -### Context Menu Actions - -1. Right-click a list item. -2. Choose Open, Run Prompt (prompts only), Reveal in OS, or Delete. -3. Use Copy Full Path / Copy Relative Path for quick path access. - -## Integration Points - -- `IPromptsService` for agent/skill/prompt/instructions discovery. -- `parseAllHookFiles` for hooks. -- `IActiveSessionService` for worktree filtering. -- `ISCMService` for git status badges. -- `ITextModelService` and `IFileService` for embedded editor I/O. -- `IDialogService` for delete confirmation and extension-file guardrails. -- `IOpenerService` for docs links and external navigation. - -## Service Alignment (Required) - -AI customizations must lean on existing VS Code services with well-defined interfaces. The management surface should not reimplement discovery, storage rules, or MCP lifecycle behavior. - -Browser compatibility is required. Do not use Node.js APIs; rely on VS Code services that work in browser contexts. - -Required services to prefer: -- Prompt discovery and metadata: [src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts](../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.ts) -- Active session scoping for worktrees: [src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts](../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts) -- MCP servers and connections: [src/vs/workbench/contrib/mcp/common/mcpService.ts](../../../../workbench/contrib/mcp/common/mcpService.ts) -- MCP management and gallery: [src/vs/platform/mcp/common/mcpManagement.ts](../../../../platform/mcp/common/mcpManagement.ts) -- Chat models: [src/vs/workbench/contrib/chat/common/chatService/chatService.ts](../../../../workbench/contrib/chat/common/chatService/chatService.ts) - -## Known Gaps - -- No bulk operations or sorting. -- Search query is not persisted between sessions. -- Hooks docs link is a placeholder and should be updated when available. - ---- - -*This specification documents the AI Customization Management Editor in `src/vs/sessions/contrib/aiCustomizationManagement/browser/`.* diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/SPEC.md b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/SPEC.md deleted file mode 100644 index 77434957f9a6a..0000000000000 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/SPEC.md +++ /dev/null @@ -1,116 +0,0 @@ -# AI Customization Tree View Specification - -## Overview - -The AI Customization Tree View is a sidebar tree that groups AI customization files by type and storage. It is optimized for agent sessions and filters worktree items to the active session repository. - -**Location:** `src/vs/sessions/contrib/aiCustomizationTreeView/browser/` - -## Architecture - -### Component Hierarchy - -``` -View Container (Sidebar) -└── AICustomizationViewPane - └── WorkbenchAsyncDataTree - ├── UnifiedAICustomizationDataSource - ├── AICustomizationTreeDelegate - └── Renderers (category, group, file) -``` - -### Tree Structure - -``` -ROOT -├── Custom Agents -│ ├── Workspace (N) -│ ├── User (N) -│ └── Extensions (N) -├── Skills -│ ├── Workspace (N) -│ ├── User (N) -│ └── Extensions (N) -├── Instructions -└── Prompts -``` - -### File Structure - -``` -aiCustomizationTreeView/browser/ -├── aiCustomizationTreeView.ts -├── aiCustomizationTreeView.contribution.ts -├── aiCustomizationTreeViewViews.ts -├── aiCustomizationTreeViewIcons.ts -└── media/ - └── aiCustomizationTreeView.css -``` - -## Key Components - -### AICustomizationViewPane - -**Responsibilities:** -- Creates the tree and renderers. -- Auto-expands categories on load/refresh. -- Refreshes on prompt service changes, workspace changes, and active session changes. -- Updates `aiCustomization.isEmpty` based on total item count. -- Worktree scoping comes from the agentic prompt service override. - -### UnifiedAICustomizationDataSource - -**Responsibilities:** -- Caches per-type data for efficient expansion. -- Builds storage groups only when items exist. -- Labels groups with counts (e.g., "Workspace (3)"). -- Uses `findAgentSkills()` to derive skill names. - - Logs errors via `ILogService` when fetching children fails. - -## Actions - -### View Title - -- **Refresh** reloads data and re-expands categories. -- **Collapse All** collapses the tree. - -### Context Menu (file items) - -- Open -- Run Prompt (prompts only) - -## Context Keys - -- `aiCustomization.isEmpty` is set based on total items for welcome content. -- `aiCustomizationItemType` controls prompt-specific context menu actions. - -## Accessibility - -- Category/group/file items provide aria labels. -- File item aria labels include description when present. - -## Integration Points - -- `IPromptsService` for agents/skills/instructions/prompts. -- `IActiveSessionService` for worktree filtering. -- `IWorkspaceContextService` to refresh on workspace changes. -- `ILogService` for error reporting during data fetch. - -## Service Alignment (Required) - -AI customizations must lean on existing VS Code services with well-defined interfaces. The tree view should rely on the prompt discovery service rather than scanning the file system directly. - -Required services to prefer: -- Prompt discovery and metadata: [src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts](../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.ts) -- Active session scoping for worktrees: [src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts](../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.ts) - -## Notes - -- Storage groups are labeled with counts; icons are not shown for group rows. -- Skills display the frontmatter name when available, falling back to the folder name. -- Creation actions are intentionally centralized in the Management Editor. -- Refresh clears cached data before rebuilding the tree. - ---- - -*This specification documents the AI Customization Tree View in `src/vs/sessions/contrib/aiCustomizationTreeView/browser/`.* diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts similarity index 91% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts rename to src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index 677fdf24320b7..0eedb76a519eb 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -22,12 +22,12 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { AICustomizationManagementSection } from './aiCustomizationManagement.js'; -import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js'; -import { AICustomizationManagementEditor } from './aiCustomizationManagementEditor.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; +import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; +import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; const $ = DOM.$; @@ -66,7 +66,7 @@ export class AICustomizationOverviewView extends ViewPane { @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IPromptsService private readonly promptsService: IPromptsService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -85,7 +85,7 @@ export class AICustomizationOverviewView extends ViewPane { // Listen to workspace folder changes to update counts this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.loadCounts())); this._register(autorun(reader => { - this.activeSessionService.activeSession.read(reader); + this.workspaceService.activeProjectRoot.read(reader); this.loadCounts(); })); diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.ts index bf2487a9e37ea..a7edc620be229 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.ts @@ -94,5 +94,3 @@ MenuRegistry.appendMenuItem(AICustomizationItemMenuId, { }); //#endregion - -//#endregion diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts index b24b57c831fb0..338e026c3f9ca 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts @@ -27,7 +27,7 @@ import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/ import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IPromptsService, PromptsStorage, IAgentSkill, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { agentIcon, extensionIcon, instructionsIcon, promptIcon, skillIcon, userIcon, workspaceIcon } from './aiCustomizationTreeViewIcons.js'; +import { agentIcon, extensionIcon, instructionsIcon, promptIcon, skillIcon, userIcon, workspaceIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { AICustomizationItemMenuId } from './aiCustomizationTreeView.js'; import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; @@ -35,7 +35,7 @@ import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; //#region Context Keys @@ -487,7 +487,7 @@ export class AICustomizationViewPane extends ViewPane { @IMenuService private readonly menuService: IMenuService, @ILogService private readonly logService: ILogService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -502,7 +502,7 @@ export class AICustomizationViewPane extends ViewPane { // Listen to workspace folder changes to refresh tree this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.refresh())); this._register(autorun(reader => { - this.activeSessionService.activeSession.read(reader); + this.workspaceService.activeProjectRoot.read(reader); this.refresh(); })); diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts new file mode 100644 index 0000000000000..32d2236832b09 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { derived, IObservable } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js'; +import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; + +/** + * Agent Sessions override of IAICustomizationWorkspaceService. + * Delegates to ISessionsManagementService to provide the active session's + * worktree/repository as the project root, and supports worktree commit. + */ +export class SessionsAICustomizationWorkspaceService implements IAICustomizationWorkspaceService { + declare readonly _serviceBrand: undefined; + + readonly activeProjectRoot: IObservable; + + constructor( + @ISessionsManagementService private readonly sessionsService: ISessionsManagementService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + this.activeProjectRoot = derived(reader => { + const session = this.sessionsService.activeSession.read(reader); + return session?.worktree ?? session?.repository; + }); + } + + getActiveProjectRoot(): URI | undefined { + const session = this.sessionsService.getActiveSession(); + return session?.worktree ?? session?.repository; + } + + readonly managementSections: readonly AICustomizationManagementSection[] = [ + AICustomizationManagementSection.Agents, + AICustomizationManagementSection.Skills, + AICustomizationManagementSection.Instructions, + AICustomizationManagementSection.Prompts, + AICustomizationManagementSection.Hooks, + AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Models, + ]; + + readonly preferManualCreation = true; + + async commitFiles(projectRoot: URI, fileUris: URI[]): Promise { + const session = this.sessionsService.getActiveSession(); + if (session) { + await this.sessionsService.commitWorktreeFiles(session, fileUris); + } + } + + async generateCustomization(type: PromptsType): Promise { + const creator = this.instantiationService.createInstance(CustomizationCreatorService); + await creator.createWithAI(type); + } +} diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index d337a84b2db72..878e97879411c 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -30,6 +30,8 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { AgenticPromptsService } from './promptsService.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { ISessionsConfigurationService, SessionsConfigurationService } from './sessionsConfigurationService.js'; +import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { SessionsAICustomizationWorkspaceService } from './aiCustomizationWorkspaceService.js'; import { ChatViewContainerId, ChatViewId } from '../../../../workbench/contrib/chat/browser/chat.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { NewChatViewPane, SessionsViewId } from './newChatViewPane.js'; @@ -232,3 +234,4 @@ registerWorkbenchContribution2(RunScriptContribution.ID, RunScriptContribution, // register services registerSingleton(IPromptsService, AgenticPromptsService, InstantiationType.Delayed); registerSingleton(ISessionsConfigurationService, SessionsConfigurationService, InstantiationType.Delayed); +registerSingleton(IAICustomizationWorkspaceService, SessionsAICustomizationWorkspaceService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index 2d14ab2f63234..c4a021a04d242 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -14,15 +14,15 @@ import { IActionViewItemService } from '../../../../platform/actions/browser/act import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; -import { AICustomizationManagementEditor } from '../../aiCustomizationManagement/browser/aiCustomizationManagementEditor.js'; -import { AICustomizationManagementSection } from '../../aiCustomizationManagement/browser/aiCustomizationManagement.js'; -import { AICustomizationManagementEditorInput } from '../../aiCustomizationManagement/browser/aiCustomizationManagementEditorInput.js'; +import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; +import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; +import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { Menus } from '../../../browser/menus.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, workspaceIcon, userIcon, extensionIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, workspaceIcon, userIcon, extensionIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../base/common/actions.js'; import { $, append } from '../../../../base/browser/dom.js'; diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 14ea745fd17be..6bf70f78d3ba9 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -191,7 +191,6 @@ import './browser/layoutActions.js'; import './contrib/accountMenu/browser/account.contribution.js'; import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.js'; -import './contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.js'; import './contrib/chat/browser/chat.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a5ed9ee7a9e58..1472e7f34c197 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -84,6 +84,7 @@ export const GENERATE_INSTRUCTION_COMMAND_ID = 'workbench.action.chat.generateIn export const GENERATE_PROMPT_COMMAND_ID = 'workbench.action.chat.generatePrompt'; export const GENERATE_SKILL_COMMAND_ID = 'workbench.action.chat.generateSkill'; export const GENERATE_AGENT_COMMAND_ID = 'workbench.action.chat.generateAgent'; +export const GENERATE_HOOK_COMMAND_ID = 'workbench.action.chat.generateHook'; const defaultChat = { manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', @@ -1298,6 +1299,29 @@ export function registerChatActions() { } }); + registerAction2(class GenerateHookAction extends Action2 { + constructor() { + super({ + id: GENERATE_HOOK_COMMAND_ID, + title: localize2('generateHook', "Generate Hook with Agent"), + shortTitle: localize2('generateHook.short', "Generate Hook with Agent"), + category: CHAT_CATEGORY, + icon: Codicon.sparkle, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + mode: 'agent', + query: '/create-hook ', + isPartialQuery: true, + }); + } + }); + registerAction2(class OpenChatFeatureSettingsAction extends Action2 { constructor() { super({ diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts similarity index 92% rename from src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts index fbe3fa1d95008..95299f8e208ef 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Codicon } from '../../../../base/common/codicons.js'; -import { localize } from '../../../../nls.js'; -import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { localize } from '../../../../../nls.js'; +import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js'; /** * Icon for the AI Customization view container (sidebar). diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts similarity index 84% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 9c05cd61f7694..9c6a6ffb64ce6 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -4,46 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import './media/aiCustomizationManagement.css'; -import * as DOM from '../../../../base/browser/dom.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { basename, dirname } from '../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; -import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; -import { IPromptsService, PromptsStorage, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; -import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, userIcon, workspaceIcon, extensionIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; -import { AICustomizationManagementItemMenuId, AICustomizationManagementSection, getActiveSessionRoot } from './aiCustomizationManagement.js'; -import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; -import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { Delayer } from '../../../../base/common/async.js'; -import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; -import { matchesFuzzy, IMatch } from '../../../../base/common/filters.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; -import { IMenuService } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { parseAllHookFiles } from '../../../../workbench/contrib/chat/browser/promptSyntax/hookUtils.js'; -import { OS } from '../../../../base/common/platform.js'; -import { IRemoteAgentService } from '../../../../workbench/services/remote/common/remoteAgentService.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { Action, Separator } from '../../../../base/common/actions.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; -import { ISCMService } from '../../../../workbench/contrib/scm/common/scm.js'; +import * as DOM from '../../../../../base/browser/dom.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { basename, dirname } from '../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { WorkbenchList } from '../../../../../platform/list/browser/listService.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../../base/browser/ui/list/list.js'; +import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promptSyntax/service/promptsService.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, userIcon, workspaceIcon, extensionIcon } from './aiCustomizationIcons.js'; +import { AICustomizationManagementItemMenuId, AICustomizationManagementSection } from './aiCustomizationManagement.js'; +import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js'; +import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { Delayer } from '../../../../../base/common/async.js'; +import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; +import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; +import { matchesFuzzy, IMatch } from '../../../../../base/common/filters.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js'; +import { IMenuService } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IPathService } from '../../../../services/path/common/pathService.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { parseAllHookFiles } from '../promptSyntax/hookUtils.js'; +import { OS } from '../../../../../base/common/platform.js'; +import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; +import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { Action, Separator } from '../../../../../base/common/actions.js'; +import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; +import { ISCMService } from '../../../scm/common/scm.js'; const $ = DOM.$; @@ -231,7 +231,7 @@ class AICustomizationItemRenderer implements IListRenderer(); + private readonly dropdownActionDisposables = this._register(new DisposableStore()); private readonly delayedFilter = new Delayer(200); @@ -324,8 +325,8 @@ export class AICustomizationListWidget extends Disposable { private readonly _onDidRequestCreate = this._register(new Emitter()); readonly onDidRequestCreate: Event = this._onDidRequestCreate.event; - private readonly _onDidRequestCreateManual = this._register(new Emitter<{ type: PromptsType; target: 'worktree' | 'user' }>()); - readonly onDidRequestCreateManual: Event<{ type: PromptsType; target: 'worktree' | 'user' }> = this._onDidRequestCreateManual.event; + private readonly _onDidRequestCreateManual = this._register(new Emitter<{ type: PromptsType; target: 'workspace' | 'user' }>()); + readonly onDidRequestCreateManual: Event<{ type: PromptsType; target: 'workspace' | 'user' }> = this._onDidRequestCreateManual.event; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -340,7 +341,7 @@ export class AICustomizationListWidget extends Disposable { @IPathService private readonly pathService: IPathService, @ILabelService private readonly labelService: ILabelService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @ILogService private readonly logService: ILogService, @IClipboardService private readonly clipboardService: IClipboardService, @ISCMService private readonly scmService: ISCMService, @@ -351,7 +352,7 @@ export class AICustomizationListWidget extends Disposable { this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.refresh())); this._register(autorun(reader => { - this.activeSessionService.activeSession.read(reader); + this.workspaceService.activeProjectRoot.read(reader); this.updateAddButton(); this.refresh(); })); @@ -507,7 +508,7 @@ export class AICustomizationListWidget extends Disposable { await this.clipboardService.writeText(item.uri.fsPath); }), new Action('copyRelativePath', localize('copyRelativePath', "Copy Relative Path"), undefined, true, async () => { - const basePath = getActiveSessionRoot(this.activeSessionService); + const basePath = this.workspaceService.getActiveProjectRoot(); if (basePath && item.uri.fsPath.startsWith(basePath.fsPath)) { const relative = item.uri.fsPath.substring(basePath.fsPath.length + 1); await this.clipboardService.writeText(relative); @@ -580,25 +581,22 @@ export class AICustomizationListWidget extends Disposable { */ private updateAddButton(): void { const typeLabel = this.getTypeLabel(); - if (this.currentSection === AICustomizationManagementSection.Hooks) { - this.addButton.label = `$(${Codicon.add.id}) New ${typeLabel}`; - const hasWorktree = !!this.activeSessionService.getActiveSession()?.worktree; - this.addButton.enabled = hasWorktree; - const disabledTitle = hasWorktree - ? '' - : localize('hooksCreateDisabled', "Open a session with a worktree to configure hooks."); - this.addButton.primaryButton.setTitle(disabledTitle); - this.addButton.dropdownButton.setTitle(disabledTitle); - return; - } this.addButton.primaryButton.setTitle(''); this.addButton.dropdownButton.setTitle(''); this.addButton.enabled = true; - const hasWorktree = this.hasActiveWorktree(); - if (hasWorktree) { - this.addButton.label = `$(${Codicon.add.id}) New ${typeLabel} (Worktree)`; + if (this.workspaceService.preferManualCreation) { + // Sessions: primary is workspace creation + const hasWorkspace = this.hasActiveWorkspace(); + this.addButton.label = `$(${Codicon.add.id}) New ${typeLabel} (Workspace)`; + this.addButton.enabled = hasWorkspace; + if (!hasWorkspace) { + const disabledTitle = localize('createDisabled', "Open a workspace folder to create customizations."); + this.addButton.primaryButton.setTitle(disabledTitle); + this.addButton.dropdownButton.setTitle(disabledTitle); + } } else { - this.addButton.label = `$(${Codicon.add.id}) New ${typeLabel} (User)`; + // Core: primary is AI generation + this.addButton.label = `$(${Codicon.sparkle.id}) Generate ${typeLabel}`; } } @@ -606,30 +604,51 @@ export class AICustomizationListWidget extends Disposable { * Gets the dropdown actions for the add button. */ private getDropdownActions(): Action[] { + this.dropdownActionDisposables.clear(); const typeLabel = this.getTypeLabel(); const actions: Action[] = []; const promptType = sectionToPromptType(this.currentSection); - const hasWorktree = this.hasActiveWorktree(); - if (hasWorktree && promptType !== PromptsType.hook) { - // Primary is worktree - dropdown shows user + generate - actions.push(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => { - this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' }); - })); + // Hooks: no user-scoped creation + if (promptType === PromptsType.hook) { + if (this.workspaceService.preferManualCreation) { + // Sessions: no dropdown for hooks + } else { + // Core: primary is generate, dropdown has configure quick pick + if (this.hasActiveWorkspace()) { + actions.push(this.dropdownActionDisposables.add(new Action('configureHooks', `$(${Codicon.add.id}) Configure Hooks`, undefined, true, () => { + this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); + }))); + } + } + return actions; } - actions.push(new Action('createWithAI', `$(${Codicon.sparkle.id}) Generate ${typeLabel}`, undefined, true, () => { - this._onDidRequestCreate.fire(promptType); - })); + if (this.workspaceService.preferManualCreation) { + // Sessions: primary is workspace, dropdown has user + actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => { + this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' }); + }))); + } else { + // Core: primary is generate, dropdown has workspace + user + if (this.hasActiveWorkspace()) { + actions.push(this.dropdownActionDisposables.add(new Action('createWorkspace', `$(${Codicon.folder.id}) New ${typeLabel} (Workspace)`, undefined, true, () => { + this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); + }))); + } + actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => { + this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' }); + }))); + } return actions; } /** - * Checks if there's an active session root (worktree or repository). + * Checks if there's an active project root (workspace folder or session repository). */ - private hasActiveWorktree(): boolean { - return !!getActiveSessionRoot(this.activeSessionService); + private hasActiveWorkspace(): boolean { + return !!this.workspaceService.getActiveProjectRoot(); } /** @@ -637,11 +656,16 @@ export class AICustomizationListWidget extends Disposable { */ private executePrimaryCreateAction(): void { const promptType = sectionToPromptType(this.currentSection); - if (promptType === PromptsType.hook && !this.activeSessionService.getActiveSession()?.worktree) { - return; + if (this.workspaceService.preferManualCreation) { + // Sessions: primary creates in workspace + if (!this.hasActiveWorkspace()) { + return; + } + this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); + } else { + // Core: primary is generate + this._onDidRequestCreate.fire(promptType); } - const target = this.hasActiveWorktree() || promptType === PromptsType.hook ? 'worktree' : 'user'; - this._onDidRequestCreateManual.fire({ type: promptType, target }); } /** @@ -679,7 +703,7 @@ export class AICustomizationListWidget extends Disposable { const items: IAICustomizationListItem[] = []; const folders = this.workspaceContextService.getWorkspace().folders; - const activeRepo = getActiveSessionRoot(this.activeSessionService); + const activeRepo = this.workspaceService.getActiveProjectRoot(); this.logService.info(`[AICustomizationListWidget] loadItems: section=${this.currentSection}, promptType=${promptType}, workspaceFolders=[${folders.map(f => f.uri.toString()).join(', ')}], activeRepo=${activeRepo?.toString() ?? 'none'}`); @@ -792,7 +816,7 @@ export class AICustomizationListWidget extends Disposable { // Sort items by name items.sort((a, b) => a.name.localeCompare(b.name)); - // Set git status for worktree (local) items + // Set git status for workspace (local) items this.updateGitStatus(items); this.logService.info(`[AICustomizationListWidget] loadItems complete: ${items.length} items loaded [${items.map(i => `${i.name}(${i.storage}:${i.uri.toString()})`).join(', ')}]`); @@ -803,7 +827,7 @@ export class AICustomizationListWidget extends Disposable { } /** - * Updates git status on worktree items by checking SCM resource groups. + * Updates git status on local workspace items by checking SCM resource groups. * Files found in resource groups have uncommitted changes; others are committed. */ private updateGitStatus(items: IAICustomizationListItem[]): void { @@ -875,7 +899,7 @@ export class AICustomizationListWidget extends Disposable { // Group items by storage const groups: { storage: PromptsStorage; label: string; icon: ThemeIcon; items: IAICustomizationListItem[] }[] = [ - { storage: PromptsStorage.local, label: localize('worktreeGroup', "Worktree"), icon: workspaceIcon, items: [] }, + { storage: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, items: [] }, { storage: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, items: [] }, { storage: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, items: [] }, ]; diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts similarity index 80% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts index 9450cd347d37b..cc7a9fcd85a77 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts @@ -3,19 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IEditorPaneRegistry, EditorPaneDescriptor } from '../../../../workbench/browser/editor.js'; -import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../../workbench/common/editor.js'; -import { EditorInput } from '../../../../workbench/common/editor/editorInput.js'; -import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { IEditorPaneRegistry, EditorPaneDescriptor } from '../../../../browser/editor.js'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../../common/editor.js'; +import { EditorInput } from '../../../../common/editor/editorInput.js'; +import { IEditorService, MODAL_GROUP } from '../../../../services/editor/common/editorService.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { AICustomizationManagementEditor } from './aiCustomizationManagementEditor.js'; import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js'; import { @@ -24,18 +23,18 @@ import { AICustomizationManagementCommands, AICustomizationManagementItemMenuId, } from './aiCustomizationManagement.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { basename } from '../../../../base/common/resources.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { isWindows, isMacintosh } from '../../../../base/common/platform.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { basename } from '../../../../../base/common/resources.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { isWindows, isMacintosh } from '../../../../../base/common/platform.js'; //#region Editor Registration @@ -277,9 +276,9 @@ class AICustomizationManagementActionsContribution extends Disposable implements } async run(accessor: ServicesAccessor): Promise { - const editorGroupsService = accessor.get(IEditorGroupsService); + const editorService = accessor.get(IEditorService); const input = AICustomizationManagementEditorInput.getOrCreate(); - await editorGroupsService.activeGroup.openEditor(input, { pinned: true }); + await editorService.openEditor(input, { pinned: true }, MODAL_GROUP); } })); } diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts similarity index 74% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts index 131fbeff9e96d..3245bfab2f21c 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { localize } from '../../../../nls.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js'; +import { localize } from '../../../../../nls.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; + +// Re-export for convenience — consumers import from this file +export { AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js'; /** * Editor pane ID for the AI Customizations Management Editor. @@ -30,21 +32,6 @@ export const AICustomizationManagementCommands = { CreateNewPrompt: 'aiCustomization.createNewPrompt', } as const; -/** - * Section IDs for the sidebar navigation. - */ -export const AICustomizationManagementSection = { - Agents: 'agents', - Skills: 'skills', - Instructions: 'instructions', - Prompts: 'prompts', - Hooks: 'hooks', - McpServers: 'mcpServers', - Models: 'models', -} as const; - -export type AICustomizationManagementSection = typeof AICustomizationManagementSection[keyof typeof AICustomizationManagementSection]; - /** * Context key indicating the AI Customization Management Editor is focused. */ @@ -95,8 +82,3 @@ export const SIDEBAR_DEFAULT_WIDTH = 200; export const SIDEBAR_MIN_WIDTH = 150; export const SIDEBAR_MAX_WIDTH = 350; export const CONTENT_MIN_WIDTH = 400; - -export function getActiveSessionRoot(activeSessionService: ISessionsManagementService): URI | undefined { - const session = activeSessionService.getActiveSession(); - return session?.worktree ?? session?.repository; -} diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts similarity index 56% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 7d2e3de1face5..fa0fb046b52d0 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -4,38 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import './media/aiCustomizationManagement.css'; -import * as DOM from '../../../../base/browser/dom.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { DisposableStore, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Event } from '../../../../base/common/event.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js'; -import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; -import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; -import { localize } from '../../../../nls.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; -import { EditorPane } from '../../../../workbench/browser/parts/editor/editorPane.js'; -import { IEditorOpenContext } from '../../../../workbench/common/editor.js'; -import { IEditorGroup } from '../../../../workbench/services/editor/common/editorGroupsService.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; -import { IListVirtualDelegate, IListRenderer } from '../../../../base/browser/ui/list/list.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { registerColor } from '../../../../platform/theme/common/colorRegistry.js'; -import { PANEL_BORDER } from '../../../../workbench/common/theme.js'; +import * as DOM from '../../../../../base/browser/dom.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { DisposableStore, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Event } from '../../../../../base/common/event.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { Orientation, Sizing, SplitView } from '../../../../../base/browser/ui/splitview/splitview.js'; +import { localize } from '../../../../../nls.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; +import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; +import { EditorPane } from '../../../../browser/parts/editor/editorPane.js'; +import { IEditorOpenContext } from '../../../../common/editor.js'; +import { IEditorGroup } from '../../../../services/editor/common/editorGroupsService.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { WorkbenchList } from '../../../../../platform/list/browser/listService.js'; +import { IListVirtualDelegate, IListRenderer } from '../../../../../base/browser/ui/list/list.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { basename, isEqual } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { registerColor } from '../../../../../platform/theme/common/colorRegistry.js'; +import { PANEL_BORDER } from '../../../../common/theme.js'; import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js'; -import { AICustomizationListWidget, IAICustomizationListItem } from './aiCustomizationListWidget.js'; +import { AICustomizationListWidget } from './aiCustomizationListWidget.js'; import { McpListWidget } from './mcpListWidget.js'; import { AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID, @@ -49,16 +44,22 @@ import { SIDEBAR_MAX_WIDTH, CONTENT_MIN_WIDTH, } from './aiCustomizationManagement.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; -import { ChatModelsWidget } from '../../../../workbench/contrib/chat/browser/chatManagement/chatModelsWidget.js'; -import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; -import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../../../../workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.js'; -import { showConfigureHooksQuickPick } from '../../../../workbench/contrib/chat/browser/promptSyntax/hookActions.js'; -import { CustomizationCreatorService } from './customizationCreatorService.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { IWorkingCopyService } from '../../../../workbench/services/workingCopy/common/workingCopyService.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from './aiCustomizationIcons.js'; +import { ChatModelsWidget } from '../chatManagement/chatModelsWidget.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../promptSyntax/newPromptFileActions.js'; +import { showConfigureHooksQuickPick } from '../promptSyntax/hookActions.js'; +import { resolveWorkspaceTargetDirectory, resolveUserTargetDirectory } from './customizationCreatorService.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js'; +import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditorOptions.js'; +import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; const $ = DOM.$; @@ -129,24 +130,22 @@ export class AICustomizationManagementEditor extends EditorPane { private sectionsList!: WorkbenchList; private contentContainer!: HTMLElement; private listWidget!: AICustomizationListWidget; - private mcpListWidget!: McpListWidget; - private modelsWidget!: ChatModelsWidget; + private mcpListWidget: McpListWidget | undefined; + private modelsWidget: ChatModelsWidget | undefined; private promptsContentContainer!: HTMLElement; - private mcpContentContainer!: HTMLElement; - private modelsContentContainer!: HTMLElement; - private modelsFooterElement!: HTMLElement; - - // Embedded editor state - private editorContentContainer!: HTMLElement; - private embeddedEditorContainer!: HTMLElement; - private embeddedEditor!: CodeEditorWidget; + private mcpContentContainer: HTMLElement | undefined; + private modelsContentContainer: HTMLElement | undefined; + private modelsFooterElement: HTMLElement | undefined; + + // Embedded editor state (sessions only — preferManualCreation) + private editorContentContainer: HTMLElement | undefined; + private embeddedEditor: CodeEditorWidget | undefined; private editorItemNameElement!: HTMLElement; private editorItemPathElement!: HTMLElement; private editorSaveIndicator!: HTMLElement; private readonly editorModelChangeDisposables = this._register(new DisposableStore()); private currentEditingUri: URI | undefined; - private currentActiveSession: IActiveSessionItem | undefined; - private currentEditingIsWorktree = false; + private currentEditingProjectRoot: URI | undefined; private currentModelRef: IReference | undefined; private viewMode: 'list' | 'editor' = 'list'; @@ -155,8 +154,6 @@ export class AICustomizationManagementEditor extends EditorPane { private selectedSection: AICustomizationManagementSection = AICustomizationManagementSection.Agents; private readonly editorDisposables = this._register(new DisposableStore()); - private readonly inputDisposables = this._register(new MutableDisposable()); - private readonly customizationCreator: CustomizationCreatorService; private readonly inEditorContextKey: IContextKey; private readonly sectionContextKey: IContextKey; @@ -169,11 +166,13 @@ export class AICustomizationManagementEditor extends EditorPane { @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService private readonly openerService: IOpenerService, + @ICommandService private readonly commandService: ICommandService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, + @IEditorService private readonly editorService: IEditorService, + @IPromptsService private readonly promptsService: IPromptsService, @ITextModelService private readonly textModelService: ITextModelService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILayoutService private readonly layoutService: ILayoutService, - @ICommandService private readonly commandService: ICommandService, - @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -181,36 +180,43 @@ export class AICustomizationManagementEditor extends EditorPane { this.inEditorContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_EDITOR.bindTo(contextKeyService); this.sectionContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_SECTION.bindTo(contextKeyService); - this.customizationCreator = this.instantiationService.createInstance(CustomizationCreatorService); + // Track workspace changes for embedded editor (sessions only) + if (this.workspaceService.preferManualCreation) { + this._register(autorun(reader => { + this.workspaceService.activeProjectRoot.read(reader); + if (this.viewMode === 'editor') { + this.currentEditingProjectRoot = this.workspaceService.getActiveProjectRoot(); + } + })); + this._register(toDisposable(() => { + this.currentModelRef?.dispose(); + this.currentModelRef = undefined; + })); + } - this._register(autorun(reader => { - this.activeSessionService.activeSession.read(reader); - if (this.viewMode !== 'editor' || !this.currentEditingIsWorktree) { - return; + // Build sections from the workspace service configuration + const sectionInfo: Record = { + [AICustomizationManagementSection.Agents]: { label: localize('agents', "Agents"), icon: agentIcon }, + [AICustomizationManagementSection.Skills]: { label: localize('skills', "Skills"), icon: skillIcon }, + [AICustomizationManagementSection.Instructions]: { label: localize('instructions', "Instructions"), icon: instructionsIcon }, + [AICustomizationManagementSection.Prompts]: { label: localize('prompts', "Prompts"), icon: promptIcon }, + [AICustomizationManagementSection.Hooks]: { label: localize('hooks', "Hooks"), icon: hookIcon }, + [AICustomizationManagementSection.McpServers]: { label: localize('mcpServers', "MCP Servers"), icon: Codicon.server }, + [AICustomizationManagementSection.Models]: { label: localize('models', "Models"), icon: Codicon.vm }, + }; + for (const id of this.workspaceService.managementSections) { + const info = sectionInfo[id]; + if (info) { + this.sections.push({ id, ...info }); } - this.currentActiveSession = this.activeSessionService.getActiveSession() ?? undefined; - })); - - // Safety disposal for the embedded editor model reference - this._register(toDisposable(() => { - this.currentModelRef?.dispose(); - this.currentModelRef = undefined; - })); + } - this.sections.push( - { id: AICustomizationManagementSection.Agents, label: localize('agents', "Agents"), icon: agentIcon }, - { id: AICustomizationManagementSection.Skills, label: localize('skills', "Skills"), icon: skillIcon }, - { id: AICustomizationManagementSection.Instructions, label: localize('instructions', "Instructions"), icon: instructionsIcon }, - { id: AICustomizationManagementSection.Prompts, label: localize('prompts', "Prompts"), icon: promptIcon }, - { id: AICustomizationManagementSection.Hooks, label: localize('hooks', "Hooks"), icon: hookIcon }, - { id: AICustomizationManagementSection.McpServers, label: localize('mcpServers', "MCP Servers"), icon: Codicon.server }, - { id: AICustomizationManagementSection.Models, label: localize('models', "Models"), icon: Codicon.vm }, - ); - - // Restore selected section from storage + // Restore selected section from storage, falling back to first available const savedSection = this.storageService.get(AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, StorageScope.PROFILE); - if (savedSection && Object.values(AICustomizationManagementSection).includes(savedSection as AICustomizationManagementSection)) { + if (savedSection && this.sections.some(s => s.id === savedSection)) { this.selectedSection = savedSection as AICustomizationManagementSection; + } else if (this.sections.length > 0) { + this.selectedSection = this.sections[0].id; } } @@ -262,19 +268,14 @@ export class AICustomizationManagementEditor extends EditorPane { layout: (width, _, height) => { this.contentContainer.style.width = `${width}px`; if (height !== undefined) { - this.listWidget.layout(height - 16, width - 24); // Account for padding - this.mcpListWidget.layout(height - 16, width - 24); // Account for padding - // Models widget has footer, subtract footer height + this.listWidget.layout(height - 16, width - 24); + this.mcpListWidget?.layout(height - 16, width - 24); const modelsFooterHeight = this.modelsFooterElement?.offsetHeight || 80; - this.modelsWidget.layout(height - 16 - modelsFooterHeight, width); - - // Layout embedded editor when in editor mode + this.modelsWidget?.layout(height - 16 - modelsFooterHeight, width); if (this.viewMode === 'editor' && this.embeddedEditor) { - const editorHeaderHeight = 50; // Back button + item info header - const padding = 24; // Content inner padding - const editorHeight = height - editorHeaderHeight - padding; - const editorWidth = width - padding; - this.embeddedEditor.layout({ width: Math.max(0, editorWidth), height: Math.max(0, editorHeight) }); + const editorHeaderHeight = 50; + const padding = 24; + this.embeddedEditor.layout({ width: Math.max(0, width - padding), height: Math.max(0, height - editorHeaderHeight - padding) }); } } }, @@ -344,9 +345,15 @@ export class AICustomizationManagementEditor extends EditorPane { this.listWidget = this.editorDisposables.add(this.instantiationService.createInstance(AICustomizationListWidget)); this.promptsContentContainer.appendChild(this.listWidget.element); - // Handle item selection - open in embedded editor + // Handle item selection this.editorDisposables.add(this.listWidget.onDidSelectItem(item => { - this.openItem(item); + if (this.workspaceService.preferManualCreation) { + const isWorkspaceFile = item.storage === PromptsStorage.local; + const isReadOnly = item.storage === PromptsStorage.extension; + this.showEmbeddedEditor(item.uri, item.name, isWorkspaceFile, isReadOnly); + } else { + this.editorService.openEditor({ resource: item.uri }); + } })); // Handle create actions - AI-guided creation @@ -359,32 +366,37 @@ export class AICustomizationManagementEditor extends EditorPane { this.createNewItemManual(type, target); })); - // Container for Models content - this.modelsContentContainer = DOM.append(contentInner, $('.models-content-container')); - - this.modelsWidget = this.editorDisposables.add(this.instantiationService.createInstance(ChatModelsWidget)); - this.modelsContentContainer.appendChild(this.modelsWidget.element); - - // Models description footer - this.modelsFooterElement = DOM.append(this.modelsContentContainer, $('.section-footer')); - const modelsDescription = DOM.append(this.modelsFooterElement, $('p.section-footer-description')); - modelsDescription.textContent = localize('modelsDescription', "Browse and manage language models from different providers. Select models for use in chat, code completion, and other AI features."); - const modelsLink = DOM.append(this.modelsFooterElement, $('a.section-footer-link')) as HTMLAnchorElement; - modelsLink.textContent = localize('learnMoreModels', "Learn more about language models"); - modelsLink.href = 'https://code.visualstudio.com/docs/copilot/customization/language-models'; - this.editorDisposables.add(DOM.addDisposableListener(modelsLink, 'click', (e) => { - e.preventDefault(); - this.openerService.open(URI.parse(modelsLink.href)); - })); + // Container for Models content (only in sessions) + const hasSections = new Set(this.workspaceService.managementSections); + if (hasSections.has(AICustomizationManagementSection.Models)) { + this.modelsContentContainer = DOM.append(contentInner, $('.models-content-container')); + this.modelsWidget = this.editorDisposables.add(this.instantiationService.createInstance(ChatModelsWidget)); + this.modelsContentContainer.appendChild(this.modelsWidget.element); + + this.modelsFooterElement = DOM.append(this.modelsContentContainer, $('.section-footer')); + const modelsDescription = DOM.append(this.modelsFooterElement, $('p.section-footer-description')); + modelsDescription.textContent = localize('modelsDescription', "Browse and manage language models from different providers. Select models for use in chat, code completion, and other AI features."); + const modelsLink = DOM.append(this.modelsFooterElement, $('a.section-footer-link')) as HTMLAnchorElement; + modelsLink.textContent = localize('learnMoreModels', "Learn more about language models"); + modelsLink.href = 'https://code.visualstudio.com/docs/copilot/customization/language-models'; + this.editorDisposables.add(DOM.addDisposableListener(modelsLink, 'click', (e) => { + e.preventDefault(); + this.openerService.open(URI.parse(modelsLink.href)); + })); + } - // Container for MCP content - this.mcpContentContainer = DOM.append(contentInner, $('.mcp-content-container')); - this.mcpListWidget = this.editorDisposables.add(this.instantiationService.createInstance(McpListWidget)); - this.mcpContentContainer.appendChild(this.mcpListWidget.element); + // Container for MCP content (only in sessions) + if (hasSections.has(AICustomizationManagementSection.McpServers)) { + this.mcpContentContainer = DOM.append(contentInner, $('.mcp-content-container')); + this.mcpListWidget = this.editorDisposables.add(this.instantiationService.createInstance(McpListWidget)); + this.mcpContentContainer.appendChild(this.mcpListWidget.element); + } - // Container for embedded editor view - this.editorContentContainer = DOM.append(contentInner, $('.editor-content-container')); - this.createEmbeddedEditor(); + // Embedded editor container (sessions only) + if (this.workspaceService.preferManualCreation) { + this.editorContentContainer = DOM.append(contentInner, $('.editor-content-container')); + this.createEmbeddedEditor(); + } // Set initial visibility based on selected section this.updateContentVisibility(); @@ -408,7 +420,6 @@ export class AICustomizationManagementEditor extends EditorPane { return; } - // If in editor view, go back to list first if (this.viewMode === 'editor') { this.goBackToList(); } @@ -444,14 +455,19 @@ export class AICustomizationManagementEditor extends EditorPane { const isModelsSection = this.selectedSection === AICustomizationManagementSection.Models; const isMcpSection = this.selectedSection === AICustomizationManagementSection.McpServers; - // Hide all list containers when in editor mode this.promptsContentContainer.style.display = !isEditorMode && isPromptsSection ? '' : 'none'; - this.modelsContentContainer.style.display = !isEditorMode && isModelsSection ? '' : 'none'; - this.mcpContentContainer.style.display = !isEditorMode && isMcpSection ? '' : 'none'; - this.editorContentContainer.style.display = isEditorMode ? '' : 'none'; + if (this.modelsContentContainer) { + this.modelsContentContainer.style.display = !isEditorMode && isModelsSection ? '' : 'none'; + } + if (this.mcpContentContainer) { + this.mcpContentContainer.style.display = !isEditorMode && isMcpSection ? '' : 'none'; + } + if (this.editorContentContainer) { + this.editorContentContainer.style.display = isEditorMode ? '' : 'none'; + } // Render and layout models widget when switching to it - if (isModelsSection) { + if (isModelsSection && this.modelsWidget) { this.modelsWidget.render(); if (this.dimension) { this.layout(this.dimension); @@ -459,215 +475,53 @@ export class AICustomizationManagementEditor extends EditorPane { } } - private openItem(item: IAICustomizationListItem): void { - const isWorktreeFile = item.storage === PromptsStorage.local; - const isReadOnly = item.storage === PromptsStorage.extension; - this.showEmbeddedEditor(item.uri, item.name, isWorktreeFile, isReadOnly); - } - - /** - * Creates the embedded editor container with back button and CodeEditorWidget. - */ - private createEmbeddedEditor(): void { - // Header with back button and item info - const editorHeader = DOM.append(this.editorContentContainer, $('.editor-header')); - - // Back button - const backButton = DOM.append(editorHeader, $('button.editor-back-button')); - backButton.setAttribute('aria-label', localize('backToList', "Back to list")); - const backIcon = DOM.append(backButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}`)); - backIcon.setAttribute('aria-hidden', 'true'); - - this.editorDisposables.add(DOM.addDisposableListener(backButton, 'click', () => { - this.goBackToList(); - })); - - // Item info - const itemInfo = DOM.append(editorHeader, $('.editor-item-info')); - this.editorItemNameElement = DOM.append(itemInfo, $('.editor-item-name')); - this.editorItemPathElement = DOM.append(itemInfo, $('.editor-item-path')); - - // Save indicator (right-aligned in header) - this.editorSaveIndicator = DOM.append(editorHeader, $('.editor-save-indicator')); - - // Editor container - this.embeddedEditorContainer = DOM.append(this.editorContentContainer, $('.embedded-editor-container')); - - // Overflow widgets container - appended to the workbench root container so - // hovers, suggest widgets, etc. are not clipped by overflow:hidden parents. - const overflowWidgetsDomNode = this.layoutService.getContainer(DOM.getWindow(this.embeddedEditorContainer)).appendChild($('.embedded-editor-overflow-widgets.monaco-editor')); - this.editorDisposables.add(toDisposable(() => overflowWidgetsDomNode.remove())); - - // Create the CodeEditorWidget - const editorOptions = { - ...getSimpleEditorOptions(this.configurationService), - readOnly: false, - minimap: { enabled: false }, - lineNumbers: 'on' as const, - wordWrap: 'on' as const, - scrollBeyondLastLine: false, - automaticLayout: false, - folding: true, - renderLineHighlight: 'all' as const, - scrollbar: { - vertical: 'auto' as const, - horizontal: 'auto' as const, - }, - overflowWidgetsDomNode, - }; - - this.embeddedEditor = this.editorDisposables.add(this.instantiationService.createInstance( - CodeEditorWidget, - this.embeddedEditorContainer, - editorOptions, - { - isSimpleWidget: false, - // Use default contributions for full IntelliSense, completions, linting, etc. - } - )); - } - - /** - * Shows the embedded editor with the content of the given item. - */ - private async showEmbeddedEditor(uri: URI, displayName: string, isWorktreeFile = false, isReadOnly = false): Promise { - // Dispose previous model reference if any - this.currentModelRef?.dispose(); - this.currentModelRef = undefined; - this.currentEditingUri = uri; - - this.viewMode = 'editor'; - - // Update header info - this.editorItemNameElement.textContent = displayName; - this.editorItemPathElement.textContent = basename(uri); - - // Track worktree URI for auto-commit on close - this.currentActiveSession = isWorktreeFile ? this.activeSessionService.getActiveSession() ?? undefined : undefined; - this.currentEditingIsWorktree = isWorktreeFile; - - // Update visibility - this.updateContentVisibility(); - - try { - // Get the text model for the file - const ref = await this.textModelService.createModelReference(uri); - this.currentModelRef = ref; - this.embeddedEditor.setModel(ref.object.textEditorModel); - this.embeddedEditor.updateOptions({ readOnly: isReadOnly }); - - // Layout the editor - if (this.dimension) { - this.layout(this.dimension); - } - - // Focus the editor - this.embeddedEditor.focus(); - - // Listen for content changes to show saving spinner - this.editorModelChangeDisposables.clear(); - this.editorModelChangeDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => { - this.showSavingSpinner(); - })); - // Listen for actual save events to show checkmark - this.editorModelChangeDisposables.add(this.workingCopyService.onDidSave(e => { - if (isEqual(e.workingCopy.resource, uri)) { - this.showSavedCheckmark(); - } - })); - } catch (error) { - // If we can't load the model, go back to the list - console.error('Failed to load model for embedded editor:', error); - this.goBackToList(); - } - } - - /** - * Goes back from the embedded editor view to the list view. - */ - private goBackToList(): void { - // Auto-commit worktree files when leaving the embedded editor - const fileUri = this.currentEditingUri; - const session = this.currentActiveSession; - if (fileUri && session) { - this.commitWorktreeFile(session, fileUri); - } - - // Dispose model reference - this.currentModelRef?.dispose(); - this.currentModelRef = undefined; - this.currentEditingUri = undefined; - this.currentActiveSession = undefined; - this.currentEditingIsWorktree = false; - this.editorModelChangeDisposables.clear(); - this.clearSaveIndicator(); - - // Clear editor model - this.embeddedEditor.setModel(null); - - this.viewMode = 'list'; - - // Update visibility - this.updateContentVisibility(); - - // Re-layout - if (this.dimension) { - this.layout(this.dimension); - } - - // Focus the list - this.listWidget?.focusSearch(); - } - /** * Creates a new customization using the AI-guided flow. - * Closes the management editor and opens a chat session with a hidden - * custom agent that guides the user through creating the customization. */ private async createNewItemWithAI(type: PromptsType): Promise { - // Close the management editor first so the chat is focused - if (this.input) { - await this.group.closeEditor(this.input); - } - - await this.customizationCreator.createWithAI(type); + this.close(); + await this.workspaceService.generateCustomization(type); } /** - * Creates a new prompt file. If there's an active worktree, asks the user - * whether to save in the worktree or user directory first. + * Creates a new prompt file and opens it. + * In sessions (preferManualCreation), uses the embedded editor with commit-on-close. + * In core, opens in a regular editor tab. */ - private async createNewItemManual(type: PromptsType, target: 'worktree' | 'user'): Promise { - // TODO: When creating a workspace customization file via 'New Workspace X', - // the file is written directly to the worktree but there is currently no way - // to commit it so it shows up in the Changes diff view for the worktree. - // We need integration with the git worktree to stage/commit these new files. + private async createNewItemManual(type: PromptsType, target: 'workspace' | 'user'): Promise { + const useEmbeddedEditor = this.workspaceService.preferManualCreation; if (type === PromptsType.hook) { - const isWorktree = target === 'worktree'; + const isWorkspace = target === 'workspace'; await this.instantiationService.invokeFunction(showConfigureHooksQuickPick, { - openEditor: async (resource, options) => { - await this.showEmbeddedEditor(resource, basename(resource), isWorktree); + openEditor: async (resource) => { + if (useEmbeddedEditor) { + await this.showEmbeddedEditor(resource, basename(resource), isWorkspace); + } else { + await this.editorService.openEditor({ resource }); + } return; }, - onHookFileCreated: isWorktree ? (_uri) => { - // Worktree tracking is handled via showEmbeddedEditor's isWorktreeFile param - } : undefined, }); return; } - const targetDir = target === 'worktree' - ? this.customizationCreator.resolveTargetDirectory(type) - : await this.customizationCreator.resolveUserDirectory(type); + const targetDir = target === 'workspace' + ? resolveWorkspaceTargetDirectory(this.workspaceService, type) + : await resolveUserTargetDirectory(this.promptsService, type); - const isWorktree = target === 'worktree'; const options: INewPromptOptions = { targetFolder: targetDir, targetStorage: target === 'user' ? PromptsStorage.user : PromptsStorage.local, openFile: async (uri) => { - await this.showEmbeddedEditor(uri, basename(uri), isWorktree); - return this.embeddedEditor; + if (useEmbeddedEditor) { + const isWorkspace = target === 'workspace'; + await this.showEmbeddedEditor(uri, basename(uri), isWorkspace); + return this.embeddedEditor; + } else { + await this.editorService.openEditor({ resource: uri }); + return undefined; + } }, }; @@ -681,8 +535,6 @@ export class AICustomizationManagementEditor extends EditorPane { } await this.commandService.executeCommand(commandId, options); - - // Refresh the list so the new item appears void this.listWidget.refresh(); } @@ -709,13 +561,9 @@ export class AICustomizationManagementEditor extends EditorPane { override clearInput(): void { this.inEditorContextKey.set(false); - this.inputDisposables.clear(); - - // Clean up embedded editor state if (this.viewMode === 'editor') { this.goBackToList(); } - super.clearInput(); } @@ -730,7 +578,6 @@ export class AICustomizationManagementEditor extends EditorPane { override focus(): void { super.focus(); - // When in editor mode, focus the editor if (this.viewMode === 'editor') { this.embeddedEditor?.focus(); return; @@ -756,40 +603,129 @@ export class AICustomizationManagementEditor extends EditorPane { } /** - * Shows the spinning loader to indicate unsaved changes. + * Refreshes the list widget. */ - private showSavingSpinner(): void { - this.editorSaveIndicator.className = 'editor-save-indicator visible'; - this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); - this.editorSaveIndicator.title = localize('saving', "Saving..."); + public refreshList(): void { + void this.listWidget.refresh(); } - /** - * Shows the checkmark after the file has been saved to disk. - */ - private showSavedCheckmark(): void { - this.editorSaveIndicator.className = 'editor-save-indicator visible saved'; - this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.check)); - this.editorSaveIndicator.title = localize('saved', "Saved"); + private close(): void { + if (!this.workspaceService.preferManualCreation && this.input) { + this.group.closeEditor(this.input); + } } - private clearSaveIndicator(): void { - this.editorSaveIndicator.className = 'editor-save-indicator'; - this.editorSaveIndicator.title = ''; + //#region Embedded Editor (sessions only) + + private createEmbeddedEditor(): void { + if (!this.editorContentContainer) { + return; + } + + const editorHeader = DOM.append(this.editorContentContainer, $('.editor-header')); + + const backButton = DOM.append(editorHeader, $('button.editor-back-button')); + backButton.setAttribute('aria-label', localize('backToList', "Back to list")); + const backIcon = DOM.append(backButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}`)); + backIcon.setAttribute('aria-hidden', 'true'); + this.editorDisposables.add(DOM.addDisposableListener(backButton, 'click', () => { + this.goBackToList(); + })); + + const itemInfo = DOM.append(editorHeader, $('.editor-item-info')); + this.editorItemNameElement = DOM.append(itemInfo, $('.editor-item-name')); + this.editorItemPathElement = DOM.append(itemInfo, $('.editor-item-path')); + this.editorSaveIndicator = DOM.append(editorHeader, $('.editor-save-indicator')); + + const embeddedEditorContainer = DOM.append(this.editorContentContainer, $('.embedded-editor-container')); + const overflowWidgetsDomNode = this.layoutService.getContainer(DOM.getWindow(embeddedEditorContainer)).appendChild($('.embedded-editor-overflow-widgets.monaco-editor')); + this.editorDisposables.add(toDisposable(() => overflowWidgetsDomNode.remove())); + + this.embeddedEditor = this.editorDisposables.add(this.instantiationService.createInstance( + CodeEditorWidget, + embeddedEditorContainer, + { + ...getSimpleEditorOptions(this.configurationService), + readOnly: false, + minimap: { enabled: false }, + lineNumbers: 'on' as const, + wordWrap: 'on' as const, + scrollBeyondLastLine: false, + automaticLayout: false, + folding: true, + renderLineHighlight: 'all' as const, + scrollbar: { vertical: 'auto' as const, horizontal: 'auto' as const }, + overflowWidgetsDomNode, + }, + { isSimpleWidget: false } + )); } - /** - * Commits a worktree file via the extension and refreshes the Changes view. - */ - private async commitWorktreeFile(session: IActiveSessionItem, fileUri: URI): Promise { - await this.activeSessionService.commitWorktreeFiles(session, [fileUri]); - this.refreshList(); + private async showEmbeddedEditor(uri: URI, displayName: string, isWorkspaceFile = false, isReadOnly = false): Promise { + this.currentModelRef?.dispose(); + this.currentModelRef = undefined; + this.currentEditingUri = uri; + this.currentEditingProjectRoot = isWorkspaceFile ? this.workspaceService.getActiveProjectRoot() : undefined; + this.viewMode = 'editor'; + + this.editorItemNameElement.textContent = displayName; + this.editorItemPathElement.textContent = basename(uri); + this.updateContentVisibility(); + + try { + const ref = await this.textModelService.createModelReference(uri); + this.currentModelRef = ref; + this.embeddedEditor!.setModel(ref.object.textEditorModel); + this.embeddedEditor!.updateOptions({ readOnly: isReadOnly }); + + if (this.dimension) { + this.layout(this.dimension); + } + this.embeddedEditor!.focus(); + + this.editorModelChangeDisposables.clear(); + this.editorModelChangeDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => { + this.editorSaveIndicator.className = 'editor-save-indicator visible'; + this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + this.editorSaveIndicator.title = localize('saving', "Saving..."); + })); + this.editorModelChangeDisposables.add(this.workingCopyService.onDidSave(e => { + if (isEqual(e.workingCopy.resource, uri)) { + this.editorSaveIndicator.className = 'editor-save-indicator visible saved'; + this.editorSaveIndicator.classList.add(...ThemeIcon.asClassNameArray(Codicon.check)); + this.editorSaveIndicator.title = localize('saved', "Saved"); + } + })); + } catch (error) { + console.error('Failed to load model for embedded editor:', error); + this.goBackToList(); + } } - /** - * Refreshes the list widget. - */ - public refreshList(): void { - void this.listWidget.refresh(); + private goBackToList(): void { + // Auto-commit workspace files when leaving the embedded editor + const fileUri = this.currentEditingUri; + const projectRoot = this.currentEditingProjectRoot; + if (fileUri && projectRoot) { + this.workspaceService.commitFiles(projectRoot, [fileUri]); + } + + this.currentModelRef?.dispose(); + this.currentModelRef = undefined; + this.currentEditingUri = undefined; + this.currentEditingProjectRoot = undefined; + this.editorModelChangeDisposables.clear(); + this.editorSaveIndicator.className = 'editor-save-indicator'; + this.editorSaveIndicator.title = ''; + this.embeddedEditor?.setModel(null); + this.viewMode = 'list'; + this.updateContentVisibility(); + + if (this.dimension) { + this.layout(this.dimension); + } + this.listWidget?.focusSearch(); } + + //#endregion } diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditorInput.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.ts similarity index 86% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditorInput.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.ts index 9c1f70b7a7fdf..1ee4543ddacc1 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Codicon } from '../../../../base/common/codicons.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { localize } from '../../../../nls.js'; -import { IUntypedEditorInput } from '../../../../workbench/common/editor.js'; -import { EditorInput } from '../../../../workbench/common/editor/editorInput.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; +import { IUntypedEditorInput } from '../../../../common/editor.js'; +import { EditorInput } from '../../../../common/editor/editorInput.js'; import { AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID } from './aiCustomizationManagement.js'; /** diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts new file mode 100644 index 0000000000000..73da9592df78d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js'; +import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { + GENERATE_AGENT_COMMAND_ID, + GENERATE_HOOK_COMMAND_ID, + GENERATE_INSTRUCTION_COMMAND_ID, + GENERATE_PROMPT_COMMAND_ID, + GENERATE_SKILL_COMMAND_ID, +} from '../actions/chatActions.js'; + +class AICustomizationWorkspaceService implements IAICustomizationWorkspaceService { + declare readonly _serviceBrand: undefined; + + readonly activeProjectRoot: IObservable; + + constructor( + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ICommandService private readonly commandService: ICommandService, + ) { + const workspaceFolders = observableFromEventOpts( + { owner: this }, + this.workspaceContextService.onDidChangeWorkspaceFolders, + () => this.workspaceContextService.getWorkspace().folders + ); + this.activeProjectRoot = derived(reader => { + const folders = workspaceFolders.read(reader); + return folders[0]?.uri; + }); + } + + getActiveProjectRoot(): URI | undefined { + const folders = this.workspaceContextService.getWorkspace().folders; + return folders[0]?.uri; + } + + readonly managementSections: readonly AICustomizationManagementSection[] = [ + AICustomizationManagementSection.Agents, + AICustomizationManagementSection.Skills, + AICustomizationManagementSection.Instructions, + AICustomizationManagementSection.Prompts, + AICustomizationManagementSection.Hooks, + ]; + + readonly preferManualCreation = false; + + async commitFiles(_projectRoot: URI, _fileUris: URI[]): Promise { + // No-op in core VS Code. + } + + async generateCustomization(type: PromptsType): Promise { + const commandIds: Partial> = { + [PromptsType.agent]: GENERATE_AGENT_COMMAND_ID, + [PromptsType.skill]: GENERATE_SKILL_COMMAND_ID, + [PromptsType.instructions]: GENERATE_INSTRUCTION_COMMAND_ID, + [PromptsType.prompt]: GENERATE_PROMPT_COMMAND_ID, + [PromptsType.hook]: GENERATE_HOOK_COMMAND_ID, + }; + const commandId = commandIds[type]; + if (commandId) { + await this.commandService.executeCommand(commandId); + } + } +} + +registerSingleton(IAICustomizationWorkspaceService, AICustomizationWorkspaceService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts similarity index 72% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts index e1e8717e6afaa..e8188abf63b04 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts @@ -3,18 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; -import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; -import { ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; -import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; -import { getPromptFileDefaultLocations } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js'; -import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { localize } from '../../../../nls.js'; -import { getActiveSessionRoot } from './aiCustomizationManagement.js'; +import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; +import { IChatWidgetService } from '../chat.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { ChatModeKind } from '../../common/constants.js'; +import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; +import { getPromptFileDefaultLocations } from '../../common/promptSyntax/config/promptFileLocations.js'; +import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; +import { localize } from '../../../../../nls.js'; /** * Service that opens an AI-guided chat session to help the user create @@ -30,7 +29,7 @@ export class CustomizationCreatorService { @ICommandService private readonly commandService: ICommandService, @IChatService private readonly chatService: IChatService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, + @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IPromptsService private readonly promptsService: IPromptsService, @IQuickInputService private readonly quickInputService: IQuickInputService, ) { } @@ -54,11 +53,11 @@ export class CustomizationCreatorService { const trimmedName = name.trim(); // TODO: The 'Generate X' flow currently opens a new chat that is not connected - // to the active worktree. For this to fully work, the background agent needs to - // accept a worktree parameter so the new session can write files into the correct - // worktree directory and have those changes tracked in the session's diff view. + // to the active workspace. For this to fully work, the background agent needs to + // accept a workspace parameter so the new session can write files into the correct + // directory and have those changes tracked. - // Capture worktree BEFORE opening new chat (which changes active session) + // Capture project root BEFORE opening new chat (which may change active session) const targetDir = this.resolveTargetDirectory(type); const systemInstructions = buildAgentInstructions(type, targetDir, trimmedName); const userMessage = buildUserMessage(type, targetDir, trimmedName); @@ -89,40 +88,44 @@ export class CustomizationCreatorService { } /** - * Returns the worktree and repository URIs from the active session. - */ - /** - * Resolves the worktree directory for a new customization file based on the - * active session's worktree (preferred) or repository path. - * Falls back to the first local source folder from promptsService.getSourceFolders() - * if there's no active worktree. + * Resolves the workspace directory for a new customization file based on the + * active project root. */ resolveTargetDirectory(type: PromptsType): URI | undefined { - const basePath = getActiveSessionRoot(this.activeSessionService); - if (!basePath) { - return undefined; - } - - // Compute the path within the worktree using default locations - const defaultLocations = getPromptFileDefaultLocations(type); - const localLocation = defaultLocations.find(loc => loc.storage === PromptsStorage.local); - if (!localLocation) { - return basePath; - } - - return URI.joinPath(basePath, localLocation.path); + return resolveWorkspaceTargetDirectory(this.workspaceService, type); } /** * Resolves the user-level directory for a new customization file. - * Delegates to IPromptsService.getSourceFolders() which knows the correct - * user data profile path. */ async resolveUserDirectory(type: PromptsType): Promise { - const folders = await this.promptsService.getSourceFolders(type); - const userFolder = folders.find(f => f.storage === PromptsStorage.user); - return userFolder?.uri; + return resolveUserTargetDirectory(this.promptsService, type); + } +} + +/** + * Resolves the workspace directory for a new customization file based on the active project root. + */ +export function resolveWorkspaceTargetDirectory(workspaceService: IAICustomizationWorkspaceService, type: PromptsType): URI | undefined { + const basePath = workspaceService.getActiveProjectRoot(); + if (!basePath) { + return undefined; + } + const defaultLocations = getPromptFileDefaultLocations(type); + const localLocation = defaultLocations.find(loc => loc.storage === PromptsStorage.local); + if (!localLocation) { + return basePath; } + return URI.joinPath(basePath, localLocation.path); +} + +/** + * Resolves the user-level directory for a new customization file. + */ +export async function resolveUserTargetDirectory(promptsService: IPromptsService, type: PromptsType): Promise { + const folders = await promptsService.getSourceFolders(type); + const userFolder = folders.find(f => f.storage === PromptsStorage.user); + return userFolder?.uri; } //#region Agent Instructions diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts similarity index 89% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/mcpListWidget.ts rename to src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index cf97aeb03f828..f717537db3023 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -4,27 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import './media/aiCustomizationManagement.css'; -import * as DOM from '../../../../base/browser/dom.js'; -import { Disposable, DisposableStore, isDisposable } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; -import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IMcpWorkbenchService, IWorkbenchMcpServer, McpConnectionState, IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; -import { McpCommandIds } from '../../../../workbench/contrib/mcp/common/mcpCommandIds.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { URI } from '../../../../base/common/uri.js'; -import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; -import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { Delayer } from '../../../../base/common/async.js'; -import { IAction, Separator } from '../../../../base/common/actions.js'; -import { getContextMenuActions } from '../../../../workbench/contrib/mcp/browser/mcpServerActions.js'; +import * as DOM from '../../../../../base/browser/dom.js'; +import { Disposable, DisposableStore, isDisposable } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { WorkbenchList } from '../../../../../platform/list/browser/listService.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../../base/browser/ui/list/list.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IMcpWorkbenchService, IWorkbenchMcpServer, McpConnectionState, IMcpService } from '../../../../contrib/mcp/common/mcpTypes.js'; +import { McpCommandIds } from '../../../../contrib/mcp/common/mcpCommandIds.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js'; +import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; +import { Delayer } from '../../../../../base/common/async.js'; +import { IAction, Separator } from '../../../../../base/common/actions.js'; +import { getContextMenuActions } from '../../../../contrib/mcp/browser/mcpServerActions.js'; const $ = DOM.$; diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css similarity index 99% rename from src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css rename to src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index a307cfa98dfce..2f99422e94076 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -131,6 +131,10 @@ white-space: nowrap; } +.ai-customization-list-widget .list-add-button .monaco-button-dropdown .monaco-button:first-child { + flex: 1; +} + .ai-customization-list-widget .list-container { flex: 1; overflow: hidden; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e577783419713..dc773171f9d41 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -114,6 +114,8 @@ import { ChatEditorInput, ChatEditorInputSerializer } from './widgetHosts/editor import { ChatLayoutService } from './widget/chatLayoutService.js'; import { ChatLanguageModelsDataContribution, LanguageModelsConfigurationService } from './languageModelsConfigurationService.js'; import './chatManagement/chatManagement.contribution.js'; +import './aiCustomization/aiCustomizationWorkspaceService.js'; +import './aiCustomization/aiCustomizationManagement.contribution.js'; import { ChatOutputRendererService, IChatOutputRendererService } from './chatOutputItemRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts new file mode 100644 index 0000000000000..b64f5ca45f656 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IObservable } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { PromptsType } from './promptSyntax/promptTypes.js'; + +export const IAICustomizationWorkspaceService = createDecorator('aiCustomizationWorkspaceService'); + +/** + * Possible section IDs for the AI Customization Management Editor sidebar. + */ +export const AICustomizationManagementSection = { + Agents: 'agents', + Skills: 'skills', + Instructions: 'instructions', + Prompts: 'prompts', + Hooks: 'hooks', + McpServers: 'mcpServers', + Models: 'models', +} as const; + +export type AICustomizationManagementSection = typeof AICustomizationManagementSection[keyof typeof AICustomizationManagementSection]; + +/** + * Provides workspace context for AI Customization views. + */ +export interface IAICustomizationWorkspaceService { + readonly _serviceBrand: undefined; + + /** + * Observable that fires when the active project root changes. + */ + readonly activeProjectRoot: IObservable; + + /** + * Returns the current active project root, if any. + */ + getActiveProjectRoot(): URI | undefined; + + /** + * The sections to show in the AI Customization Management Editor sidebar. + */ + readonly managementSections: readonly AICustomizationManagementSection[]; + + /** + * Whether the primary creation action should create a file directly + */ + readonly preferManualCreation: boolean; + + /** + * Commits files in the active project. + */ + commitFiles(projectRoot: URI, fileUris: URI[]): Promise; + + /** + * Launches the AI-guided creation flow for the given customization type. + */ + generateCustomization(type: PromptsType): Promise; +}