Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Comment on lines 48 to +50
PromptsStorage.plugin,
PromptsStorage.extension,
PromptsStorage.builtIn,
Expand All @@ -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.
Expand All @@ -86,19 +99,16 @@ export async function enumerateLocalCustomizationsForHarness(
): Promise<readonly ILocalCustomizationFile[]> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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');
Expand Down
Loading