Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/attachInstructionsAction.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 { ChatViewId, IChatWidget, IChatWidgetService, showChatView } from '../chat.js';6import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from '../actions/chatActions.js';7import { URI } from '../../../../../base/common/uri.js';8import { localize, localize2 } from '../../../../../nls.js';9import { ChatContextKeys } from '../../common/chatContextKeys.js';10import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';11import { PromptsConfig } from '../../common/promptSyntax/config/config.js';12import { IViewsService } from '../../../../services/views/common/viewsService.js';13import { PromptFilePickers } from './pickers/promptFilePickers.js';14import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';15import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';16import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';17import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';18import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../chatContextPickService.js';19import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';20import { Codicon } from '../../../../../base/common/codicons.js';21import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';22import { INSTRUCTIONS_LANGUAGE_ID, PromptsType } from '../../common/promptSyntax/promptTypes.js';23import { compare } from '../../../../../base/common/strings.js';24import { ILabelService } from '../../../../../platform/label/common/label.js';25import { dirname } from '../../../../../base/common/resources.js';26import { IPromptFileVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/chatVariableEntries.js';27import { KeyMod, KeyCode } from '../../../../../base/common/keyCodes.js';28import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';29import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';30import { CancellationToken } from '../../../../../base/common/cancellation.js';31import { IOpenerService } from '../../../../../platform/opener/common/opener.js';32import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';3334/**35* Action ID for the `Attach Instruction` action.36*/37const ATTACH_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.attach.instructions';3839/**40* Action ID for the `Configure Instruction` action.41*/42const CONFIGURE_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.configure.instructions';434445/**46* Options for the {@link AttachInstructionsAction} action.47*/48export interface IAttachInstructionsActionOptions {4950/**51* Target chat widget reference to attach the instruction to. If the reference is52* provided, the command will attach the instruction as attachment of the widget.53* Otherwise, the command will re-use an existing one.54*/55readonly widget?: IChatWidget;5657/**58* Instruction resource `URI` to attach to the chat input, if any.59* If provided the resource will be pre-selected in the prompt picker dialog,60* otherwise the dialog will show the prompts list without any pre-selection.61*/62readonly resource?: URI;6364/**65* Whether to skip the instructions files selection dialog.66*67* Note! if this option is set to `true`, the {@link resource}68* option `must be defined`.69*/70readonly skipSelectionDialog?: boolean;71}7273/**74* Action to attach a prompt to a chat widget input.75*/76class AttachInstructionsAction extends Action2 {77constructor() {78super({79id: ATTACH_INSTRUCTIONS_ACTION_ID,80title: localize2('attach-instructions.capitalized.ellipses', "Attach Instructions..."),81f1: false,82precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled),83category: CHAT_CATEGORY,84keybinding: {85primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Slash,86weight: KeybindingWeight.WorkbenchContrib87},88menu: {89id: MenuId.CommandPalette,90when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled)91}92});93}9495public override async run(96accessor: ServicesAccessor,97options?: IAttachInstructionsActionOptions,98): Promise<void> {99const viewsService = accessor.get(IViewsService);100const instaService = accessor.get(IInstantiationService);101102if (!options) {103options = {104resource: getActiveInstructionsFileUri(accessor),105widget: getFocusedChatWidget(accessor),106};107}108109const pickers = instaService.createInstance(PromptFilePickers);110111const { skipSelectionDialog, resource } = options;112113114const widget = options.widget ?? (await showChatView(viewsService));115if (!widget) {116return;117}118119if (skipSelectionDialog && resource) {120widget.attachmentModel.addContext(toPromptFileVariableEntry(resource, PromptFileVariableKind.Instruction));121widget.focusInput();122return;123}124125const placeholder = localize(126'commands.instructions.select-dialog.placeholder',127'Select instructions files to attach',128);129130const result = await pickers.selectPromptFile({ resource, placeholder, type: PromptsType.instructions });131132if (result !== undefined) {133widget.attachmentModel.addContext(toPromptFileVariableEntry(result.promptFile, PromptFileVariableKind.Instruction));134widget.focusInput();135}136}137}138139class ManageInstructionsFilesAction extends Action2 {140constructor() {141super({142id: CONFIGURE_INSTRUCTIONS_ACTION_ID,143title: localize2('configure-instructions', "Configure Instructions..."),144shortTitle: localize2('configure-instructions.short', "Instructions"),145icon: Codicon.bookmark,146f1: true,147precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled),148category: CHAT_CATEGORY,149menu: {150id: CHAT_CONFIG_MENU_ID,151when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),152order: 11,153group: '0_level'154}155});156}157158public override async run(159accessor: ServicesAccessor,160): Promise<void> {161const openerService = accessor.get(IOpenerService);162const instaService = accessor.get(IInstantiationService);163164const pickers = instaService.createInstance(PromptFilePickers);165166const placeholder = localize(167'commands.prompt.manage-dialog.placeholder',168'Select the instructions file to open'169);170171const result = await pickers.selectPromptFile({ placeholder, type: PromptsType.instructions, optionEdit: false });172if (result !== undefined) {173await openerService.open(result.promptFile);174}175176}177}178179180function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined {181const chatWidgetService = accessor.get(IChatWidgetService);182183const { lastFocusedWidget } = chatWidgetService;184if (!lastFocusedWidget) {185return undefined;186}187188// the widget input `must` be focused at the time when command run189if (!lastFocusedWidget.hasInputFocus()) {190return undefined;191}192193return lastFocusedWidget;194}195196/**197* Gets `URI` of a instructions file open in an active editor instance, if any.198*/199function getActiveInstructionsFileUri(accessor: ServicesAccessor): URI | undefined {200const codeEditorService = accessor.get(ICodeEditorService);201const model = codeEditorService.getActiveCodeEditor()?.getModel();202if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) {203return model.uri;204}205return undefined;206}207208/**209* Helper to register the `Attach Prompt` action.210*/211export function registerAttachPromptActions(): void {212registerAction2(AttachInstructionsAction);213registerAction2(ManageInstructionsFilesAction);214}215216217export class ChatInstructionsPickerPick implements IChatContextPickerItem {218219readonly type = 'pickerPick';220readonly label = localize('chatContext.attach.instructions.label', 'Instructions...');221readonly icon = Codicon.bookmark;222readonly commandId = ATTACH_INSTRUCTIONS_ACTION_ID;223224constructor(225@IPromptsService private readonly promptsService: IPromptsService,226@ILabelService private readonly labelService: ILabelService,227@IConfigurationService private readonly configurationService: IConfigurationService,228) { }229230isEnabled(widget: IChatWidget): Promise<boolean> | boolean {231return PromptsConfig.enabled(this.configurationService);232}233234asPicker(): IChatContextPicker {235236const picks = this.promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None).then(value => {237238const result: (IChatContextPickerPickItem | IQuickPickSeparator)[] = [];239240value = value.slice(0).sort((a, b) => compare(a.storage, b.storage));241242let storageType: string | undefined;243244for (const { uri, storage } of value) {245246if (storageType !== storage) {247storageType = storage;248result.push({249type: 'separator',250label: storage === 'user'251? localize('user-data-dir.capitalized', 'User data folder')252: this.labelService.getUriLabel(dirname(uri), { relative: true })253});254}255256result.push({257label: getCleanPromptName(uri),258asAttachment: (): IPromptFileVariableEntry => {259return toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction);260}261});262}263return result;264});265266return {267placeholder: localize('placeholder', 'Select instructions files to attach'),268picks,269configure: {270label: localize('configureInstructions', 'Configure Instructions...'),271commandId: CONFIGURE_INSTRUCTIONS_ACTION_ID272}273};274}275276277}278279280