Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { RenameToolId } from '../../tools/renameTool.js';
import { IMcpService } from '../../../../mcp/common/mcpTypes.js';
import { IConfigurationResolverService } from '../../../../../services/configurationResolver/common/configurationResolver.js';
import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
import { IWorkspaceAgentPluginActivationService } from '../../workspaceAgentPluginActivation.js';
import { AgentCustomizationSyncProvider } from './agentCustomizationSyncProvider.js';
import { type ILocalCustomizationSyncOptions, resolveCustomizationRefs, resolveLocalCustomAgents } from './agentHostLocalCustomizations.js';
import { toolDataToDefinition } from './agentHostToolUtils.js';
Expand Down Expand Up @@ -121,6 +122,7 @@ class AgentCustomizationScope extends Disposable {
@IFileService private readonly _fileService: IFileService,
@IPromptsService private readonly _promptsService: IPromptsService,
@IAgentPluginService private readonly _agentPluginService: IAgentPluginService,
@IWorkspaceAgentPluginActivationService private readonly _workspaceAgentPluginActivationService: IWorkspaceAgentPluginActivationService,
@IInstantiationService instantiationService: IInstantiationService,
@IMcpService private readonly _mcpService: IMcpService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService,
Expand All @@ -133,6 +135,7 @@ class AgentCustomizationScope extends Disposable {
const seq = ++this._updateSeq;
let completedInitialResolution = false;
try {
await this._workspaceAgentPluginActivationService.reconcile(this._roots.slice(0, 1));
const [refs, agents] = await Promise.all([
resolveCustomizationRefs(
this._fileService,
Expand All @@ -147,7 +150,7 @@ class AgentCustomizationScope extends Disposable {
this._roots,
this._windowRemoteAuthority,
),
resolveLocalCustomAgents(this._fileService, this._promptsService, this._syncProvider, this._agentPluginService, this._sessionType, this._options),
resolveLocalCustomAgents(this._fileService, this._promptsService, this._syncProvider, this._agentPluginService, this._sessionType, this._options, this._roots),
]);
if (seq !== this._updateSeq) {
return;
Expand Down Expand Up @@ -186,6 +189,7 @@ class AgentCustomizationScope extends Disposable {
this._promptsService.onDidChangeInstructions,
)(() => scheduleUpdate()));
this._register(autorun(reader => {
const workspaceFolder = this._roots[0];
for (const plugin of this._agentPluginService.plugins.read(reader)) {
plugin.enablement.read(reader);
plugin.hooks.read(reader);
Expand All @@ -194,6 +198,9 @@ class AgentCustomizationScope extends Disposable {
plugin.agents.read(reader);
plugin.instructions.read(reader);
plugin.mcpServerDefinitions.read(reader);
if (workspaceFolder) {
this._agentPluginService.getWorkspaceConfiguredEnablement(plugin, workspaceFolder, reader);
}
}
scheduleUpdate();
}));
Expand Down Expand Up @@ -300,17 +307,19 @@ export class AgentHostActiveClientService extends Disposable implements IAgentHo
}

acquireScope(sessionType: string, roots: readonly URI[]): IAgentCustomizationScope {
const orderedRoots = deduplicateRoots(roots, this._uriIdentityService.extUri);
const normalizedRoots = normalizeRoots(roots, this._uriIdentityService.extUri);
const scopeKey = getScopeKey(normalizedRoots, this._uriIdentityService.extUri);
const serviceScopeKey = getServiceScopeKey(sessionType, scopeKey);
const primaryRootKey = orderedRoots[0] ? this._uriIdentityService.extUri.getComparisonKey(orderedRoots[0]) : undefined;
const serviceScopeKey = getServiceScopeKey(sessionType, primaryRootKey, scopeKey);
let scope = this._scopes.get(serviceScopeKey);
if (!scope) {
// A host that does not share the client's filesystem needs user storage shipped over the wire.
const options = isRemoteAgentHostSessionType(sessionType) ? { includeUserStorage: true } : undefined;
const createdScope: AgentCustomizationScope = this._instantiationService.createInstance(
AgentCustomizationScope,
sessionType,
normalizedRoots,
orderedRoots,
scopeKey,
this.getSyncProvider(sessionType),
options,
Expand Down Expand Up @@ -451,33 +460,40 @@ export class AgentHostActiveClientService extends Disposable implements IAgentHo
}

function normalizeRoots(roots: readonly URI[], extUri: IExtUri): readonly URI[] {
const rootsByUri = new ResourceMap<URI>(root => extUri.getComparisonKey(root));
for (const root of roots) {
rootsByUri.set(root, root);
}
// Ordinal (not locale) ordering: this order feeds `getScopeKey`, whose hash
// becomes an on-disk plugin cache directory name on the agent host side.
return [...rootsByUri.values()].sort((a, b) => {
return [...deduplicateRoots(roots, extUri)].sort((a, b) => {
const left = extUri.getComparisonKey(a);
const right = extUri.getComparisonKey(b);
return left < right ? -1 : left > right ? 1 : 0;
});
}

function deduplicateRoots(roots: readonly URI[], extUri: IExtUri): readonly URI[] {
const rootsByUri = new ResourceMap<URI>(root => extUri.getComparisonKey(root));
for (const root of roots) {
rootsByUri.set(root, root);
}
return [...rootsByUri.values()];
}

/** Returns whether two working-directory sets describe the same customization scope. */
export function areCustomizationScopeRootsEqual(first: readonly URI[] | undefined, second: readonly URI[], extUri: IExtUri): boolean {
const toComparisonKey = (root: URI) => extUri.getComparisonKey(root);
const firstRoots = new ResourceSet(first ?? [], toComparisonKey);
const secondRoots = new ResourceSet(second, toComparisonKey);
return firstRoots.size === secondRoots.size && [...firstRoots].every(root => secondRoots.has(root));
const firstPrimary = first?.[0];
const secondPrimary = second[0];
const samePrimary = firstPrimary === undefined
? secondPrimary === undefined
: secondPrimary !== undefined && extUri.isEqual(firstPrimary, secondPrimary);
return samePrimary && firstRoots.size === secondRoots.size && [...firstRoots].every(root => secondRoots.has(root));
}

function getScopeKey(roots: readonly URI[], extUri: IExtUri): string {
return roots.map(root => extUri.getComparisonKey(root)).join('\n');
}

function getServiceScopeKey(sessionType: string, scopeKey: string): string {
return JSON.stringify([sessionType, scopeKey]);
function getServiceScopeKey(sessionType: string, primaryRootKey: string | undefined, scopeKey: string): string {
return JSON.stringify([sessionType, primaryRootKey, scopeKey]);
}

function createScopeAuthority(sessionType: string, scopeKey: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export async function resolveLocalCustomAgents(
agentPluginService: IAgentPluginService,
sessionType: string,
options: ILocalCustomizationSyncOptions | undefined,
workingDirectories: readonly URI[] = [],
): Promise<readonly AgentCustomization[]> {
const plugins = agentPluginService.plugins.get();
const result: AgentCustomization[] = [];
Expand All @@ -154,7 +155,7 @@ export async function resolveLocalCustomAgents(
const enumerated = await enumerateLocalCustomizationsForHarness(promptsService, syncProvider, sessionType, CancellationToken.None, options);

for (const agent of enumerated) {
if (agent.type !== PromptsType.agent || agent.disabled) {
if (agent.type !== PromptsType.agent) {
continue;
}
const plugin = agent.source === AICustomizationSources.plugin
Expand All @@ -163,6 +164,12 @@ export async function resolveLocalCustomAgents(
if (agent.source === AICustomizationSources.plugin && !plugin) {
continue;
}
const workspaceConfiguredEnablement = plugin && workingDirectories[0]
? agentPluginService.getWorkspaceConfiguredEnablement(plugin, workingDirectories[0])
: undefined;
if (workspaceConfiguredEnablement === false || (agent.disabled && workspaceConfiguredEnablement !== true)) {
continue;
}
const pluginAgent = plugin?.agents.get().find(candidate => candidate.uri.toString() === agent.uri.toString());
pending.push((async () => {
let name = pluginAgent?.name ?? basename(agent.uri, '.agent.md');
Expand Down Expand Up @@ -255,15 +262,28 @@ export async function resolveCustomizationRefs(
// ignored, sync will probably fail later though...
}

let enablement = withCustomizationEnablement(undefined, CustomizationEnablementKind.Global, {
kind: CustomizationEnablementKind.Global,
enabled: agentPluginService.enablementModel.readProfileEnabled(key),
});
const workspaceFolder = workingDirectories[0];
const workspaceConfiguredEnablement = workspaceFolder
? agentPluginService.getWorkspaceConfiguredEnablement(plugin, workspaceFolder)
: undefined;
if (workspaceConfiguredEnablement !== undefined && workspaceFolder) {
enablement = withCustomizationEnablement(enablement, CustomizationEnablementKind.Workspace, {
kind: CustomizationEnablementKind.Workspace,
uri: workspaceFolder.toString() as ProtocolURI,
enabled: workspaceConfiguredEnablement,
});
}

const ref: ClientPluginCustomization = {
type: CustomizationType.Plugin,
id: customizationId(key),
uri: key as ProtocolURI,
name: plugin.label,
enablement: withCustomizationEnablement(undefined, CustomizationEnablementKind.Global, {
kind: CustomizationEnablementKind.Global,
enabled: agentPluginService.enablementModel.readProfileEnabled(key),
}),
enablement,
};
if (nonce !== undefined) {
ref.nonce = nonce.toString(16);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js';
import { CodexStatusBarEntry } from './chatStatus/codexStatusEntry.js';
import { ChatTipService, IChatTipService } from './chatTipService.js';
import { ChatWindowNotifier } from './chatWindowNotifier.js';
import { AgentPluginRecommendations } from './claudePluginRecommendations.js';
import { IWorkspaceAgentPluginActivationService, WorkspaceAgentPluginActivation, WorkspaceAgentPluginActivationService } from './workspaceAgentPluginActivation.js';
import { ChatCodeBlockContextProviderService } from './codeBlockContextProviderService.js';
import { ExploreAgentDefaultModel } from './exploreAgentDefaultModel.js';
import { HasByokModelsContribution } from './hasByokModelsContribution.js';
Expand Down Expand Up @@ -3365,7 +3365,7 @@ registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContrib
registerWorkbenchContribution2(PromptLanguageFeaturesProvider.ID, PromptLanguageFeaturesProvider, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(ChatWindowNotifier.ID, ChatWindowNotifier, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatRepoInfoContribution.ID, ChatRepoInfoContribution, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(AgentPluginRecommendations.ID, AgentPluginRecommendations, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(WorkspaceAgentPluginActivation.ID, WorkspaceAgentPluginActivation, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(AgentPluginCommandsContribution.ID, AgentPluginCommandsContribution, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(PluginAutoUpdate.ID, PluginAutoUpdate, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(ChatReferenceAttachmentWidgetContribution.ID, ChatReferenceAttachmentWidgetContribution, WorkbenchPhase.AfterRestored);
Expand Down Expand Up @@ -3439,6 +3439,7 @@ registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType
registerSingleton(IAgentPluginService, AgentPluginService, InstantiationType.Delayed);
registerSingleton(IPluginMarketplaceService, PluginMarketplaceService, InstantiationType.Delayed);
registerSingleton(IWorkspacePluginSettingsService, WorkspacePluginSettingsService, InstantiationType.Delayed);
registerSingleton(IWorkspaceAgentPluginActivationService, WorkspaceAgentPluginActivationService, InstantiationType.Delayed);
registerSingleton(IAgentPluginRepositoryService, AgentPluginRepositoryService, InstantiationType.Delayed);
registerSingleton(IPluginGitService, BrowserPluginGitCommandService, InstantiationType.Delayed);
registerSingleton(IPluginInstallService, PluginInstallService, InstantiationType.Delayed);
Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions src/vs/workbench/contrib/chat/browser/pluginInstallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickin
import { IPathService } from '../../../services/path/common/pathService.js';
import { IAgentPluginRepositoryService } from '../common/plugins/agentPluginRepositoryService.js';
import { ChatConfiguration } from '../common/constants.js';
import { IPluginInstallService, IInstallPluginFromSourceOptions, IInstallPluginFromSourceResult, IUpdateAllPluginsOptions, IUpdateAllPluginsResult } from '../common/plugins/pluginInstallService.js';
import { IPluginInstallService, IInstallMarketplacePluginOptions, IInstallPluginFromSourceOptions, IInstallPluginFromSourceResult, IUpdateAllPluginsOptions, IUpdateAllPluginsResult } from '../common/plugins/pluginInstallService.js';
import { IMarketplacePlugin, IMarketplaceReference, IPluginMarketplaceService, MarketplaceReferenceKind, MarketplaceType, hasSourceChanged, parseMarketplaceReference, parseMarketplaceReferences, PluginSourceKind, readConfiguredMarketplaces } from '../common/plugins/pluginMarketplaceService.js';

const maxPluginSubdirectoryLength = 8192;
Expand All @@ -45,9 +45,9 @@ export class PluginInstallService implements IPluginInstallService {
@IPathService private readonly _pathService: IPathService,
) { }

async installPlugin(plugin: IMarketplacePlugin, token: CancellationToken = CancellationToken.None): Promise<void> {
async installPlugin(plugin: IMarketplacePlugin, token: CancellationToken = CancellationToken.None, options?: IInstallMarketplacePluginOptions): Promise<void> {
this.throwIfCancelled(token);
if (!await this._ensureMarketplaceTrusted(plugin, token)) {
if (!options?.skipTrust && !await this._ensureMarketplaceTrusted(plugin, token)) {
throw new CancellationError();
}
this.throwIfCancelled(token);
Expand Down
Loading
Loading