diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4fd8b274b75f61..954b313b84a88f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -653,12 +653,11 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['preview'], }, - [ChatConfiguration.PluginPaths]: { + [ChatConfiguration.PluginLocations]: { 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: {}, + markdownDescription: nls.localize('chat.pluginLocations', "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, relative to the workspace root, or start with `~/` for the user's home directory."), scope: ConfigurationScope.MACHINE, tags: ['experimental'], }, @@ -1323,6 +1322,13 @@ Registry.as(Extensions.ConfigurationMigration). return []; } }, + { + key: 'chat.plugins.paths', + migrateFn: (value: unknown, _accessor) => ([ + ['chat.plugins.paths', { value: undefined }], + [ChatConfiguration.PluginLocations, { value }] + ]) + }, ]); class ChatResolverContribution extends Disposable { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 52fe2267506755..9ed80f0451e936 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -11,7 +11,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey export enum ChatConfiguration { AIDisabled = 'chat.disableAIFeatures', PluginsEnabled = 'chat.plugins.enabled', - PluginPaths = 'chat.plugins.paths', + PluginLocations = 'chat.pluginLocations', PluginMarketplaces = 'chat.plugins.marketplaces', AgentEnabled = 'chat.agent.enabled', PlanAgentDefaultModel = 'chat.planAgent.defaultModel', diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index fa2ed06dbc4565..bfd8c768e5e6ff 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -5,6 +5,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; +import { untildify } from '../../../../../base/common/labels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { cloneAndChange } from '../../../../../base/common/objects.js'; @@ -699,7 +700,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery { - private readonly _pluginPathsConfig: IObservable>; + private readonly _pluginLocationsConfig: IObservable>; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -711,13 +712,13 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery @IInstantiationService instantiationService: IInstantiationService, ) { super(fileService, pathService, logService, instantiationService); - this._pluginPathsConfig = observableConfigValue>(ChatConfiguration.PluginPaths, {}, _configurationService); + this._pluginLocationsConfig = observableConfigValue>(ChatConfiguration.PluginLocations, {}, _configurationService); } public override start(): void { const scheduler = this._register(new RunOnceScheduler(() => this._refreshPlugins(), 0)); this._register(autorun(reader => { - this._pluginPathsConfig.read(reader); + this._pluginLocationsConfig.read(reader); scheduler.schedule(); })); scheduler.schedule(); @@ -725,14 +726,15 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery protected override async _discoverPluginSources(): Promise { const sources: IPluginSource[] = []; - const config = this._pluginPathsConfig.get(); + const config = this._pluginLocationsConfig.get(); + const userHome = await this._getUserHome(); for (const [path, enabled] of Object.entries(config)) { if (!path.trim()) { continue; } - const resources = this._resolvePluginPath(path.trim()); + const resources = this._resolvePluginPath(path.trim(), userHome); for (const resource of resources) { let stat; try { @@ -762,11 +764,23 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery return sources; } + private async _getUserHome(): Promise { + const userHome = await this._pathService.userHome(); + return userHome.scheme === 'file' ? userHome.fsPath : userHome.path; + } + /** - * Resolves a plugin path to one or more resource URIs. Absolute paths are - * used directly; relative paths are resolved against each workspace folder. + * Resolves a plugin path to one or more resource URIs. Supports: + * - Absolute paths (used directly) + * - Tilde paths (expanded to user home directory) + * - Relative paths (resolved against each workspace folder) */ - private _resolvePluginPath(path: string): URI[] { + private _resolvePluginPath(path: string, userHome: string): URI[] { + if (path.startsWith('~')) { + path = untildify(path, userHome); + } + + // Handle absolute paths if (win32.isAbsolute(path) || posix.isAbsolute(path)) { return [URI.file(path)]; } @@ -781,7 +795,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery * 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); + const inspected = this._configurationService.inspect>(ChatConfiguration.PluginLocations); // Walk from most specific to least specific to find where this key is defined const targets = [ @@ -797,7 +811,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery const mapping = getConfigValueInTarget(inspected, target); if (mapping && Object.prototype.hasOwnProperty.call(mapping, configKey)) { this._configurationService.updateValue( - ChatConfiguration.PluginPaths, + ChatConfiguration.PluginLocations, { ...mapping, [configKey]: value }, target, ); @@ -808,18 +822,18 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery // Key not found in any target; write to USER_LOCAL as default const current = getConfigValueInTarget(inspected, ConfigurationTarget.USER_LOCAL) ?? {}; this._configurationService.updateValue( - ChatConfiguration.PluginPaths, + ChatConfiguration.PluginLocations, { ...current, [configKey]: value }, ConfigurationTarget.USER_LOCAL, ); } /** - * Removes a plugin path from `chat.plugins.paths` in the most specific + * Removes a plugin path from `chat.pluginLocations` in the most specific * config target where the key is defined. */ private _removePluginPath(configKey: string): void { - const inspected = this._configurationService.inspect>(ChatConfiguration.PluginPaths); + const inspected = this._configurationService.inspect>(ChatConfiguration.PluginLocations); const targets = [ ConfigurationTarget.WORKSPACE_FOLDER, @@ -836,7 +850,7 @@ export class ConfiguredAgentPluginDiscovery extends AbstractAgentPluginDiscovery const updated = { ...mapping }; delete updated[configKey]; this._configurationService.updateValue( - ChatConfiguration.PluginPaths, + ChatConfiguration.PluginLocations, updated, target, );