From cff2cd069f217832d89269a2c330b4236b7b94b5 Mon Sep 17 00:00:00 2001 From: Paul Wang <8560030+pwang347@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:03:30 -0700 Subject: [PATCH] Fix configured customizations in Agents window Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a4bf98f8-855c-4524-8aa4-52ad8d1abc9c --- .../agentHost/agentHostLocalCustomizations.ts | 30 +++++++++----- ...erateLocalCustomizationsForHarness.test.ts | 30 ++++++++++++-- .../resolveCustomizationRefs.test.ts | 41 +++++++++++++++++-- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts index 18e5781b97d560..e69925b0ab325d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.ts @@ -11,8 +11,8 @@ import { CustomizationEnablementKind, type AgentCustomization, CustomizationType import { customizationId, type ClientPluginCustomization } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { withCustomizationEnablement } from '../../../../../../platform/agentHost/common/customizationEnablement.js'; import { AICustomizationSource, AICustomizationSources } from '../../../common/aiCustomizationWorkspaceService.js'; -import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; -import { IPromptsService, isUserToggleableCustomization, matchesSessionType, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { PromptFileSource, PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { type IPromptPath, IPromptsService, isUserToggleableCustomization, matchesSessionType, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; import { type ICustomizationSyncProvider } from '../../../common/customizationHarnessService.js'; import { IAgentPlugin, IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; import { IMcpService } from '../../../../mcp/common/mcpTypes.js'; @@ -37,14 +37,17 @@ export const SYNCABLE_PROMPT_TYPES: readonly PromptsType[] = [ ]; /** - * Storage sources whose contents are auto-synced by default. Remote agent - * registrations can additionally include user storage. + * Storage sources whose contents may be auto-synced. Local and user storage + * are filtered to configured locations unless the remote host needs all user + * storage. * * `builtin` only yields skills bundled with the Agents app (e.g. `/create-pr`, * `/merge`); for every other prompt type the prompts service returns nothing, * and in the regular VS Code workbench window it returns nothing at all. */ export const SYNCABLE_STORAGE_SOURCES: readonly PromptsStorage[] = [ + PromptsStorage.local, + PromptsStorage.user, PromptsStorage.plugin, PromptsStorage.extension, PromptsStorage.builtIn, @@ -63,6 +66,16 @@ export interface ILocalCustomizationFile { readonly extensionId?: string; } +function shouldSyncPromptFile(file: IPromptPath, storage: PromptsStorage, options: ILocalCustomizationSyncOptions | undefined): boolean { + if (storage === PromptsStorage.local) { + return file.source === PromptFileSource.ConfigWorkspace; + } + if (storage === PromptsStorage.user) { + return options?.includeUserStorage === true || file.source === PromptFileSource.ConfigPersonal; + } + return true; +} + /** * Enumerates all local customization files eligible for auto-sync to an * agent host harness, annotating each with whether the user has opted out. @@ -86,19 +99,16 @@ export async function enumerateLocalCustomizationsForHarness( ): Promise { const result: ILocalCustomizationFile[] = []; const seenUris = new ResourceSet(); - const storageSources = options?.includeUserStorage - ? [PromptsStorage.user, ...SYNCABLE_STORAGE_SOURCES] - : SYNCABLE_STORAGE_SOURCES; for (const type of SYNCABLE_PROMPT_TYPES) { const userDisabled = promptsService.getDisabledPromptFiles(type); const lists = await Promise.all( - storageSources.map(storage => promptsService.listPromptFilesForStorage(type, storage, token)), + SYNCABLE_STORAGE_SOURCES.map(storage => promptsService.listPromptFilesForStorage(type, storage, token)), ); for (let i = 0; i < lists.length; i++) { - const source = storageSources[i]; + const source = SYNCABLE_STORAGE_SOURCES[i]; const userToggleable = isUserToggleableCustomization(type, source); for (const file of lists[i]) { - if (matchesSessionType(file.sessionTypes, sessionType) && !seenUris.has(file.uri)) { + if (shouldSyncPromptFile(file, source, options) && matchesSessionType(file.sessionTypes, sessionType) && !seenUris.has(file.uri)) { seenUris.add(file.uri); result.push({ uri: file.uri, diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/enumerateLocalCustomizationsForHarness.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/enumerateLocalCustomizationsForHarness.test.ts index c9527532bc89e0..819d8d526cb402 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/enumerateLocalCustomizationsForHarness.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/enumerateLocalCustomizationsForHarness.test.ts @@ -12,12 +12,12 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { enumerateLocalCustomizationsForHarness } from '../../../browser/agentSessions/agentHost/agentHostLocalCustomizations.js'; import { AICustomizationSources, BUILTIN_STORAGE } from '../../../common/aiCustomizationWorkspaceService.js'; import { type ICustomizationSyncProvider } from '../../../common/customizationHarnessService.js'; -import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { PromptFileSource, PromptsType } from '../../../common/promptSyntax/promptTypes.js'; import { type IPromptPath, type IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; import { SessionType } from '../../../common/chatSessionsService.js'; -function makePromptPath(uri: URI, type: PromptsType, storage: PromptsStorage): IPromptPath { - return { uri, type, storage } as IPromptPath; +function makePromptPath(uri: URI, type: PromptsType, storage: PromptsStorage, source?: PromptFileSource): IPromptPath { + return { uri, type, storage, source } as IPromptPath; } function makePromptsService( @@ -82,6 +82,30 @@ suite('enumerateLocalCustomizationsForHarness', () => { ]); }); + test('includes configured local and personal locations without syncing default locations', async () => { + const defaultWorkspaceAgent = URI.file('/workspace/.github/agents/default.agent.md'); + const configuredWorkspaceAgent = URI.file('/workspace/custom/agents/configured.agent.md'); + const defaultPersonalSkill = URI.file('/home/user/.copilot/skills/default/SKILL.md'); + const configuredPersonalSkill = URI.file('/home/user/custom/skills/configured/SKILL.md'); + const promptsService = makePromptsService(new Map([ + [`${PromptsType.agent}/${PromptsStorage.local}`, [ + makePromptPath(defaultWorkspaceAgent, PromptsType.agent, PromptsStorage.local, PromptFileSource.GitHubWorkspace), + makePromptPath(configuredWorkspaceAgent, PromptsType.agent, PromptsStorage.local, PromptFileSource.ConfigWorkspace), + ]], + [`${PromptsType.skill}/${PromptsStorage.user}`, [ + makePromptPath(defaultPersonalSkill, PromptsType.skill, PromptsStorage.user, PromptFileSource.CopilotPersonal), + makePromptPath(configuredPersonalSkill, PromptsType.skill, PromptsStorage.user, PromptFileSource.ConfigPersonal), + ]], + ])); + + const result = await enumerateLocalCustomizationsForHarness(promptsService, new FakeSyncProvider(), SessionType.CopilotCLI, CancellationToken.None, undefined); + + assert.deepStrictEqual(result.map(item => ({ uri: item.uri.toString(), source: item.source })), [ + { uri: configuredWorkspaceAgent.toString(), source: AICustomizationSources.local }, + { uri: configuredPersonalSkill.toString(), source: AICustomizationSources.user }, + ]); + }); + test('marks built-in skills disabled when the sync provider says so', async () => { const builtin = URI.file('/builtin/create-pr/SKILL.md'); const promptsService = makePromptsService(new Map([ diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts index f87a74a02303f7..1736007e2d6f5f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/resolveCustomizationRefs.test.ts @@ -19,19 +19,19 @@ import { IFileService } from '../../../../../../platform/files/common/files.js'; import { McpServerType } from '../../../../../../platform/mcp/common/mcpPlatformTypes.js'; import { resolveCustomizationRefs, resolveLocalCustomAgents } from '../../../browser/agentSessions/agentHost/agentHostLocalCustomizations.js'; import { type ISyncableFile, type ISyncableMcpServer, type SyncedCustomizationBundler } from '../../../browser/agentSessions/agentHost/syncedCustomizationBundler.js'; -import { BUILTIN_STORAGE } from '../../../common/aiCustomizationWorkspaceService.js'; +import { AICustomizationSources, BUILTIN_STORAGE } from '../../../common/aiCustomizationWorkspaceService.js'; import { type ICustomizationSyncProvider } from '../../../common/customizationHarnessService.js'; import { ContributionEnablementState } from '../../../common/enablement.js'; import { type IAgentPlugin, type IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; -import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { PromptFileSource, PromptsType } from '../../../common/promptSyntax/promptTypes.js'; import { type IPromptPath, type IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; import { type IMcpServer, type IMcpService, McpCollectionDefinition, McpServerLaunch, McpServerTransportType } from '../../../../mcp/common/mcpTypes.js'; import { IConfigurationResolverService } from '../../../../../services/configurationResolver/common/configurationResolver.js'; import { ConfigurationResolverExpression } from '../../../../../services/configurationResolver/common/configurationResolverExpression.js'; import { SessionType } from '../../../common/chatSessionsService.js'; -function makePromptPath(uri: URI, type: PromptsType, storage: PromptsStorage): IPromptPath { - return { uri, type, storage } as IPromptPath; +function makePromptPath(uri: URI, type: PromptsType, storage: PromptsStorage, source?: PromptFileSource): IPromptPath { + return { uri, type, storage, source } as IPromptPath; } /** @@ -232,6 +232,39 @@ suite('resolveCustomizationRefs - built-in skills', () => { assert.strictEqual(refs[0].name, 'Open Plugin'); }); + test('bundles configured locations without bundling default workspace files', async () => { + const defaultWorkspaceAgent = URI.file('/workspace/.github/agents/default.agent.md'); + const configuredWorkspaceAgent = URI.file('/workspace/custom/agents/configured.agent.md'); + const configuredPersonalSkill = URI.file('/home/user/custom/skills/configured/SKILL.md'); + const promptsService = makePromptsService(new Map([ + [`${PromptsType.agent}/${PromptsStorage.local}`, [ + makePromptPath(defaultWorkspaceAgent, PromptsType.agent, PromptsStorage.local, PromptFileSource.GitHubWorkspace), + makePromptPath(configuredWorkspaceAgent, PromptsType.agent, PromptsStorage.local, PromptFileSource.ConfigWorkspace), + ]], + [`${PromptsType.skill}/${PromptsStorage.user}`, [ + makePromptPath(configuredPersonalSkill, PromptsType.skill, PromptsStorage.user, PromptFileSource.ConfigPersonal), + ]], + ])); + const bundler = new FakeBundler(); + + await resolveCustomizationRefs( + makeFileService(), + promptsService, + new FakeSyncProvider(), + makeAgentPluginService(), + makeMcpService(), + makeConfigurationResolverService(), + bundler as unknown as SyncedCustomizationBundler, + SessionType.CopilotCLI, + undefined, + ); + + assert.deepStrictEqual(bundler.received[0].map(file => ({ uri: file.uri.toString(), type: file.type, source: file.source })), [ + { uri: configuredWorkspaceAgent.toString(), type: PromptsType.agent, source: AICustomizationSources.local }, + { uri: configuredPersonalSkill.toString(), type: PromptsType.skill, source: AICustomizationSources.user }, + ]); + }); + test('omits disabled built-in skills from the bundle', async () => { const enabled = URI.file('/builtin/create-pr/SKILL.md'); const disabled = URI.file('/builtin/merge/SKILL.md');