From aad1561e221ca507309e7ee1ca099634c170b535 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:45:10 -0800 Subject: [PATCH] Merge pull request #294774 from microsoft/josh/upstream-newpromptactions prompt actions: add extension points for folder and editor overrides --- .../chat/browser/promptSyntax/hookActions.ts | 78 +++++++++++++------ .../promptSyntax/newPromptFileActions.ts | 75 +++++++++++++----- 2 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index e37b11c22cd..00255f74508 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -96,7 +96,8 @@ async function addHookToFile( fileService: IFileService, editorService: IEditorService, notificationService: INotificationService, - bulkEditService: IBulkEditService + bulkEditService: IBulkEditService, + openEditorOverride?: (resource: URI, options?: { selection?: ITextEditorSelection }) => Promise, ): Promise { // Parse existing file let hooksContent: { hooks: Record }; @@ -240,13 +241,17 @@ async function addHookToFile( const selection = findHookCommandSelection(jsonContent, keyToUse, newHookIndex, 'command'); // Open editor with selection (or re-focus if already open) - await editorService.openEditor({ - resource: hookFileUri, - options: { - selection, - pinned: false - } - }); + if (openEditorOverride) { + await openEditorOverride(hookFileUri, { selection }); + } else { + await editorService.openEditor({ + resource: hookFileUri, + options: { + selection, + pinned: false + } + }); + } } } @@ -290,12 +295,25 @@ const enum Step { EnterFilename = 5, } +/** + * Optional callbacks for customizing the hook creation and opening behaviour. + * The agentic editor passes these to open hooks in the embedded editor and + * track worktree files for auto-commit. + */ +export interface IHookQuickPickCallbacks { + /** Override how the hook file is opened. If not provided, uses editorService.openEditor. */ + readonly openEditor?: (resource: URI, options?: { selection?: ITextEditorSelection }) => Promise; + /** Called after a new hook file is created on disk. */ + readonly onHookFileCreated?: (uri: URI) => void; +} + /** * Shows the Configure Hooks quick pick UI, allowing the user to view, * open, or create hooks. Can be called from the action or slash command. */ export async function showConfigureHooksQuickPick( accessor: ServicesAccessor, + callbacks?: IHookQuickPickCallbacks, ): Promise { const promptsService = accessor.get(IPromptsService); const quickInputService = accessor.get(IQuickInputService); @@ -470,13 +488,17 @@ export async function showConfigureHooksQuickPick( } picker.hide(); - await editorService.openEditor({ - resource: entry.fileUri, - options: { - selection, - pinned: false - } - }); + if (callbacks?.openEditor) { + await callbacks.openEditor(entry.fileUri, { selection }); + } else { + await editorService.openEditor({ + resource: entry.fileUri, + options: { + selection, + pinned: false + } + }); + } return; } @@ -548,7 +570,8 @@ export async function showConfigureHooksQuickPick( fileService, editorService, notificationService, - bulkEditService + bulkEditService, + callbacks?.openEditor, ); return; } @@ -679,7 +702,8 @@ export async function showConfigureHooksQuickPick( fileService, editorService, notificationService, - bulkEditService + bulkEditService, + callbacks?.openEditor, ); return; } @@ -704,18 +728,24 @@ export async function showConfigureHooksQuickPick( const jsonContent = JSON.stringify(hooksContent, null, '\t'); await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); + callbacks?.onHookFileCreated?.(hookFileUri); + // Find the selection for the new hook's command field const selection = findHookCommandSelection(jsonContent, hookTypeKey, 0, 'command'); // Open editor with selection store.dispose(); - await editorService.openEditor({ - resource: hookFileUri, - options: { - selection, - pinned: false - } - }); + if (callbacks?.openEditor) { + await callbacks.openEditor(hookFileUri, { selection }); + } else { + await editorService.openEditor({ + resource: hookFileUri, + options: { + selection, + pinned: false + } + }); + } return; } } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 5928158fa19..5b3393e59d2 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -5,7 +5,7 @@ import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { getCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { SnippetController2 } from '../../../../../editor/contrib/snippet/browser/snippetController2.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -26,9 +26,19 @@ import { askForPromptFileName } from './pickers/askForPromptName.js'; import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js'; -import { Target } from '../../common/promptSyntax/service/promptsService.js'; +import { Target, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { getTarget } from '../../common/promptSyntax/languageProviders/promptValidator.js'; +/** + * Options to override the default folder-picker and editor-open behaviour + * of the new-prompt-file actions. The agentic editor passes these to open + * files in the embedded editor and pre-resolve the target folder. + */ +export interface INewPromptOptions { + readonly targetFolder?: URI; + readonly targetStorage?: PromptsStorage; + readonly openFile?: (uri: URI) => Promise; +} class AbstractNewPromptFileAction extends Action2 { @@ -49,7 +59,7 @@ class AbstractNewPromptFileAction extends Action2 { }); } - public override async run(accessor: ServicesAccessor) { + public override async run(accessor: ServicesAccessor, options?: INewPromptOptions) { const logService = accessor.get(ILogService); const openerService = accessor.get(IOpenerService); const commandService = accessor.get(ICommandService); @@ -59,27 +69,40 @@ class AbstractNewPromptFileAction extends Action2 { const fileService = accessor.get(IFileService); const instaService = accessor.get(IInstantiationService); - const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type); - if (!selectedFolder) { - return; + let folderUri: URI; + let storage: string; + if (options?.targetFolder) { + folderUri = options.targetFolder; + storage = options.targetStorage ?? PromptsStorage.local; + } else { + const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type); + if (!selectedFolder) { + return; + } + folderUri = selectedFolder.uri; + storage = selectedFolder.storage; } - const fileName = await instaService.invokeFunction(askForPromptFileName, this.type, selectedFolder.uri); + const fileName = await instaService.invokeFunction(askForPromptFileName, this.type, folderUri); if (!fileName) { return; } // create the prompt file - await fileService.createFolder(selectedFolder.uri); + await fileService.createFolder(folderUri); - const promptUri = URI.joinPath(selectedFolder.uri, fileName); + const promptUri = URI.joinPath(folderUri, fileName); await fileService.createFile(promptUri); - await openerService.open(promptUri); - const cleanName = getCleanPromptName(promptUri); - const editor = getCodeEditor(editorService.activeTextEditorControl); + let editor: ICodeEditor | null | undefined; + if (options?.openFile) { + editor = await options.openFile(promptUri); + } else { + await openerService.open(promptUri); + editor = getCodeEditor(editorService.activeTextEditorControl); + } if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) { SnippetController2.get(editor)?.apply([{ range: editor.getModel().getFullModelRange(), @@ -87,7 +110,7 @@ class AbstractNewPromptFileAction extends Action2 { }]); } - if (selectedFolder.storage !== 'user') { + if (storage !== 'user') { return; } @@ -247,16 +270,22 @@ class NewSkillFileAction extends Action2 { }); } - public override async run(accessor: ServicesAccessor) { + public override async run(accessor: ServicesAccessor, options?: INewPromptOptions) { const openerService = accessor.get(IOpenerService); const editorService = accessor.get(IEditorService); const fileService = accessor.get(IFileService); const instaService = accessor.get(IInstantiationService); const quickInputService = accessor.get(IQuickInputService); - const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.skill); - if (!selectedFolder) { - return; + let folderUri: URI; + if (options?.targetFolder) { + folderUri = options.targetFolder; + } else { + const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.skill); + if (!selectedFolder) { + return; + } + folderUri = selectedFolder.uri; } // Ask for skill name (will be the folder name) @@ -294,15 +323,19 @@ class NewSkillFileAction extends Action2 { const trimmedName = skillName.trim(); // Create the skill folder and SKILL.md file - const skillFolder = URI.joinPath(selectedFolder.uri, trimmedName); + const skillFolder = URI.joinPath(folderUri, trimmedName); await fileService.createFolder(skillFolder); const skillFileUri = URI.joinPath(skillFolder, SKILL_FILENAME); await fileService.createFile(skillFileUri); - await openerService.open(skillFileUri); - - const editor = getCodeEditor(editorService.activeTextEditorControl); + let editor: ICodeEditor | null | undefined; + if (options?.openFile) { + editor = await options.openFile(skillFileUri); + } else { + await openerService.open(skillFileUri); + editor = getCodeEditor(editorService.activeTextEditorControl); + } if (editor && editor.hasModel() && isEqual(editor.getModel().uri, skillFileUri)) { SnippetController2.get(editor)?.apply([{ range: editor.getModel().getFullModelRange(),