Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts
3296 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 { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';13import { IFileService } from '../../../../../platform/files/common/files.js';14import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';15import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';16import { ILogService } from '../../../../../platform/log/common/log.js';17import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../platform/notification/common/notification.js';18import { IOpenerService } from '../../../../../platform/opener/common/opener.js';19import { PromptsConfig } from '../../common/promptSyntax/config/config.js';20import { getLanguageIdForPromptsType, PromptsType } from '../../common/promptSyntax/promptTypes.js';21import { IUserDataSyncEnablementService, SyncResource } from '../../../../../platform/userDataSync/common/userDataSync.js';22import { IEditorService } from '../../../../services/editor/common/editorService.js';23import { CONFIGURE_SYNC_COMMAND_ID } from '../../../../services/userDataSync/common/userDataSync.js';24import { ISnippetsService } from '../../../snippets/browser/snippets.js';25import { ChatContextKeys } from '../../common/chatContextKeys.js';26import { CHAT_CATEGORY } from '../actions/chatActions.js';27import { askForPromptFileName } from './pickers/askForPromptName.js';28import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';29import { IChatModeService } from '../../common/chatModes.js';303132class AbstractNewPromptFileAction extends Action2 {3334constructor(id: string, title: string, private readonly type: PromptsType) {35super({36id,37title,38f1: false,39precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled),40category: CHAT_CATEGORY,41keybinding: {42weight: KeybindingWeight.WorkbenchContrib43},44menu: {45id: MenuId.CommandPalette,46when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled)47}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);60const chatModeService = accessor.get(IChatModeService);6162const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type);63if (!selectedFolder) {64return;65}6667const fileName = await instaService.invokeFunction(askForPromptFileName, this.type, selectedFolder.uri);68if (!fileName) {69return;70}7172// create the prompt file7374await fileService.createFolder(selectedFolder.uri);7576const promptUri = URI.joinPath(selectedFolder.uri, fileName);77await fileService.createFile(promptUri);7879await openerService.open(promptUri);8081const editor = getCodeEditor(editorService.activeTextEditorControl);82if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) {83SnippetController2.get(editor)?.apply([{84range: editor.getModel().getFullModelRange(),85template: this.getDefaultContentSnippet(this.type, chatModeService),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 mode 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}141142private getDefaultContentSnippet(promptType: PromptsType, chatModeService: IChatModeService): string {143const modes = chatModeService.getModes();144const modeNames = modes.builtin.map(mode => mode.name).join(',') + (modes.custom.length ? (',' + modes.custom.map(mode => mode.name).join(',')) : '');145switch (promptType) {146case PromptsType.prompt:147return [148`---`,149`mode: \${1|${modeNames}|}`,150`---`,151`\${2:Define the task to achieve, including specific requirements, constraints, and success criteria.}`,152].join('\n');153case PromptsType.instructions:154return [155`---`,156`applyTo: '\${1|**,**/*.ts|}'`,157`---`,158`\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,159].join('\n');160case PromptsType.mode:161return [162`---`,163`description: '\${1:Description of the custom chat mode.}'`,164`tools: []`,165`---`,166`\${2:Define the purpose of this chat mode and how AI should behave: response style, available tools, focus areas, and any mode-specific instructions or constraints.}`,167].join('\n');168default:169throw new Error(`Unknown prompt type: ${promptType}`);170}171}172}173174175176export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt';177export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions';178export const NEW_MODE_COMMAND_ID = 'workbench.command.new.mode';179180class NewPromptFileAction extends AbstractNewPromptFileAction {181constructor() {182super(NEW_PROMPT_COMMAND_ID, localize('commands.new.prompt.local.title', "New Prompt File..."), PromptsType.prompt);183}184}185186class NewInstructionsFileAction extends AbstractNewPromptFileAction {187constructor() {188super(NEW_INSTRUCTIONS_COMMAND_ID, localize('commands.new.instructions.local.title', "New Instructions File..."), PromptsType.instructions);189}190}191192class NewModeFileAction extends AbstractNewPromptFileAction {193constructor() {194super(NEW_MODE_COMMAND_ID, localize('commands.new.mode.local.title', "New Mode File..."), PromptsType.mode);195}196}197198class NewUntitledPromptFileAction extends Action2 {199constructor() {200super({201id: 'workbench.command.new.untitled.prompt',202title: localize2('commands.new.untitled.prompt.title', "New Untitled Prompt File"),203f1: true,204precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled),205category: CHAT_CATEGORY,206keybinding: {207weight: KeybindingWeight.WorkbenchContrib208},209});210}211212public override async run(accessor: ServicesAccessor) {213const editorService = accessor.get(IEditorService);214const snippetService = accessor.get(ISnippetsService);215216const languageId = getLanguageIdForPromptsType(PromptsType.prompt);217218const input = await editorService.openEditor({219resource: undefined,220languageId,221options: {222pinned: true223}224});225226const editor = getCodeEditor(editorService.activeTextEditorControl);227if (editor && editor.hasModel()) {228const snippets = await snippetService.getSnippets(languageId, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true });229if (snippets.length > 0) {230SnippetController2.get(editor)?.apply([{231range: editor.getModel().getFullModelRange(),232template: snippets[0].body233}]);234}235}236237return input;238}239}240241export function registerNewPromptFileActions(): void {242registerAction2(NewPromptFileAction);243registerAction2(NewInstructionsFileAction);244registerAction2(NewModeFileAction);245registerAction2(NewUntitledPromptFileAction);246}247248249