Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts
5245 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
7
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
8
import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js';
9
import { localize } from '../../../../../nls.js';
10
import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js';
11
import { IWorkbenchContribution } from '../../../../common/contributions.js';
12
import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';
13
import { IPromptsService, PromptsStorage } from './service/promptsService.js';
14
import { PromptsType } from './promptTypes.js';
15
import { UriComponents } from '../../../../../base/common/uri.js';
16
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
17
import { CancellationToken } from '../../../../../base/common/cancellation.js';
18
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
19
import { Registry } from '../../../../../platform/registry/common/platform.js';
20
import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js';
21
22
interface IRawChatFileContribution {
23
readonly path: string;
24
readonly name?: string;
25
readonly description?: string;
26
}
27
28
enum ChatContributionPoint {
29
chatInstructions = 'chatInstructions',
30
chatAgents = 'chatAgents',
31
chatPromptFiles = 'chatPromptFiles',
32
chatSkills = 'chatSkills',
33
}
34
35
function registerChatFilesExtensionPoint(point: ChatContributionPoint) {
36
return extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatFileContribution[]>({
37
extensionPoint: point,
38
jsonSchema: {
39
description: localize('chatContribution.schema.description', 'Contributes {0} for chat prompts.', point),
40
type: 'array',
41
items: {
42
additionalProperties: false,
43
type: 'object',
44
defaultSnippets: [{
45
body: {
46
path: './relative/path/to/file.md',
47
}
48
}],
49
required: ['path'],
50
properties: {
51
path: {
52
description: localize('chatContribution.property.path', 'Path to the file relative to the extension root.'),
53
type: 'string'
54
},
55
name: {
56
description: localize('chatContribution.property.name', '(Optional) Name for this entry.'),
57
deprecationMessage: localize('chatContribution.property.name.deprecated', 'Specify "name" in the prompt file itself instead.'),
58
type: 'string'
59
},
60
description: {
61
description: localize('chatContribution.property.description', '(Optional) Description of the entry.'),
62
deprecationMessage: localize('chatContribution.property.description.deprecated', 'Specify "description" in the prompt file itself instead.'),
63
type: 'string'
64
}
65
}
66
}
67
}
68
});
69
}
70
71
const epPrompt = registerChatFilesExtensionPoint(ChatContributionPoint.chatPromptFiles);
72
const epInstructions = registerChatFilesExtensionPoint(ChatContributionPoint.chatInstructions);
73
const epAgents = registerChatFilesExtensionPoint(ChatContributionPoint.chatAgents);
74
const epSkills = registerChatFilesExtensionPoint(ChatContributionPoint.chatSkills);
75
76
function pointToType(contributionPoint: ChatContributionPoint): PromptsType {
77
switch (contributionPoint) {
78
case ChatContributionPoint.chatPromptFiles: return PromptsType.prompt;
79
case ChatContributionPoint.chatInstructions: return PromptsType.instructions;
80
case ChatContributionPoint.chatAgents: return PromptsType.agent;
81
case ChatContributionPoint.chatSkills: return PromptsType.skill;
82
default: {
83
const exhaustiveCheck: never = contributionPoint;
84
throw new Error(`Unknown contribution point: ${exhaustiveCheck}`);
85
}
86
}
87
}
88
89
function key(extensionId: ExtensionIdentifier, type: PromptsType, path: string) {
90
return `${extensionId.value}/${type}/${path}`;
91
}
92
93
export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribution {
94
public static readonly ID = 'workbench.contrib.chatPromptFilesExtensionPointHandler';
95
96
private readonly registrations = new DisposableMap<string>();
97
98
constructor(
99
@IPromptsService private readonly promptsService: IPromptsService,
100
) {
101
this.handle(epPrompt, ChatContributionPoint.chatPromptFiles);
102
this.handle(epInstructions, ChatContributionPoint.chatInstructions);
103
this.handle(epAgents, ChatContributionPoint.chatAgents);
104
this.handle(epSkills, ChatContributionPoint.chatSkills);
105
}
106
107
private handle(extensionPoint: extensionsRegistry.IExtensionPoint<IRawChatFileContribution[]>, contributionPoint: ChatContributionPoint) {
108
extensionPoint.setHandler((_extensions, delta) => {
109
for (const ext of delta.added) {
110
const type = pointToType(contributionPoint);
111
for (const raw of ext.value) {
112
if (!raw.path) {
113
ext.collector.error(localize('extension.missing.path', "Extension '{0}' cannot register {1} entry without path.", ext.description.identifier.value, contributionPoint));
114
continue;
115
}
116
const fileUri = joinPath(ext.description.extensionLocation, raw.path);
117
if (!isEqualOrParent(fileUri, ext.description.extensionLocation)) {
118
ext.collector.error(localize('extension.invalid.path', "Extension '{0}' {1} entry '{2}' resolves outside the extension.", ext.description.identifier.value, contributionPoint, raw.path));
119
continue;
120
}
121
try {
122
const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description);
123
this.registrations.set(key(ext.description.identifier, type, raw.path), d);
124
} catch (e) {
125
const msg = e instanceof Error ? e.message : String(e);
126
ext.collector.error(localize('extension.registration.failed', "Extension '{0}' {1}. Failed to register {2}: {3}", ext.description.identifier.value, contributionPoint, raw.path, msg));
127
}
128
}
129
}
130
for (const ext of delta.removed) {
131
const type = pointToType(contributionPoint);
132
for (const raw of ext.value) {
133
this.registrations.deleteAndDispose(key(ext.description.identifier, type, raw.path));
134
}
135
}
136
});
137
}
138
}
139
140
/**
141
* Result type for the extension prompt file provider command.
142
*/
143
export interface IExtensionPromptFileResult {
144
readonly uri: UriComponents;
145
readonly type: PromptsType;
146
}
147
148
/**
149
* Register the command to list all extension-contributed prompt files.
150
*/
151
CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): Promise<IExtensionPromptFileResult[]> => {
152
const promptsService = accessor.get(IPromptsService);
153
154
// Get extension prompt files for all prompt types in parallel
155
const [agents, instructions, prompts, skills, hooks] = await Promise.all([
156
promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None),
157
promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None),
158
promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None),
159
promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None),
160
promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None),
161
]);
162
163
// Combine all files and collect extension-contributed ones
164
const result: IExtensionPromptFileResult[] = [];
165
for (const file of [...agents, ...instructions, ...prompts, ...skills, ...hooks]) {
166
if (file.storage === PromptsStorage.extension) {
167
result.push({ uri: file.uri.toJSON(), type: file.type });
168
}
169
}
170
171
return result;
172
});
173
174
class ChatPromptFilesDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
175
readonly type = 'table';
176
177
constructor(private readonly contributionPoint: ChatContributionPoint) {
178
super();
179
}
180
181
shouldRender(manifest: IExtensionManifest): boolean {
182
return !!manifest.contributes?.[this.contributionPoint];
183
}
184
185
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
186
const contributions = manifest.contributes?.[this.contributionPoint] ?? [];
187
if (!contributions.length) {
188
return { data: { headers: [], rows: [] }, dispose: () => { } };
189
}
190
191
const headers = [
192
localize('chatFilesName', "Name"),
193
localize('chatFilesDescription', "Description"),
194
localize('chatFilesPath', "Path"),
195
];
196
197
const rows: IRowData[][] = contributions.map(d => {
198
return [
199
d.name ?? '-',
200
d.description ?? '-',
201
d.path,
202
];
203
});
204
205
return {
206
data: {
207
headers,
208
rows
209
},
210
dispose: () => { }
211
};
212
}
213
}
214
215
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
216
id: ChatContributionPoint.chatPromptFiles,
217
label: localize('chatPromptFiles', "Chat Prompt Files"),
218
access: {
219
canToggle: false
220
},
221
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatPromptFiles]),
222
});
223
224
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
225
id: ChatContributionPoint.chatInstructions,
226
label: localize('chatInstructions', "Chat Instructions"),
227
access: {
228
canToggle: false
229
},
230
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatInstructions]),
231
});
232
233
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
234
id: ChatContributionPoint.chatAgents,
235
label: localize('chatAgents', "Chat Agents"),
236
access: {
237
canToggle: false
238
},
239
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatAgents]),
240
});
241
242
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
243
id: ChatContributionPoint.chatSkills,
244
label: localize('chatSkills', "Chat Skills"),
245
access: {
246
canToggle: false
247
},
248
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatSkills]),
249
});
250
251