Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts
5256 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
import { isEqual } from '../../../../../base/common/resources.js';
7
import { URI } from '../../../../../base/common/uri.js';
8
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
9
import { SnippetController2 } from '../../../../../editor/contrib/snippet/browser/snippetController2.js';
10
import { localize, localize2 } from '../../../../../nls.js';
11
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
12
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
13
import { IFileService } from '../../../../../platform/files/common/files.js';
14
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
15
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
16
import { ILogService } from '../../../../../platform/log/common/log.js';
17
import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../platform/notification/common/notification.js';
18
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
19
import { getLanguageIdForPromptsType, PromptsType } from '../../common/promptSyntax/promptTypes.js';
20
import { IUserDataSyncEnablementService, SyncResource } from '../../../../../platform/userDataSync/common/userDataSync.js';
21
import { IEditorService } from '../../../../services/editor/common/editorService.js';
22
import { CONFIGURE_SYNC_COMMAND_ID } from '../../../../services/userDataSync/common/userDataSync.js';
23
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
24
import { CHAT_CATEGORY } from '../actions/chatActions.js';
25
import { askForPromptFileName } from './pickers/askForPromptName.js';
26
import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';
27
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
28
import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';
29
import { Target } from '../../common/promptSyntax/service/promptsService.js';
30
import { getTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js';
31
32
33
class AbstractNewPromptFileAction extends Action2 {
34
35
constructor(id: string, title: string, private readonly type: PromptsType) {
36
super({
37
id,
38
title,
39
f1: false,
40
precondition: ChatContextKeys.enabled,
41
category: CHAT_CATEGORY,
42
keybinding: {
43
weight: KeybindingWeight.WorkbenchContrib
44
},
45
menu: {
46
id: MenuId.CommandPalette,
47
when: ChatContextKeys.enabled
48
}
49
});
50
}
51
52
public override async run(accessor: ServicesAccessor) {
53
const logService = accessor.get(ILogService);
54
const openerService = accessor.get(IOpenerService);
55
const commandService = accessor.get(ICommandService);
56
const notificationService = accessor.get(INotificationService);
57
const userDataSyncEnablementService = accessor.get(IUserDataSyncEnablementService);
58
const editorService = accessor.get(IEditorService);
59
const fileService = accessor.get(IFileService);
60
const instaService = accessor.get(IInstantiationService);
61
62
const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type);
63
if (!selectedFolder) {
64
return;
65
}
66
67
const fileName = await instaService.invokeFunction(askForPromptFileName, this.type, selectedFolder.uri);
68
if (!fileName) {
69
return;
70
}
71
// create the prompt file
72
73
await fileService.createFolder(selectedFolder.uri);
74
75
const promptUri = URI.joinPath(selectedFolder.uri, fileName);
76
await fileService.createFile(promptUri);
77
78
await openerService.open(promptUri);
79
80
const cleanName = getCleanPromptName(promptUri);
81
82
const editor = getCodeEditor(editorService.activeTextEditorControl);
83
if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) {
84
SnippetController2.get(editor)?.apply([{
85
range: editor.getModel().getFullModelRange(),
86
template: getDefaultContentSnippet(this.type, cleanName, getTarget(this.type, promptUri)),
87
}]);
88
}
89
90
if (selectedFolder.storage !== 'user') {
91
return;
92
}
93
94
// due to PII concerns, synchronization of the 'user' reusable prompts
95
// is disabled by default, but we want to make that fact clear to the user
96
// hence after a 'user' prompt is create, we check if the synchronization
97
// was explicitly configured before, and if it wasn't, we show a suggestion
98
// to enable the synchronization logic in the Settings Sync configuration
99
100
const isConfigured = userDataSyncEnablementService
101
.isResourceEnablementConfigured(SyncResource.Prompts);
102
const isSettingsSyncEnabled = userDataSyncEnablementService.isEnabled();
103
104
// if prompts synchronization has already been configured before or
105
// if settings sync service is currently disabled, nothing to do
106
if ((isConfigured === true) || (isSettingsSyncEnabled === false)) {
107
return;
108
}
109
110
// show suggestion to enable synchronization of the user prompts and instructions to the user
111
notificationService.prompt(
112
Severity.Info,
113
localize(
114
'workbench.command.prompts.create.user.enable-sync-notification',
115
"Do you want to backup and sync your user prompt, instruction and custom agent files with Setting Sync?'",
116
),
117
[
118
{
119
label: localize('enable.capitalized', "Enable"),
120
run: () => {
121
commandService.executeCommand(CONFIGURE_SYNC_COMMAND_ID)
122
.catch((error) => {
123
logService.error(`Failed to run '${CONFIGURE_SYNC_COMMAND_ID}' command: ${error}.`);
124
});
125
},
126
},
127
{
128
label: localize('learnMore.capitalized', "Learn More"),
129
run: () => {
130
openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help'));
131
},
132
},
133
],
134
{
135
neverShowAgain: {
136
id: 'workbench.command.prompts.create.user.enable-sync-notification',
137
scope: NeverShowAgainScope.PROFILE,
138
},
139
},
140
);
141
}
142
}
143
144
function getDefaultContentSnippet(promptType: PromptsType, name: string | undefined, target: Target): string {
145
switch (promptType) {
146
case PromptsType.prompt:
147
return [
148
`---`,
149
`name: ${name ?? '${1:prompt-name}'}`,
150
`description: \${2:Describe when to use this prompt}`,
151
`---`,
152
`\${3:Define the prompt content here. You can include instructions, examples, and any other relevant information to guide the AI's responses.}`,
153
].join('\n');
154
case PromptsType.instructions:
155
if (target === Target.Claude) {
156
return [
157
`---`,
158
`description: \${1:Describe when these instructions should be loaded}`,
159
`paths:`,
160
`. - "src/**/*.ts"`,
161
`---`,
162
`\${2:Provide coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,
163
].join('\n');
164
} else {
165
return [
166
`---`,
167
`description: \${1:Describe when these instructions should be loaded}`,
168
`# applyTo: '\${1|**,**/*.ts|}' # when provided, instructions will automatically be added to the request context when the pattern matches an attached file`,
169
`---`,
170
`\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,
171
].join('\n');
172
}
173
case PromptsType.agent:
174
if (target === Target.Claude) {
175
return [
176
`---`,
177
`name: ${name ?? '${1:agent-name}'}`,
178
`description: \${2:Describe what this custom agent does and when to use it.}`,
179
`tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed.`,
180
`---`,
181
`\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`,
182
].join('\n');
183
} else {
184
return [
185
`---`,
186
`name: ${name ?? '${1:agent-name}'}`,
187
`description: \${2:Describe what this custom agent does and when to use it.}`,
188
`argument-hint: \${3:The inputs this agent expects, e.g., "a task to implement" or "a question to answer".}`,
189
`# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.`,
190
`---`,
191
`\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`,
192
].join('\n');
193
}
194
case PromptsType.skill:
195
return [
196
`---`,
197
`name: ${name ?? '${1:skill-name}'}`,
198
`description: \${2:Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.}`,
199
`---`,
200
`\${3:Define the functionality provided by this skill, including detailed instructions and examples}`,
201
].join('\n');
202
default:
203
throw new Error(`Unsupported prompt type: ${promptType}`);
204
}
205
}
206
207
208
209
export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt';
210
export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions';
211
export const NEW_AGENT_COMMAND_ID = 'workbench.command.new.agent';
212
export const NEW_SKILL_COMMAND_ID = 'workbench.command.new.skill';
213
214
class NewPromptFileAction extends AbstractNewPromptFileAction {
215
constructor() {
216
super(NEW_PROMPT_COMMAND_ID, localize('commands.new.prompt.local.title', "New Prompt File..."), PromptsType.prompt);
217
}
218
}
219
220
class NewInstructionsFileAction extends AbstractNewPromptFileAction {
221
constructor() {
222
super(NEW_INSTRUCTIONS_COMMAND_ID, localize('commands.new.instructions.local.title', "New Instructions File..."), PromptsType.instructions);
223
}
224
}
225
226
class NewAgentFileAction extends AbstractNewPromptFileAction {
227
constructor() {
228
super(NEW_AGENT_COMMAND_ID, localize('commands.new.agent.local.title', "New Custom Agent..."), PromptsType.agent);
229
}
230
}
231
232
class NewSkillFileAction extends Action2 {
233
constructor() {
234
super({
235
id: NEW_SKILL_COMMAND_ID,
236
title: localize('commands.new.skill.local.title', "New Skill File..."),
237
f1: false,
238
precondition: ChatContextKeys.enabled,
239
category: CHAT_CATEGORY,
240
keybinding: {
241
weight: KeybindingWeight.WorkbenchContrib
242
},
243
menu: {
244
id: MenuId.CommandPalette,
245
when: ChatContextKeys.enabled
246
}
247
});
248
}
249
250
public override async run(accessor: ServicesAccessor) {
251
const openerService = accessor.get(IOpenerService);
252
const editorService = accessor.get(IEditorService);
253
const fileService = accessor.get(IFileService);
254
const instaService = accessor.get(IInstantiationService);
255
const quickInputService = accessor.get(IQuickInputService);
256
257
const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.skill);
258
if (!selectedFolder) {
259
return;
260
}
261
262
// Ask for skill name (will be the folder name)
263
// Per agentskills.io/specification: name must be 1-64 chars, lowercase alphanumeric + hyphens,
264
// no leading/trailing hyphens, no consecutive hyphens, must match folder name
265
const skillName = await quickInputService.input({
266
prompt: localize('commands.new.skill.name.prompt', "Enter a name for the skill (lowercase letters, numbers, and hyphens only)"),
267
placeHolder: localize('commands.new.skill.name.placeholder', "e.g., pdf-processing, data-analysis"),
268
validateInput: async (value) => {
269
if (!value || !value.trim()) {
270
return localize('commands.new.skill.name.required', "Skill name is required");
271
}
272
const name = value.trim();
273
if (name.length > 64) {
274
return localize('commands.new.skill.name.tooLong', "Skill name must be 64 characters or less");
275
}
276
// Per spec: lowercase alphanumeric and hyphens only
277
if (!/^[a-z0-9-]+$/.test(name)) {
278
return localize('commands.new.skill.name.invalidChars', "Skill name may only contain lowercase letters, numbers, and hyphens");
279
}
280
if (name.startsWith('-') || name.endsWith('-')) {
281
return localize('commands.new.skill.name.hyphenEdge', "Skill name must not start or end with a hyphen");
282
}
283
if (name.includes('--')) {
284
return localize('commands.new.skill.name.consecutiveHyphens', "Skill name must not contain consecutive hyphens");
285
}
286
return undefined;
287
}
288
});
289
290
if (!skillName) {
291
return;
292
}
293
294
const trimmedName = skillName.trim();
295
296
// Create the skill folder and SKILL.md file
297
const skillFolder = URI.joinPath(selectedFolder.uri, trimmedName);
298
await fileService.createFolder(skillFolder);
299
300
const skillFileUri = URI.joinPath(skillFolder, SKILL_FILENAME);
301
await fileService.createFile(skillFileUri);
302
303
await openerService.open(skillFileUri);
304
305
const editor = getCodeEditor(editorService.activeTextEditorControl);
306
if (editor && editor.hasModel() && isEqual(editor.getModel().uri, skillFileUri)) {
307
SnippetController2.get(editor)?.apply([{
308
range: editor.getModel().getFullModelRange(),
309
template: getDefaultContentSnippet(PromptsType.skill, trimmedName, Target.Undefined),
310
}]);
311
}
312
}
313
}
314
315
class NewUntitledPromptFileAction extends Action2 {
316
constructor() {
317
super({
318
id: 'workbench.command.new.untitled.prompt',
319
title: localize2('commands.new.untitled.prompt.title', "New Untitled Prompt File"),
320
f1: true,
321
precondition: ChatContextKeys.enabled,
322
category: CHAT_CATEGORY,
323
keybinding: {
324
weight: KeybindingWeight.WorkbenchContrib
325
},
326
});
327
}
328
329
public override async run(accessor: ServicesAccessor) {
330
const editorService = accessor.get(IEditorService);
331
332
const languageId = getLanguageIdForPromptsType(PromptsType.prompt);
333
334
const input = await editorService.openEditor({
335
resource: undefined,
336
languageId,
337
options: {
338
pinned: true
339
}
340
});
341
const type = PromptsType.prompt;
342
343
const editor = getCodeEditor(editorService.activeTextEditorControl);
344
if (editor && editor.hasModel()) {
345
SnippetController2.get(editor)?.apply([{
346
range: editor.getModel().getFullModelRange(),
347
template: getDefaultContentSnippet(type, undefined, Target.Undefined),
348
}]);
349
}
350
351
return input;
352
}
353
}
354
355
export function registerNewPromptFileActions(): void {
356
registerAction2(NewPromptFileAction);
357
registerAction2(NewInstructionsFileAction);
358
registerAction2(NewAgentFileAction);
359
registerAction2(NewSkillFileAction);
360
registerAction2(NewUntitledPromptFileAction);
361
}
362
363