mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Merge pull request #294774 from microsoft/josh/upstream-newpromptactions
prompt actions: add extension points for folder and editor overrides
This commit is contained in:
@@ -96,7 +96,8 @@ async function addHookToFile(
|
||||
fileService: IFileService,
|
||||
editorService: IEditorService,
|
||||
notificationService: INotificationService,
|
||||
bulkEditService: IBulkEditService
|
||||
bulkEditService: IBulkEditService,
|
||||
openEditorOverride?: (resource: URI, options?: { selection?: ITextEditorSelection }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
// Parse existing file
|
||||
let hooksContent: { hooks: Record<string, unknown[]> };
|
||||
@@ -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<void>;
|
||||
/** 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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ICodeEditor | undefined>;
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user