Skip to content

Commit 852abaf

Browse files
committed
promptsService: add IInstructionFile and make other types consistent
1 parent fdaa8c8 commit 852abaf

21 files changed

Lines changed: 341 additions & 224 deletions

src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
266266
async getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
267267
const allCommands = await this.promptsService.getPromptSlashCommands(token);
268268
return allCommands.filter(cmd => {
269-
const filter = this.getStorageSourceFilter(cmd.promptPath.type);
270-
return applyStorageSourceFilter([cmd.promptPath], filter).length > 0;
269+
const filter = this.getStorageSourceFilter(cmd.type);
270+
return applyStorageSourceFilter([cmd], filter).length > 0;
271271
});
272272
}
273273

src/vs/sessions/contrib/chat/browser/slashCommands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ export class SlashCommandHandler extends Disposable {
128128
}
129129

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

299299
const promptCommands = await this.aiCustomizationWorkspaceService.getFilteredPromptSlashCommands(token);
300-
const userInvocable = promptCommands.filter(c => c.parsedPromptFile?.header?.userInvocable !== false);
300+
const userInvocable = promptCommands.filter(c => c.userInvocable !== false);
301301
if (userInvocable.length === 0) {
302302
return null;
303303
}

src/vs/sessions/contrib/sessions/browser/customizationCounts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ export async function getSourceCounts(
7272
// Must match loadItems: uses getPromptSlashCommands() filtering out skills
7373
const commands = await promptsService.getPromptSlashCommands(CancellationToken.None);
7474
for (const c of commands) {
75-
if (c.promptPath.type === PromptsType.skill) {
75+
if (c.type === PromptsType.skill) {
7676
continue;
7777
}
78-
items.push({ storage: c.promptPath.storage, uri: c.promptPath.uri });
78+
items.push({ storage: c.storage, uri: c.uri });
7979
}
8080
} else if (promptType === PromptsType.instructions) {
8181
// Must match loadItems: uses listPromptFiles + listAgentInstructions

src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,13 @@ function createMockPromptsServiceWithCounts(counts?: ICustomizationCounts): IPro
128128
}));
129129
const skills = Array.from({ length: counts?.skills ?? 0 }, (_, i) => fakeItem('skill', i));
130130
const prompts = Array.from({ length: counts?.prompts ?? 0 }, (_, i) => ({
131-
promptPath: { uri: fakeUri('prompt', i), storage: PromptsStorage.local, type: PromptsType.prompt },
131+
uri: fakeUri('prompt', i),
132+
name: `prompt-${i}`,
133+
type: PromptsType.prompt,
134+
storage: PromptsStorage.local,
135+
userInvocable: true,
136+
parsedPromptFile: undefined,
137+
when: undefined,
132138
}));
133139
const instructions = Array.from({ length: counts?.instructions ?? 0 }, (_, i) => fakeItem('instructions', i));
134140
const hooks = Array.from({ length: counts?.hooks ?? 0 }, (_, i) => fakeItem('hook', i));

src/vs/sessions/contrib/sessions/test/browser/customizationCounts.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import assert from 'assert';
77
import { URI } from '../../../../../base/common/uri.js';
88
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
99
import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
10-
import { IPromptsService, PromptsStorage, IPromptPath, ILocalPromptPath, IUserPromptPath, IExtensionPromptPath, IResolvedAgentFile, AgentFileType } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
10+
import { IPromptsService, PromptsStorage, IPromptPath, ILocalPromptPath, IUserPromptPath, IExtensionPromptPath, IAgentInstructionFile, AgentFileType } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
1111
import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
1212
import { IWorkspaceContextService, IWorkspace, IWorkspaceFolder, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
1313
import { getSourceCounts, getSourceCountsTotal, getCustomizationTotalCount } from '../../browser/customizationCounts.js';
@@ -33,7 +33,7 @@ function extensionFile(path: string): IExtensionPromptPath {
3333
};
3434
}
3535

36-
function agentInstructionFile(path: string): IResolvedAgentFile {
36+
function agentInstructionFile(path: string): IAgentInstructionFile {
3737
return { uri: URI.file(path), realPath: undefined, type: AgentFileType.agentsMd };
3838
}
3939

@@ -52,7 +52,7 @@ function createMockPromptsService(opts: {
5252
userFiles?: IPromptPath[];
5353
extensionFiles?: IPromptPath[];
5454
allFiles?: IPromptPath[];
55-
agentInstructions?: IResolvedAgentFile[];
55+
agentInstructions?: IAgentInstructionFile[];
5656
agents?: { name: string; uri: URI; storage: PromptsStorage }[];
5757
skills?: { name: string; uri: URI; storage: PromptsStorage }[];
5858
commands?: { name: string; uri: URI; storage: PromptsStorage; type: PromptsType }[];
@@ -77,8 +77,13 @@ function createMockPromptsService(opts: {
7777
storage: s.storage,
7878
})),
7979
getPromptSlashCommands: async () => (opts.commands ?? []).map(c => ({
80+
uri: c.uri,
8081
name: c.name,
81-
promptPath: { uri: c.uri, storage: c.storage, type: c.type },
82+
type: c.type,
83+
storage: c.storage,
84+
userInvocable: true,
85+
parsedPromptFile: undefined!,
86+
when: undefined,
8287
})),
8388
getSourceFolders: async () => [],
8489
getResolvedSourceFolders: async () => [],

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationDebugPanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ async function appendRawServiceData(lines: string[], promptsService: IPromptsSer
165165
const commands = await promptsService.getPromptSlashCommands(CancellationToken.None);
166166
lines.push(` getPromptSlashCommands: ${commands.length} commands`);
167167
for (const c of commands) {
168-
lines.push(` /${c.name} [${c.promptPath.storage}] ${c.promptPath.uri.fsPath} (type=${c.promptPath.type})`);
168+
lines.push(` /${c.name} [${c.storage}] ${c.uri.fsPath} (type=${c.type})`);
169169
}
170170
}
171171

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { autorun } from '../../../../../base/common/observable.js';
1414
import { basename, dirname, isEqual, isEqualOrParent } from '../../../../../base/common/resources.js';
1515
import { ThemeIcon } from '../../../../../base/common/themables.js';
1616
import { URI } from '../../../../../base/common/uri.js';
17-
import { ResourceSet } from '../../../../../base/common/map.js';
17+
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
1818
import { Codicon } from '../../../../../base/common/codicons.js';
1919
import { localize } from '../../../../../nls.js';
2020
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
@@ -55,8 +55,6 @@ import { Schemas } from '../../../../../base/common/network.js';
5555
import { OS } from '../../../../../base/common/platform.js';
5656
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
5757
import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCustomizationItemProvider, matchesWorkspaceSubpath, matchesInstructionFileFilter, ICustomizationSyncProvider } from '../../common/customizationHarnessService.js';
58-
import { evaluateApplyToPattern } from '../../common/promptSyntax/computeAutomaticInstructions.js';
59-
import { isInClaudeRulesFolder, getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
6058
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
6159
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
6260
import { IProductService } from '../../../../../platform/product/common/productService.js';
@@ -1202,12 +1200,12 @@ export class AICustomizationListWidget extends Disposable {
12021200
* agent hooks) are left untouched — groupKey overrides are only applied to
12031201
* items whose current groupKey is `undefined`.
12041202
*/
1205-
private applyBuiltinGroupKeys(items: IAICustomizationListItem[], extensionInfoByUri: ReadonlyMap<string, { id: ExtensionIdentifier; displayName?: string }>): IAICustomizationListItem[] {
1203+
private applyBuiltinGroupKeys(items: IAICustomizationListItem[], extensionInfoByUri: ResourceMap<{ id: ExtensionIdentifier; displayName?: string }>): IAICustomizationListItem[] {
12061204
return items.map(item => {
12071205
if (item.storage !== PromptsStorage.extension) {
12081206
return item;
12091207
}
1210-
const extInfo = extensionInfoByUri.get(item.uri.toString());
1208+
const extInfo = extensionInfoByUri.get(item.uri);
12111209
if (!extInfo) {
12121210
return item;
12131211
}
@@ -1249,7 +1247,7 @@ export class AICustomizationListWidget extends Disposable {
12491247

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

12541252

12551253
if (promptType === PromptsType.agent) {
@@ -1259,7 +1257,7 @@ export class AICustomizationListWidget extends Disposable {
12591257
const allAgentFiles = await this.promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None);
12601258
for (const file of allAgentFiles) {
12611259
if (file.extension) {
1262-
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
1260+
extensionInfoByUri.set(file.uri, { id: file.extension.identifier, displayName: file.extension.displayName });
12631261
}
12641262
}
12651263
for (const agent of agents) {
@@ -1276,8 +1274,8 @@ export class AICustomizationListWidget extends Disposable {
12761274
disabled: disabledUris.has(agent.uri),
12771275
});
12781276
// Track extension ID for built-in grouping (if not already set from file list)
1279-
if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri.toString())) {
1280-
extensionInfoByUri.set(agent.uri.toString(), { id: agent.source.extensionId });
1277+
if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri)) {
1278+
extensionInfoByUri.set(agent.uri, { id: agent.source.extensionId });
12811279
}
12821280
}
12831281
} else if (promptType === PromptsType.skill) {
@@ -1287,7 +1285,7 @@ export class AICustomizationListWidget extends Disposable {
12871285
const allSkillFiles = await this.promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None);
12881286
for (const file of allSkillFiles) {
12891287
if (file.extension) {
1290-
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
1288+
extensionInfoByUri.set(file.uri, { id: file.extension.identifier, displayName: file.extension.displayName });
12911289
}
12921290
}
12931291
const uiIntegrations = this.workspaceService.getSkillUIIntegrations();
@@ -1340,23 +1338,23 @@ export class AICustomizationListWidget extends Disposable {
13401338
// Filter out skills since they have their own section
13411339
const commands = await this.promptsService.getPromptSlashCommands(CancellationToken.None);
13421340
for (const command of commands) {
1343-
if (command.promptPath.type === PromptsType.skill) {
1341+
if (command.type === PromptsType.skill) {
13441342
continue;
13451343
}
1346-
const filename = basename(command.promptPath.uri);
1344+
const filename = basename(command.uri);
13471345
items.push({
1348-
id: command.promptPath.uri.toString(),
1349-
uri: command.promptPath.uri,
1346+
id: command.uri.toString(),
1347+
uri: command.uri,
13501348
name: command.name,
13511349
filename,
13521350
description: command.description,
1353-
storage: command.promptPath.storage,
1351+
storage: command.storage,
13541352
promptType,
1355-
pluginUri: command.promptPath.storage === PromptsStorage.plugin ? command.promptPath.pluginUri : undefined,
1356-
disabled: disabledUris.has(command.promptPath.uri),
1353+
pluginUri: command.storage === PromptsStorage.plugin ? command.pluginUri : undefined,
1354+
disabled: disabledUris.has(command.uri),
13571355
});
1358-
if (command.promptPath.extension) {
1359-
extensionInfoByUri.set(command.promptPath.uri.toString(), { id: command.promptPath.extension.identifier, displayName: command.promptPath.extension.displayName });
1356+
if (command.extension) {
1357+
extensionInfoByUri.set(command.uri, { id: command.extension.identifier, displayName: command.extension.displayName });
13601358
}
13611359
}
13621360
} else if (promptType === PromptsType.hook) {
@@ -1463,10 +1461,10 @@ export class AICustomizationListWidget extends Disposable {
14631461
}
14641462
} else {
14651463
// For instructions, group by category: agent instructions, context instructions, on-demand instructions
1466-
const promptFiles = await this.promptsService.listPromptFiles(promptType, CancellationToken.None);
1467-
for (const file of promptFiles) {
1464+
const instructionFiles = await this.promptsService.getInstructionFiles(CancellationToken.None);
1465+
for (const file of instructionFiles) {
14681466
if (file.extension) {
1469-
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
1467+
extensionInfoByUri.set(file.uri, { id: file.extension.identifier, displayName: file.extension.displayName });
14701468
}
14711469
}
14721470
const agentInstructionFiles = await this.promptsService.listAgentInstructions(CancellationToken.None, undefined);
@@ -1496,63 +1494,53 @@ export class AICustomizationListWidget extends Disposable {
14961494
}
14971495

14981496
// Parse prompt files to separate into context vs on-demand
1499-
const promptFilesToParse = promptFiles.filter(item => !agentInstructionUris.has(item.uri));
1500-
const parseResults = await Promise.all(promptFilesToParse.map(async item => {
1501-
try {
1502-
const parsed = await this.promptsService.parseNew(item.uri, CancellationToken.None);
1503-
return { item, parsed };
1504-
} catch {
1505-
// Parse failed — treat as on-demand
1506-
return { item, parsed: undefined };
1497+
1498+
for (const { uri, pattern, name, description, storage, pluginUri } of instructionFiles) {
1499+
if (agentInstructionUris.has(uri)) {
1500+
continue; // already added as agent instruction
15071501
}
1508-
}));
15091502

1510-
for (const { item, parsed } of parseResults) {
1511-
const applyTo = evaluateApplyToPattern(parsed?.header, isInClaudeRulesFolder(item.uri));
1512-
const name = parsed?.header?.name;
1513-
let description = parsed?.header?.description;
1514-
const friendlyName = this.getFriendlyName(name || item.name || getCleanPromptName(item.uri));
1515-
description = description || item.description;
1503+
const friendlyName = this.getFriendlyName(name);
15161504

1517-
if (applyTo !== undefined) {
1505+
if (pattern !== undefined) {
15181506
// Context instruction
1519-
const badge = applyTo === '**'
1507+
const badge = pattern === '**'
15201508
? localize('alwaysAdded', "always added")
1521-
: applyTo;
1522-
const badgeTooltip = applyTo === '**'
1509+
: pattern;
1510+
const badgeTooltip = pattern === '**'
15231511
? localize('alwaysAddedTooltip', "This instruction is automatically included in every interaction.")
1524-
: localize('onContextTooltip', "This instruction is automatically included when files matching '{0}' are in context.", applyTo);
1512+
: localize('onContextTooltip', "This instruction is automatically included when files matching '{0}' are in context.", pattern);
15251513
items.push({
1526-
id: item.uri.toString(),
1527-
uri: item.uri,
1514+
id: uri.toString(),
1515+
uri,
15281516
name: friendlyName,
1529-
filename: this.labelService.getUriLabel(item.uri, { relative: true }),
1517+
filename: this.labelService.getUriLabel(uri, { relative: true }),
15301518
displayName: friendlyName,
15311519
badge,
15321520
badgeTooltip,
1533-
description: description,
1534-
storage: item.storage,
1521+
description,
1522+
storage,
15351523
promptType,
1536-
typeIcon: storageToIcon(item.storage),
1524+
typeIcon: storageToIcon(storage),
15371525
groupKey: 'context-instructions',
1538-
pluginUri: item.storage === PromptsStorage.plugin ? item.pluginUri : undefined,
1539-
disabled: disabledUris.has(item.uri),
1526+
pluginUri,
1527+
disabled: disabledUris.has(uri),
15401528
});
15411529
} else {
15421530
// On-demand instruction
15431531
items.push({
1544-
id: item.uri.toString(),
1545-
uri: item.uri,
1532+
id: uri.toString(),
1533+
uri,
15461534
name: friendlyName,
1547-
filename: basename(item.uri),
1535+
filename: basename(uri),
15481536
displayName: friendlyName,
1549-
description: description,
1550-
storage: item.storage,
1537+
description,
1538+
storage,
15511539
promptType,
1552-
typeIcon: storageToIcon(item.storage),
1540+
typeIcon: storageToIcon(storage),
15531541
groupKey: 'on-demand-instructions',
1554-
pluginUri: item.storage === PromptsStorage.plugin ? item.pluginUri : undefined,
1555-
disabled: disabledUris.has(item.uri),
1542+
pluginUri,
1543+
disabled: disabledUris.has(uri),
15561544
});
15571545
}
15581546
}

src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,12 +2270,11 @@ export class ChatWidget extends Disposable implements IChatWidget {
22702270
const toolReferences = this.toolsService.toToolReferences(refs);
22712271
requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences));
22722272

2273-
const promptPath = slashCommand.promptPath;
22742273
const promptRunEvent: ChatPromptRunEvent = {
2275-
storage: promptPath.storage,
2274+
storage: slashCommand.storage,
22762275
};
2277-
if (promptPath.storage === PromptsStorage.extension) {
2278-
promptRunEvent.extensionId = promptPath.extension.identifier.value;
2276+
if (slashCommand.extension) {
2277+
promptRunEvent.extensionId = slashCommand.extension.identifier.value;
22792278
promptRunEvent.promptName = slashCommand.name;
22802279
} else {
22812280
promptRunEvent.promptNameHash = hash(slashCommand.name).toString(16);

src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ class SlashCommandCompletions extends Disposable {
258258
if (widget.lockedAgentId) {
259259
// Exclude hooks as those don't work in locked agent scenarios.
260260
try {
261-
const promptType = getPromptFileType(c.promptPath.uri);
261+
const promptType = getPromptFileType(c.uri);
262262
if (promptType && promptType === PromptsType.hook) {
263263
return false;
264264
}
@@ -268,7 +268,7 @@ class SlashCommandCompletions extends Disposable {
268268
}
269269
return true;
270270
})
271-
.filter(c => c.parsedPromptFile?.header?.userInvocable !== false)
271+
.filter(c => c.userInvocable !== false)
272272
.filter(c => !c.when || widget.scopedContextKeyService.contextMatchesRules(c.when));
273273
if (userInvocableCommands.length === 0) {
274274
return null;

src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,13 +349,13 @@ class InputEditorDecorations extends Disposable {
349349
if (slashPromptPart && promptSlashCommand) {
350350
this.clickablePromptSlashCommand = {
351351
range: Range.lift(slashPromptPart.editorRange),
352-
uri: promptSlashCommand.promptPath.uri,
352+
uri: promptSlashCommand.uri,
353353
};
354354
const promptHoverMessage = new MarkdownString();
355355
promptHoverMessage.appendText(localize(
356356
'chatInput.promptSlashCommand.open',
357357
"Click to open {0}",
358-
this.labelService.getUriLabel(promptSlashCommand.promptPath.uri, { relative: true })
358+
this.labelService.getUriLabel(promptSlashCommand.uri, { relative: true })
359359
));
360360
const promptDecoration = {
361361
range: slashPromptPart.editorRange,

0 commit comments

Comments
 (0)