Skip to content
Merged
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 @@ -266,8 +266,8 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
async getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
const allCommands = await this.promptsService.getPromptSlashCommands(token);
return allCommands.filter(cmd => {
const filter = this.getStorageSourceFilter(cmd.promptPath.type);
return applyStorageSourceFilter([cmd.promptPath], filter).length > 0;
const filter = this.getStorageSourceFilter(cmd.type);
return applyStorageSourceFilter([cmd], filter).length > 0;
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/vs/sessions/contrib/chat/browser/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export class SlashCommandHandler extends Disposable {
}

const args = match[2]?.trim() ?? '';
const uri = promptCommand.promptPath.uri;
const typeLabel = promptCommand.promptPath.type === PromptsType.skill ? 'skill' : 'prompt file';
const uri = promptCommand.uri;
const typeLabel = promptCommand.type === PromptsType.skill ? 'skill' : 'prompt file';
const expanded = `Use the ${typeLabel} located at [${promptCommand.name}](${uri.toString()}).`;
return args ? `${expanded} ${args}` : expanded;
}
Expand Down Expand Up @@ -297,7 +297,7 @@ export class SlashCommandHandler extends Disposable {
}

const promptCommands = await this.aiCustomizationWorkspaceService.getFilteredPromptSlashCommands(token);
const userInvocable = promptCommands.filter(c => c.parsedPromptFile?.header?.userInvocable !== false);
const userInvocable = promptCommands.filter(c => c.userInvocable);
if (userInvocable.length === 0) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ export async function getSourceCounts(
// Must match loadItems: uses getPromptSlashCommands() filtering out skills
const commands = await promptsService.getPromptSlashCommands(CancellationToken.None);
for (const c of commands) {
if (c.promptPath.type === PromptsType.skill) {
if (c.type === PromptsType.skill) {
continue;
}
items.push({ storage: c.promptPath.storage, uri: c.promptPath.uri });
items.push({ storage: c.storage, uri: c.uri });
}
} else if (promptType === PromptsType.instructions) {
// Must match loadItems: uses listPromptFiles + listAgentInstructions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ function createMockPromptsServiceWithCounts(counts?: ICustomizationCounts): IPro
}));
const skills = Array.from({ length: counts?.skills ?? 0 }, (_, i) => fakeItem('skill', i));
const prompts = Array.from({ length: counts?.prompts ?? 0 }, (_, i) => ({
promptPath: { uri: fakeUri('prompt', i), storage: PromptsStorage.local, type: PromptsType.prompt },
uri: fakeUri('prompt', i),
name: `prompt-${i}`,
type: PromptsType.prompt,
storage: PromptsStorage.local,
userInvocable: true,
parsedPromptFile: undefined,
when: undefined,
}));
const instructions = Array.from({ length: counts?.instructions ?? 0 }, (_, i) => fakeItem('instructions', i));
const hooks = Array.from({ length: counts?.hooks ?? 0 }, (_, i) => fakeItem('hook', i));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import assert from 'assert';
import { URI } from '../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
import { IPromptsService, PromptsStorage, IPromptPath, ILocalPromptPath, IUserPromptPath, IExtensionPromptPath, IResolvedAgentFile, AgentFileType } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { IPromptsService, PromptsStorage, IPromptPath, ILocalPromptPath, IUserPromptPath, IExtensionPromptPath, IAgentInstructionFile, AgentInstructionFileType } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IWorkspaceContextService, IWorkspace, IWorkspaceFolder, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
import { getSourceCounts, getSourceCountsTotal, getCustomizationTotalCount } from '../../browser/customizationCounts.js';
Expand All @@ -33,8 +33,8 @@ function extensionFile(path: string): IExtensionPromptPath {
};
}

function agentInstructionFile(path: string): IResolvedAgentFile {
return { uri: URI.file(path), realPath: undefined, type: AgentFileType.agentsMd };
function agentInstructionFile(path: string): IAgentInstructionFile {
return { uri: URI.file(path), realPath: undefined, type: AgentInstructionFileType.agentsMd };
}

function makeWorkspaceFolder(path: string, name?: string): IWorkspaceFolder {
Expand All @@ -52,7 +52,7 @@ function createMockPromptsService(opts: {
userFiles?: IPromptPath[];
extensionFiles?: IPromptPath[];
allFiles?: IPromptPath[];
agentInstructions?: IResolvedAgentFile[];
agentInstructions?: IAgentInstructionFile[];
agents?: { name: string; uri: URI; storage: PromptsStorage }[];
skills?: { name: string; uri: URI; storage: PromptsStorage }[];
commands?: { name: string; uri: URI; storage: PromptsStorage; type: PromptsType }[];
Expand All @@ -77,8 +77,13 @@ function createMockPromptsService(opts: {
storage: s.storage,
})),
getPromptSlashCommands: async () => (opts.commands ?? []).map(c => ({
uri: c.uri,
name: c.name,
promptPath: { uri: c.uri, storage: c.storage, type: c.type },
type: c.type,
storage: c.storage,
userInvocable: true,
parsedPromptFile: undefined!,
when: undefined,
})),
getSourceFolders: async () => [],
getResolvedSourceFolders: async () => [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ async function appendRawServiceData(lines: string[], promptsService: IPromptsSer
const commands = await promptsService.getPromptSlashCommands(CancellationToken.None);
lines.push(` getPromptSlashCommands: ${commands.length} commands`);
for (const c of commands) {
lines.push(` /${c.name} [${c.promptPath.storage}] ${c.promptPath.uri.fsPath} (type=${c.promptPath.type})`);
lines.push(` /${c.name} [${c.storage}] ${c.uri.fsPath} (type=${c.type})`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { autorun } from '../../../../../base/common/observable.js';
import { basename, dirname, isEqual, isEqualOrParent } from '../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { ResourceSet } from '../../../../../base/common/map.js';
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { localize } from '../../../../../nls.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
Expand Down Expand Up @@ -55,8 +55,6 @@ import { Schemas } from '../../../../../base/common/network.js';
import { OS } from '../../../../../base/common/platform.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCustomizationItemProvider, matchesWorkspaceSubpath, matchesInstructionFileFilter, ICustomizationSyncProvider } from '../../common/customizationHarnessService.js';
import { evaluateApplyToPattern } from '../../common/promptSyntax/computeAutomaticInstructions.js';
import { isInClaudeRulesFolder, getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
Expand Down Expand Up @@ -1202,12 +1200,12 @@ export class AICustomizationListWidget extends Disposable {
* agent hooks) are left untouched — groupKey overrides are only applied to
* items whose current groupKey is `undefined`.
*/
private applyBuiltinGroupKeys(items: IAICustomizationListItem[], extensionInfoByUri: ReadonlyMap<string, { id: ExtensionIdentifier; displayName?: string }>): IAICustomizationListItem[] {
private applyBuiltinGroupKeys(items: IAICustomizationListItem[], extensionInfoByUri: ResourceMap<{ id: ExtensionIdentifier; displayName?: string }>): IAICustomizationListItem[] {
return items.map(item => {
if (item.storage !== PromptsStorage.extension) {
return item;
}
const extInfo = extensionInfoByUri.get(item.uri.toString());
const extInfo = extensionInfoByUri.get(item.uri);
if (!extInfo) {
return item;
}
Expand Down Expand Up @@ -1249,7 +1247,7 @@ export class AICustomizationListWidget extends Disposable {

const items: IAICustomizationListItem[] = [];
const disabledUris = this.promptsService.getDisabledPromptFiles(promptType);
const extensionInfoByUri = new Map<string, { id: ExtensionIdentifier; displayName?: string }>();
const extensionInfoByUri = new ResourceMap<{ id: ExtensionIdentifier; displayName?: string }>();


if (promptType === PromptsType.agent) {
Expand All @@ -1259,7 +1257,7 @@ export class AICustomizationListWidget extends Disposable {
const allAgentFiles = await this.promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None);
for (const file of allAgentFiles) {
if (file.extension) {
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
extensionInfoByUri.set(file.uri, { id: file.extension.identifier, displayName: file.extension.displayName });
}
}
for (const agent of agents) {
Expand All @@ -1276,8 +1274,8 @@ export class AICustomizationListWidget extends Disposable {
disabled: disabledUris.has(agent.uri),
});
// Track extension ID for built-in grouping (if not already set from file list)
if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri.toString())) {
extensionInfoByUri.set(agent.uri.toString(), { id: agent.source.extensionId });
if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri)) {
extensionInfoByUri.set(agent.uri, { id: agent.source.extensionId });
}
}
} else if (promptType === PromptsType.skill) {
Expand All @@ -1287,7 +1285,7 @@ export class AICustomizationListWidget extends Disposable {
const allSkillFiles = await this.promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None);
for (const file of allSkillFiles) {
if (file.extension) {
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
extensionInfoByUri.set(file.uri, { id: file.extension.identifier, displayName: file.extension.displayName });
}
}
const uiIntegrations = this.workspaceService.getSkillUIIntegrations();
Expand Down Expand Up @@ -1340,23 +1338,23 @@ export class AICustomizationListWidget extends Disposable {
// Filter out skills since they have their own section
const commands = await this.promptsService.getPromptSlashCommands(CancellationToken.None);
for (const command of commands) {
if (command.promptPath.type === PromptsType.skill) {
if (command.type === PromptsType.skill) {
continue;
}
const filename = basename(command.promptPath.uri);
const filename = basename(command.uri);
items.push({
id: command.promptPath.uri.toString(),
uri: command.promptPath.uri,
id: command.uri.toString(),
uri: command.uri,
name: command.name,
filename,
description: command.description,
storage: command.promptPath.storage,
storage: command.storage,
promptType,
pluginUri: command.promptPath.storage === PromptsStorage.plugin ? command.promptPath.pluginUri : undefined,
disabled: disabledUris.has(command.promptPath.uri),
pluginUri: command.storage === PromptsStorage.plugin ? command.pluginUri : undefined,
disabled: disabledUris.has(command.uri),
});
if (command.promptPath.extension) {
extensionInfoByUri.set(command.promptPath.uri.toString(), { id: command.promptPath.extension.identifier, displayName: command.promptPath.extension.displayName });
if (command.extension) {
extensionInfoByUri.set(command.uri, { id: command.extension.identifier, displayName: command.extension.displayName });
}
}
} else if (promptType === PromptsType.hook) {
Expand Down Expand Up @@ -1463,10 +1461,10 @@ export class AICustomizationListWidget extends Disposable {
}
} else {
// For instructions, group by category: agent instructions, context instructions, on-demand instructions
const promptFiles = await this.promptsService.listPromptFiles(promptType, CancellationToken.None);
for (const file of promptFiles) {
const instructionFiles = await this.promptsService.getInstructionFiles(CancellationToken.None);
for (const file of instructionFiles) {
if (file.extension) {
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
extensionInfoByUri.set(file.uri, { id: file.extension.identifier, displayName: file.extension.displayName });
}
}
const agentInstructionFiles = await this.promptsService.listAgentInstructions(CancellationToken.None, undefined);
Expand Down Expand Up @@ -1496,63 +1494,53 @@ export class AICustomizationListWidget extends Disposable {
}

// Parse prompt files to separate into context vs on-demand
const promptFilesToParse = promptFiles.filter(item => !agentInstructionUris.has(item.uri));
const parseResults = await Promise.all(promptFilesToParse.map(async item => {
try {
const parsed = await this.promptsService.parseNew(item.uri, CancellationToken.None);
return { item, parsed };
} catch {
// Parse failed — treat as on-demand
return { item, parsed: undefined };

for (const { uri, pattern, name, description, storage, pluginUri } of instructionFiles) {
if (agentInstructionUris.has(uri)) {
continue; // already added as agent instruction
}
}));

for (const { item, parsed } of parseResults) {
const applyTo = evaluateApplyToPattern(parsed?.header, isInClaudeRulesFolder(item.uri));
const name = parsed?.header?.name;
let description = parsed?.header?.description;
const friendlyName = this.getFriendlyName(name || item.name || getCleanPromptName(item.uri));
description = description || item.description;
const friendlyName = this.getFriendlyName(name);

if (applyTo !== undefined) {
if (pattern !== undefined) {
// Context instruction
const badge = applyTo === '**'
const badge = pattern === '**'
? localize('alwaysAdded', "always added")
: applyTo;
const badgeTooltip = applyTo === '**'
: pattern;
const badgeTooltip = pattern === '**'
? localize('alwaysAddedTooltip', "This instruction is automatically included in every interaction.")
: localize('onContextTooltip', "This instruction is automatically included when files matching '{0}' are in context.", applyTo);
: localize('onContextTooltip', "This instruction is automatically included when files matching '{0}' are in context.", pattern);
items.push({
id: item.uri.toString(),
uri: item.uri,
id: uri.toString(),
uri,
name: friendlyName,
filename: this.labelService.getUriLabel(item.uri, { relative: true }),
filename: this.labelService.getUriLabel(uri, { relative: true }),
displayName: friendlyName,
badge,
badgeTooltip,
description: description,
storage: item.storage,
description,
storage,
promptType,
typeIcon: storageToIcon(item.storage),
typeIcon: storageToIcon(storage),
groupKey: 'context-instructions',
pluginUri: item.storage === PromptsStorage.plugin ? item.pluginUri : undefined,
disabled: disabledUris.has(item.uri),
pluginUri,
disabled: disabledUris.has(uri),
});
} else {
// On-demand instruction
items.push({
id: item.uri.toString(),
uri: item.uri,
id: uri.toString(),
uri,
name: friendlyName,
filename: basename(item.uri),
filename: basename(uri),
displayName: friendlyName,
description: description,
storage: item.storage,
description,
storage,
promptType,
typeIcon: storageToIcon(item.storage),
typeIcon: storageToIcon(storage),
groupKey: 'on-demand-instructions',
pluginUri: item.storage === PromptsStorage.plugin ? item.pluginUri : undefined,
disabled: disabledUris.has(item.uri),
pluginUri,
disabled: disabledUris.has(uri),
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte
import { ILogService } from '../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ChatContextKeys } from '../common/actions/chatContextKeys.js';
import { AgentFileType, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
import { AgentInstructionFileType, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js';
import { TipEligibilityStorageKeys } from './chatTipStorageKeys.js';
Expand All @@ -30,7 +30,7 @@ export interface ITipExclusionConfig {
/** File-based exclusion configuration. */
readonly excludeWhenPromptFilesExist?: {
readonly promptType: PromptsType;
readonly agentFileType?: AgentFileType;
readonly agentFileType?: AgentInstructionFileType;
readonly excludeUntilChecked?: boolean;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { localize } from '../../../../../../nls.js';
import { URI } from '../../../../../../base/common/uri.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { AgentFileType, IExtensionPromptPath, IPromptPath, IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
import { AgentInstructionFileType, IExtensionPromptPath, IPromptPath, IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
import { basename, dirname, extUri, joinPath } from '../../../../../../base/common/resources.js';
import { DisposableStore } from '../../../../../../base/common/lifecycle.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
Expand Down Expand Up @@ -475,7 +475,7 @@ export class PromptFilePickers {
// Don't show the folder path for files under .github folder (namely, copilot-instructions.md) since that is only defined once per repo.
return {
uri: agentInstructionFile.uri,
description: agentInstructionFile.type !== AgentFileType.copilotInstructionsMd ? folderName : undefined,
description: agentInstructionFile.type !== AgentInstructionFileType.copilotInstructionsMd ? folderName : undefined,
storage: PromptsStorage.local,
type: options.type
} satisfies IPromptPath;
Expand Down
7 changes: 3 additions & 4 deletions src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2272,12 +2272,11 @@ export class ChatWidget extends Disposable implements IChatWidget {
const toolReferences = this.toolsService.toToolReferences(refs);
requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences));

const promptPath = slashCommand.promptPath;
const promptRunEvent: ChatPromptRunEvent = {
storage: promptPath.storage,
storage: slashCommand.storage,
};
if (promptPath.storage === PromptsStorage.extension) {
promptRunEvent.extensionId = promptPath.extension.identifier.value;
if (slashCommand.extension) {
promptRunEvent.extensionId = slashCommand.extension.identifier.value;
promptRunEvent.promptName = slashCommand.name;
} else {
promptRunEvent.promptNameHash = hash(slashCommand.name).toString(16);
Expand Down
Loading
Loading