Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts
5256 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { isEqual } from '../../../../../base/common/resources.js';6import { URI } from '../../../../../base/common/uri.js';7import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';8import { SnippetController2 } from '../../../../../editor/contrib/snippet/browser/snippetController2.js';9import { localize, localize2 } from '../../../../../nls.js';10import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';11import { ICommandService } from '../../../../../platform/commands/common/commands.js';12import { IFileService } from '../../../../../platform/files/common/files.js';13import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';14import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';15import { ILogService } from '../../../../../platform/log/common/log.js';16import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../platform/notification/common/notification.js';17import { IOpenerService } from '../../../../../platform/opener/common/opener.js';18import { getLanguageIdForPromptsType, PromptsType } from '../../common/promptSyntax/promptTypes.js';19import { IUserDataSyncEnablementService, SyncResource } from '../../../../../platform/userDataSync/common/userDataSync.js';20import { IEditorService } from '../../../../services/editor/common/editorService.js';21import { CONFIGURE_SYNC_COMMAND_ID } from '../../../../services/userDataSync/common/userDataSync.js';22import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';23import { CHAT_CATEGORY } from '../actions/chatActions.js';24import { askForPromptFileName } from './pickers/askForPromptName.js';25import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';26import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';27import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';28import { Target } from '../../common/promptSyntax/service/promptsService.js';29import { getTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js';303132class AbstractNewPromptFileAction extends Action2 {3334constructor(id: string, title: string, private readonly type: PromptsType) {35super({36id,37title,38f1: false,39precondition: ChatContextKeys.enabled,40category: CHAT_CATEGORY,41keybinding: {42weight: KeybindingWeight.WorkbenchContrib43},44menu: {45id: MenuId.CommandPalette,46when: ChatContextKeys.enabled47}48});49}5051public override async run(accessor: ServicesAccessor) {52const logService = accessor.get(ILogService);53const openerService = accessor.get(IOpenerService);54const commandService = accessor.get(ICommandService);55const notificationService = accessor.get(INotificationService);56const userDataSyncEnablementService = accessor.get(IUserDataSyncEnablementService);57const editorService = accessor.get(IEditorService);58const fileService = accessor.get(IFileService);59const instaService = accessor.get(IInstantiationService);6061const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type);62if (!selectedFolder) {63return;64}6566const fileName = await instaService.invokeFunction(askForPromptFileName, this.type, selectedFolder.uri);67if (!fileName) {68return;69}70// create the prompt file7172await fileService.createFolder(selectedFolder.uri);7374const promptUri = URI.joinPath(selectedFolder.uri, fileName);75await fileService.createFile(promptUri);7677await openerService.open(promptUri);7879const cleanName = getCleanPromptName(promptUri);8081const editor = getCodeEditor(editorService.activeTextEditorControl);82if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) {83SnippetController2.get(editor)?.apply([{84range: editor.getModel().getFullModelRange(),85template: getDefaultContentSnippet(this.type, cleanName, getTarget(this.type, promptUri)),86}]);87}8889if (selectedFolder.storage !== 'user') {90return;91}9293// due to PII concerns, synchronization of the 'user' reusable prompts94// is disabled by default, but we want to make that fact clear to the user95// hence after a 'user' prompt is create, we check if the synchronization96// was explicitly configured before, and if it wasn't, we show a suggestion97// to enable the synchronization logic in the Settings Sync configuration9899const isConfigured = userDataSyncEnablementService100.isResourceEnablementConfigured(SyncResource.Prompts);101const isSettingsSyncEnabled = userDataSyncEnablementService.isEnabled();102103// if prompts synchronization has already been configured before or104// if settings sync service is currently disabled, nothing to do105if ((isConfigured === true) || (isSettingsSyncEnabled === false)) {106return;107}108109// show suggestion to enable synchronization of the user prompts and instructions to the user110notificationService.prompt(111Severity.Info,112localize(113'workbench.command.prompts.create.user.enable-sync-notification',114"Do you want to backup and sync your user prompt, instruction and custom agent files with Setting Sync?'",115),116[117{118label: localize('enable.capitalized', "Enable"),119run: () => {120commandService.executeCommand(CONFIGURE_SYNC_COMMAND_ID)121.catch((error) => {122logService.error(`Failed to run '${CONFIGURE_SYNC_COMMAND_ID}' command: ${error}.`);123});124},125},126{127label: localize('learnMore.capitalized', "Learn More"),128run: () => {129openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help'));130},131},132],133{134neverShowAgain: {135id: 'workbench.command.prompts.create.user.enable-sync-notification',136scope: NeverShowAgainScope.PROFILE,137},138},139);140}141}142143function getDefaultContentSnippet(promptType: PromptsType, name: string | undefined, target: Target): string {144switch (promptType) {145case PromptsType.prompt:146return [147`---`,148`name: ${name ?? '${1:prompt-name}'}`,149`description: \${2:Describe when to use this prompt}`,150`---`,151`\${3:Define the prompt content here. You can include instructions, examples, and any other relevant information to guide the AI's responses.}`,152].join('\n');153case PromptsType.instructions:154if (target === Target.Claude) {155return [156`---`,157`description: \${1:Describe when these instructions should be loaded}`,158`paths:`,159`. - "src/**/*.ts"`,160`---`,161`\${2:Provide coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,162].join('\n');163} else {164return [165`---`,166`description: \${1:Describe when these instructions should be loaded}`,167`# applyTo: '\${1|**,**/*.ts|}' # when provided, instructions will automatically be added to the request context when the pattern matches an attached file`,168`---`,169`\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,170].join('\n');171}172case PromptsType.agent:173if (target === Target.Claude) {174return [175`---`,176`name: ${name ?? '${1:agent-name}'}`,177`description: \${2:Describe what this custom agent does and when to use it.}`,178`tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed.`,179`---`,180`\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`,181].join('\n');182} else {183return [184`---`,185`name: ${name ?? '${1:agent-name}'}`,186`description: \${2:Describe what this custom agent does and when to use it.}`,187`argument-hint: \${3:The inputs this agent expects, e.g., "a task to implement" or "a question to answer".}`,188`# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.`,189`---`,190`\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`,191].join('\n');192}193case PromptsType.skill:194return [195`---`,196`name: ${name ?? '${1:skill-name}'}`,197`description: \${2:Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.}`,198`---`,199`\${3:Define the functionality provided by this skill, including detailed instructions and examples}`,200].join('\n');201default:202throw new Error(`Unsupported prompt type: ${promptType}`);203}204}205206207208export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt';209export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions';210export const NEW_AGENT_COMMAND_ID = 'workbench.command.new.agent';211export const NEW_SKILL_COMMAND_ID = 'workbench.command.new.skill';212213class NewPromptFileAction extends AbstractNewPromptFileAction {214constructor() {215super(NEW_PROMPT_COMMAND_ID, localize('commands.new.prompt.local.title', "New Prompt File..."), PromptsType.prompt);216}217}218219class NewInstructionsFileAction extends AbstractNewPromptFileAction {220constructor() {221super(NEW_INSTRUCTIONS_COMMAND_ID, localize('commands.new.instructions.local.title', "New Instructions File..."), PromptsType.instructions);222}223}224225class NewAgentFileAction extends AbstractNewPromptFileAction {226constructor() {227super(NEW_AGENT_COMMAND_ID, localize('commands.new.agent.local.title', "New Custom Agent..."), PromptsType.agent);228}229}230231class NewSkillFileAction extends Action2 {232constructor() {233super({234id: NEW_SKILL_COMMAND_ID,235title: localize('commands.new.skill.local.title', "New Skill File..."),236f1: false,237precondition: ChatContextKeys.enabled,238category: CHAT_CATEGORY,239keybinding: {240weight: KeybindingWeight.WorkbenchContrib241},242menu: {243id: MenuId.CommandPalette,244when: ChatContextKeys.enabled245}246});247}248249public override async run(accessor: ServicesAccessor) {250const openerService = accessor.get(IOpenerService);251const editorService = accessor.get(IEditorService);252const fileService = accessor.get(IFileService);253const instaService = accessor.get(IInstantiationService);254const quickInputService = accessor.get(IQuickInputService);255256const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.skill);257if (!selectedFolder) {258return;259}260261// Ask for skill name (will be the folder name)262// Per agentskills.io/specification: name must be 1-64 chars, lowercase alphanumeric + hyphens,263// no leading/trailing hyphens, no consecutive hyphens, must match folder name264const skillName = await quickInputService.input({265prompt: localize('commands.new.skill.name.prompt', "Enter a name for the skill (lowercase letters, numbers, and hyphens only)"),266placeHolder: localize('commands.new.skill.name.placeholder', "e.g., pdf-processing, data-analysis"),267validateInput: async (value) => {268if (!value || !value.trim()) {269return localize('commands.new.skill.name.required', "Skill name is required");270}271const name = value.trim();272if (name.length > 64) {273return localize('commands.new.skill.name.tooLong', "Skill name must be 64 characters or less");274}275// Per spec: lowercase alphanumeric and hyphens only276if (!/^[a-z0-9-]+$/.test(name)) {277return localize('commands.new.skill.name.invalidChars', "Skill name may only contain lowercase letters, numbers, and hyphens");278}279if (name.startsWith('-') || name.endsWith('-')) {280return localize('commands.new.skill.name.hyphenEdge', "Skill name must not start or end with a hyphen");281}282if (name.includes('--')) {283return localize('commands.new.skill.name.consecutiveHyphens', "Skill name must not contain consecutive hyphens");284}285return undefined;286}287});288289if (!skillName) {290return;291}292293const trimmedName = skillName.trim();294295// Create the skill folder and SKILL.md file296const skillFolder = URI.joinPath(selectedFolder.uri, trimmedName);297await fileService.createFolder(skillFolder);298299const skillFileUri = URI.joinPath(skillFolder, SKILL_FILENAME);300await fileService.createFile(skillFileUri);301302await openerService.open(skillFileUri);303304const editor = getCodeEditor(editorService.activeTextEditorControl);305if (editor && editor.hasModel() && isEqual(editor.getModel().uri, skillFileUri)) {306SnippetController2.get(editor)?.apply([{307range: editor.getModel().getFullModelRange(),308template: getDefaultContentSnippet(PromptsType.skill, trimmedName, Target.Undefined),309}]);310}311}312}313314class NewUntitledPromptFileAction extends Action2 {315constructor() {316super({317id: 'workbench.command.new.untitled.prompt',318title: localize2('commands.new.untitled.prompt.title', "New Untitled Prompt File"),319f1: true,320precondition: ChatContextKeys.enabled,321category: CHAT_CATEGORY,322keybinding: {323weight: KeybindingWeight.WorkbenchContrib324},325});326}327328public override async run(accessor: ServicesAccessor) {329const editorService = accessor.get(IEditorService);330331const languageId = getLanguageIdForPromptsType(PromptsType.prompt);332333const input = await editorService.openEditor({334resource: undefined,335languageId,336options: {337pinned: true338}339});340const type = PromptsType.prompt;341342const editor = getCodeEditor(editorService.activeTextEditorControl);343if (editor && editor.hasModel()) {344SnippetController2.get(editor)?.apply([{345range: editor.getModel().getFullModelRange(),346template: getDefaultContentSnippet(type, undefined, Target.Undefined),347}]);348}349350return input;351}352}353354export function registerNewPromptFileActions(): void {355registerAction2(NewPromptFileAction);356registerAction2(NewInstructionsFileAction);357registerAction2(NewAgentFileAction);358registerAction2(NewSkillFileAction);359registerAction2(NewUntitledPromptFileAction);360}361362363