diff --git a/extensions/prompt-basics/package.json b/extensions/prompt-basics/package.json index 96fa45e18f7..9d56f9f92f0 100644 --- a/extensions/prompt-basics/package.json +++ b/extensions/prompt-basics/package.json @@ -22,6 +22,18 @@ "copilot-instructions.md" ], "configuration": "./language-configuration.json" + }, + { + "id": "instructions", + "aliases": [ + "Instructions", + "instructions" + ], + "extensions": [ + ".instructions.md", + "copilot-instructions.md" + ], + "configuration": "./language-configuration.json" } ], "grammars": [ @@ -33,6 +45,15 @@ "markup.underline.link.markdown", "punctuation.definition.list.begin.markdown" ] + }, + { + "language": "instructions", + "path": "./syntaxes/prompt.tmLanguage.json", + "scopeName": "text.html.markdown.prompt", + "unbalancedBracketScopes": [ + "markup.underline.link.markdown", + "punctuation.definition.list.begin.markdown" + ] } ], "configurationDefaults": { @@ -40,6 +61,11 @@ "editor.unicodeHighlight.ambiguousCharacters": false, "editor.unicodeHighlight.invisibleCharacters": false, "diffEditor.ignoreTrimWhitespace": false + }, + "[instructions]": { + "editor.unicodeHighlight.ambiguousCharacters": false, + "editor.unicodeHighlight.invisibleCharacters": false, + "diffEditor.ignoreTrimWhitespace": false } } }, diff --git a/extensions/prompt-basics/package.nls.json b/extensions/prompt-basics/package.nls.json index 1a98e5f9ca4..207593c43c8 100644 --- a/extensions/prompt-basics/package.nls.json +++ b/extensions/prompt-basics/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Prompt Language Basics", - "description": "Syntax highlighting for Prompt documents." + "description": "Syntax highlighting for Prompt and Instructions documents." } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index d8c7aebc77b..ecec4d98404 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -63,7 +63,7 @@ import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../cont import { resizeImage } from '../imageUtils.js'; import { INSTRUCTIONS_COMMAND_ID } from '../promptSyntax/contributions/usePromptCommand.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { runAttachPromptAction, registerReusablePromptActions } from './reusablePromptActions/index.js'; +import { runAttachInstructionsAction, registerReusablePromptActions } from './reusablePromptActions/index.js'; export function registerChatContextActions() { registerAction2(AttachContextAction); @@ -78,7 +78,7 @@ export function registerChatContextActions() { */ type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | - IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IReusablePromptQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem; + IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IInstructionsQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem; /** * These are the types that we can get out of the quick pick @@ -147,12 +147,12 @@ function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPick /** * Checks is a provided object is a prompt instructions quick pick item. */ -function isPromptInstructionsQuickPickItem(obj: unknown): obj is IReusablePromptQuickPickItem { +function isPromptInstructionsQuickPickItem(obj: unknown): obj is IInstructionsQuickPickItem { if (!obj || typeof obj !== 'object') { return false; } - return ('kind' in obj && obj.kind === 'reusable-prompt'); + return ('kind' in obj && obj.kind === INSTRUCTION_PICK_ID); } interface IRelatedFilesQuickPickItem extends IQuickPickItem { @@ -235,17 +235,17 @@ interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem { /** * Quick pick item for reusable prompt attachment. */ -const REUSABLE_PROMPT_PICK_ID = 'reusable-prompt'; -interface IReusablePromptQuickPickItem extends IQuickPickItem { +const INSTRUCTION_PICK_ID = 'instructions'; +interface IInstructionsQuickPickItem extends IQuickPickItem { /** * The ID of the quick pick item. */ - id: typeof REUSABLE_PROMPT_PICK_ID; + id: typeof INSTRUCTION_PICK_ID; /** - * Unique kind identifier of the reusable prompt attachment. + * Unique kind identifier of the instructions attachment. */ - kind: typeof REUSABLE_PROMPT_PICK_ID; + kind: typeof INSTRUCTION_PICK_ID; /** * Keybinding of the command. @@ -628,7 +628,7 @@ export class AttachContextAction extends Action2 { toAttach.push(convertBufferToScreenshotVariable(blob)); } } else if (isPromptInstructionsQuickPickItem(pick)) { - await runAttachPromptAction({ widget }, commandService); + await runAttachInstructionsAction({ widget }, commandService); } else { // Anything else is an attachment const attachmentPick = pick as IAttachmentQuickPickItem; @@ -836,9 +836,9 @@ export class AttachContextAction extends Action2 { const keybinding = keybindingService.lookupKeybinding(INSTRUCTIONS_COMMAND_ID, contextKeyService); quickPickItems.push({ - id: REUSABLE_PROMPT_PICK_ID, - kind: REUSABLE_PROMPT_PICK_ID, - label: localize('chatContext.attach.prompt.label', 'Prompt...'), + id: INSTRUCTION_PICK_ID, + kind: INSTRUCTION_PICK_ID, + label: localize('chatContext.attach.instructions.label', 'Instructions...'), iconClass: ThemeIcon.asClassName(Codicon.bookmark), keybinding, }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts index 6821d3ade77..3d56e033e92 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts @@ -19,8 +19,8 @@ import { ICommandService } from '../../../../../../platform/commands/common/comm import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { IQuickInputService } from '../../../../../../platform/quickinput/common/quickInput.js'; -import { attachInstructionsFile, IAttachOptions } from './dialogs/askToSelectPrompt/utils/attachPrompt.js'; -import { ISelectPromptOptions, askToSelectPrompt } from './dialogs/askToSelectPrompt/askToSelectPrompt.js'; +import { attachInstructionsFiles, IAttachOptions } from './dialogs/askToSelectPrompt/utils/attachPrompt.js'; +import { ISelectInstructionsOptions, askToSelectInstructions } from './dialogs/askToSelectPrompt/askToSelectPrompt.js'; /** * Action ID for the `Attach Instruction` action. @@ -30,8 +30,8 @@ const ATTACH_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.attach.instructions /** * Options for the {@link AttachInstructionsAction} action. */ -export interface IChatAttachPromptActionOptions extends Pick< - ISelectPromptOptions, 'resource' | 'widget' +export interface IChatAttachInstructionsActionOptions extends Pick< + ISelectInstructionsOptions, 'resource' | 'widget' > { /** * Whether to create a new chat panel or open @@ -40,7 +40,7 @@ export interface IChatAttachPromptActionOptions extends Pick< inNewChat?: boolean; /** - * Whether to skip the prompt files selection dialog. + * Whether to skip the instructions files selection dialog. * * Note! if this option is set to `true`, the {@link resource} * option `must be defined`. @@ -64,7 +64,7 @@ class AttachInstructionsAction extends Action2 { public override async run( accessor: ServicesAccessor, - options: IChatAttachPromptActionOptions, + options: IChatAttachInstructionsActionOptions, ): Promise { const fileService = accessor.get(IFileService); const labelService = accessor.get(ILabelService); @@ -89,8 +89,8 @@ class AttachInstructionsAction extends Action2 { commandService, }; - const { widget } = await attachInstructionsFile( - resource, + const { widget } = await attachInstructionsFiles( + [resource], attachOptions, ); @@ -102,9 +102,8 @@ class AttachInstructionsAction extends Action2 { // find all prompt files in the user workspace const promptFiles = await promptsService.listPromptFiles('instructions'); - await askToSelectPrompt({ + await askToSelectInstructions({ ...options, - type: 'instructions', promptFiles, fileService, viewsService, @@ -118,12 +117,12 @@ class AttachInstructionsAction extends Action2 { } /** - * Runs the `Attach Prompt` action with provided options. We export this + * Runs the `Attach Instructions` action with provided options. We export this * function instead of {@link ATTACH_INSTRUCTIONS_ACTION_ID} directly to * encapsulate/enforce the correct options to be passed to the action. */ -export const runAttachPromptAction = async ( - options: IChatAttachPromptActionOptions, +export const runAttachInstructionsAction = async ( + options: IChatAttachInstructionsActionOptions, commandService: ICommandService, ): Promise => { return await commandService.executeCommand( diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts index 6b565a5b01e..c61544372e5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts @@ -14,18 +14,17 @@ import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { ResourceContextKey } from '../../../../../common/contextkeys.js'; import { KeyCode, KeyMod } from '../../../../../../base/common/keyCodes.js'; import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; -import { attachInstructionsFile } from './dialogs/askToSelectPrompt/utils/attachPrompt.js'; -import { detachPrompt } from './dialogs/askToSelectPrompt/utils/detachPrompt.js'; +import { runPromptFile } from './dialogs/askToSelectPrompt/utils/attachPrompt.js'; import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; import { ICommandAction } from '../../../../../../platform/action/common/action.js'; import { IViewsService } from '../../../../../services/views/common/viewsService.js'; import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; import { EditorContextKeys } from '../../../../../../editor/common/editorContextKeys.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { getActivePromptUri } from '../../promptSyntax/contributions/usePromptCommand.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; /** * Condition for the `Run Current Prompt` action. @@ -121,31 +120,21 @@ abstract class RunPromptBaseAction extends Action2 { const viewsService = accessor.get(IViewsService); const commandService = accessor.get(ICommandService); - resource ||= getActivePromptUri(accessor); + resource ||= getActivePromptFileUri(accessor); assertDefined( resource, 'Cannot find URI resource for an active text editor.', ); - const { widget, wasAlreadyAttached } = await attachInstructionsFile( + const { widget } = await runPromptFile( resource, { inNewChat, - skipIfImplicitlyAttached: true, commandService, viewsService, }, ); - // submit the prompt immediately - await widget.acceptInput(); - - // detach the prompt immediately, unless was attached - // before the action was executed - if (wasAlreadyAttached === false) { - await detachPrompt(resource, { widget }); - } - return widget; } } @@ -182,6 +171,20 @@ class RunCurrentPromptAction extends RunPromptBaseAction { } } +/** + * Gets `URI` of a prompt file open in an active editor instance, if any. + */ +export const getActivePromptFileUri = ( + accessor: ServicesAccessor, +): URI | undefined => { + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === PROMPT_LANGUAGE_ID) { + return model.uri; + } + return undefined; +}; + /** * Action ID for the `Run Current Prompt In New Chat` action. */ diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts index cbbe427803d..1fc7a269884 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts @@ -3,17 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PROMPT_DOCS_OPTION } from './constants.js'; import { IChatWidget } from '../../../../chat.js'; -import { attachInstructionsFile } from './utils/attachPrompt.js'; +import { attachInstructionsFiles } from './utils/attachPrompt.js'; import { handleButtonClick } from './utils/handleButtonClick.js'; import { URI } from '../../../../../../../../base/common/uri.js'; -import { assert } from '../../../../../../../../base/common/assert.js'; import { createPromptPickItem } from './utils/createPromptPickItem.js'; import { createPlaceholderText } from './utils/createPlaceholderText.js'; import { extUri } from '../../../../../../../../base/common/resources.js'; import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { IPromptPath, TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; +import { IPromptPath } from '../../../../../common/promptSyntax/service/types.js'; import { DisposableStore } from '../../../../../../../../base/common/lifecycle.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; @@ -24,9 +22,9 @@ import { ICommandService } from '../../../../../../../../platform/commands/commo import { IQuickInputService, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; /** - * Options for the {@link askToSelectPrompt} function. + * Options for the {@link askToSelectInstructions} function. */ -export interface ISelectPromptOptions { +export interface ISelectInstructionsOptions { /** * Prompt resource `URI` to attach to the chat input, if any. * If provided the resource will be pre-selected in the prompt picker dialog, @@ -48,11 +46,6 @@ export interface ISelectPromptOptions { */ readonly promptFiles: readonly IPromptPath[]; - /** - * Type of the prompt files to select. - */ - readonly type: TPromptsType; - readonly fileService: IFileService; readonly labelService: ILabelService; readonly viewsService: IViewsService; @@ -63,25 +56,20 @@ export interface ISelectPromptOptions { } /** - * Shows the prompt selection dialog to the user that allows to select a prompt file(s). + * Shows the instructions selection dialog to the user that allows to select a instructions file(s). * - * If {@link ISelectPromptOptions.resource resource} is provided, the dialog will have + * If {@link ISelectInstructionsOptions.resource resource} is provided, the dialog will have * the resource pre-selected in the prompts list. */ -export const askToSelectPrompt = async ( - options: ISelectPromptOptions, +export const askToSelectInstructions = async ( + options: ISelectInstructionsOptions, ): Promise => { - const { promptFiles, resource, quickInputService, labelService, type } = options; + const { promptFiles, resource, quickInputService, labelService } = options; const fileOptions = promptFiles.map((promptFile) => { return createPromptPickItem(promptFile, labelService); }); - /** - * Add a link to the documentation to the end of prompts list. - */ - fileOptions.push(PROMPT_DOCS_OPTION); - // if a resource is provided, create an `activeItem` for it to pre-select // it in the UI, and sort the list so the active item appears at the top let activeItem: WithUriValue | undefined; @@ -100,7 +88,7 @@ export const askToSelectPrompt = async ( // "user" prompts are always registered in the prompts list, hence it // should be safe to assume that `resource` is not "user" prompt here storage: 'local', - type, + type: 'instructions', }, labelService); fileOptions.push(activeItem); } @@ -138,8 +126,8 @@ export const askToSelectPrompt = async ( quickPick.canAcceptInBackground = true; quickPick.matchOnDescription = true; quickPick.items = fileOptions; + quickPick.canSelectMany = true; - const { openerService } = options; return await new Promise(resolve => { const disposables = new DisposableStore(); @@ -161,27 +149,9 @@ export const askToSelectPrompt = async ( const { selectedItems } = quickPick; const { keyMods } = quickPick; - // sanity check to confirm our expectations - assert( - selectedItems.length === 1, - `Only one item can be accepted, got '${selectedItems.length}'.`, - ); - - const selectedOption = selectedItems[0]; - - // whether user selected the docs link option - const docsSelected = (selectedOption === PROMPT_DOCS_OPTION); - - // if documentation item was selected, open its link in a browser - if (docsSelected) { - // note that opening a file in editor also hides(disposes) the dialog - await openerService.open(selectedOption.value); - return; - } - - // otherwise attach the selected prompt to a chat input - const attachResult = await attachInstructionsFile( - selectedOption.value, + // otherwise attach the selected instructions file to a chat input + const attachResult = await attachInstructionsFiles( + selectedItems.map(item => item.value), { ...options, inNewChat: keyMods.ctrlCmd, diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts index 324bc963d17..501e61a1332 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts @@ -8,12 +8,13 @@ import { URI } from '../../../../../../../../../base/common/uri.js'; import { ACTION_ID_NEW_CHAT } from '../../../../chatClearActions.js'; import { extUri } from '../../../../../../../../../base/common/resources.js'; import { assertDefined } from '../../../../../../../../../base/common/types.js'; -import { IChatAttachPromptActionOptions } from '../../../chatAttachPromptAction.js'; +import { IChatAttachInstructionsActionOptions } from '../../../chatAttachPromptAction.js'; import { IViewsService } from '../../../../../../../../services/views/common/viewsService.js'; import { ICommandService } from '../../../../../../../../../platform/commands/common/commands.js'; +import { detachPrompt } from './detachPrompt.js'; /** - * Options for the {@link attachInstructionsFile} function. + * Options for the {@link attachInstructionsFiles} function. */ export interface IAttachOptions { /** @@ -26,22 +27,43 @@ export interface IAttachOptions { */ readonly inNewChat?: boolean; - /** - * Whether to skip attaching provided prompt if it is - * already attached as an implicit "current file" context. - */ - readonly skipIfImplicitlyAttached?: boolean; - readonly viewsService: IViewsService; readonly commandService: ICommandService; } /** - * Return value of the {@link attachInstructionsFile} function. + * Return value of the {@link attachInstructionsFiles} function. */ interface IAttachResult { readonly widget: IChatWidget; - readonly wasAlreadyAttached: boolean; + readonly wasAlreadyAttached: URI[]; +} + + +/** + * Options for the {@link attachInstructionsFiles} function. + */ +export interface IRunPromptOptions { + /** + * Chat widget instance to attach the prompt to. + */ + readonly widget?: IChatWidget; + /** + * Whether to create a new chat session and + * attach the prompt to it. + */ + readonly inNewChat?: boolean; + + readonly viewsService: IViewsService; + readonly commandService: ICommandService; +} + + +/** + * Return value of the {@link attachInstructionsFiles} function. + */ +interface IRunPromptResult { + readonly widget: IChatWidget; } /** @@ -57,7 +79,7 @@ const isAttachedAsCurrentPrompt = ( return false; } - if (implicitContext.isPrompt === false) { + if (implicitContext.isInstructions === false) { return false; } @@ -78,30 +100,61 @@ const isAttachedAsCurrentPrompt = ( }; /** - * Attaches provided prompts to a chat input. + * Attaches provided instructions to a chat input. */ -export const attachInstructionsFile = async ( - file: URI, +export const attachInstructionsFiles = async ( + files: URI[], options: IAttachOptions, ): Promise => { - const { skipIfImplicitlyAttached } = options; const widget = await getChatWidgetObject(options); - if (skipIfImplicitlyAttached && isAttachedAsCurrentPrompt(file, widget)) { - return { widget, wasAlreadyAttached: true }; - } + const wasAlreadyAttached: URI[] = []; - const wasAlreadyAttached = widget - .attachmentModel - .promptInstructions - .add(file); + for (const file of files) { + if (widget.attachmentModel.promptInstructions.add(file)) { + wasAlreadyAttached.push(file); + continue; + } + } return { widget, wasAlreadyAttached }; }; /** - * Gets a chat widget based on the provided {@link IChatAttachPromptActionOptions.widget widget} + * Runs the prompt file + */ +export const runPromptFile = async ( + file: URI, + options: IRunPromptOptions, +): Promise => { + + const widget = await getChatWidgetObject(options); + + let wasAlreadyAttached: boolean = false; + + if (isAttachedAsCurrentPrompt(file, widget)) { + wasAlreadyAttached = true; + } else { + if (widget.attachmentModel.promptInstructions.add(file)) { + wasAlreadyAttached = true; + } + } + + // submit the prompt immediately + await widget.acceptInput(); + + // detach the prompt immediately, unless was attached + // before the action was executed + if (wasAlreadyAttached === false) { + await detachPrompt(file, { widget }); + } + + return { widget }; +}; + +/** + * Gets a chat widget based on the provided {@link IChatAttachInstructionsActionOptions.widget widget} * reference and the `inNewChat` flag. * * @throws if failed to reveal a chat widget. diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts index 2cdfbc3d3c5..007d56982bc 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts @@ -5,32 +5,32 @@ import { SUPER_KEY_NAME } from '../constants.js'; import { localize } from '../../../../../../../../../nls.js'; -import { ISelectPromptOptions } from '../askToSelectPrompt.js'; +import { ISelectInstructionsOptions } from '../askToSelectPrompt.js'; /** - * Creates a placeholder text to show in the prompt selection dialog. + * Creates a placeholder text to show in the attach instructions selection dialog. */ export const createPlaceholderText = ( - options: ISelectPromptOptions, + options: ISelectInstructionsOptions, ): string => { const { widget } = options; let text = localize( - 'commands.prompts.use.select-dialog.placeholder', - 'Select a prompt to use', + 'commands.select-dialog.instructions.placeholder', + 'Select instructions files to attach', ); // if no widget reference is provided, add the note about the `ctrl`/`cmd` // modifier that can be leveraged by users to alter the command behavior if (widget === undefined) { const superModifierNote = localize( - 'commands.prompts.use.select-dialog.super-modifier-note', + 'commands.select-dialog.super-modifier-note', '{0}-key to use in new chat', SUPER_KEY_NAME, ); text += localize( - 'commands.prompts.use.select-dialog.modifier-notes', + 'commands.select-dialog.modifier-notes', ' (hold {0})', superModifierNote, ); diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts index a5af698eb45..e99c2868139 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts @@ -14,4 +14,4 @@ export const registerReusablePromptActions = () => { registerAttachPromptActions(); }; -export { runAttachPromptAction } from './chatAttachPromptAction.js'; +export { runAttachInstructionsAction } from './chatAttachPromptAction.js'; diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 73b16e8fa5c..cf98cd06aa6 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -53,7 +53,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { dom.clearNode(this.domNode); this.renderDisposables.clear(); - const attachmentTypeName = (this.attachment.isPrompt === false) + const attachmentTypeName = (this.attachment.isInstructions === false) ? localize('file.lowercase', "file") : localize('prompt.lowercase', "prompt"); @@ -73,7 +73,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { const currentFileHint = currentFile + (this.attachment.enabled ? '' : ` (${inactive})`); const title = `${currentFileHint}\n${uriLabel}`; - const icon = (this.attachment.isPrompt) + const icon = this.attachment.isInstructions ? ThemeIcon.fromId(Codicon.bookmark.id) : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts b/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts index ac4fcefcd20..ac42a552db4 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts @@ -106,12 +106,12 @@ export class PromptAttachmentWidget extends Disposable { const fileBasename = basename(file); const fileDirname = dirname(file); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = localize('chat.promptAttachment', "Prompt attachment, {0}", friendlyName); + const ariaLabel = localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName); const uriLabel = this.labelService.getUriLabel(file, { relative: true }); - const promptLabel = localize('prompt', "Prompt"); + const instructionsLabel = localize('instructions', "Instructions"); - let title = `${promptLabel} ${uriLabel}`; + let title = `${instructionsLabel} ${uriLabel}`; // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), @@ -144,7 +144,7 @@ export class PromptAttachmentWidget extends Disposable { this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, promptLabel)); + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, instructionsLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); // create the `remove` button diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index b4ed7d4b906..813eb776cca 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -204,7 +204,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } // if prompt attached as an implicit "current file" context - return (this.implicitContext.isPrompt && this.implicitContext.enabled); + return (this.implicitContext.isInstructions && this.implicitContext.enabled); } private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 5a6d6a5ef57..d3b520d6661 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -24,7 +24,7 @@ import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry } from '../../ import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; -import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/constants.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../common/promptSyntax/constants.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { createPromptVariableId } from '../chatAttachmentModel/chatPromptAttachmentsCollection.js'; @@ -238,7 +238,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli get id() { // IDs for prompt files need to start with a special prefix // that is used by the copilot extension to identify them - if (this.isPrompt) { + if (this.isInstructions) { assertDefined( this.value, 'Implicit prompt attachments must have a value.', @@ -265,7 +265,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } get name(): string { - const fileType = this.isPrompt ? 'prompt' : 'file'; + const fileType = this.isInstructions ? 'prompt' : 'file'; if (URI.isUri(this.value)) { return `${fileType}:${basename(this.value)}`; @@ -279,7 +279,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli readonly kind = 'implicit'; get modelDescription(): string { - if (this.isPrompt) { + if (this.isInstructions) { if (URI.isUri(this.value)) { return `User's active prompt instructions file`; } else if (this._isSelection) { @@ -314,8 +314,8 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } private _languageId: string | undefined; - get isPrompt() { - return (this._languageId === PROMPT_LANGUAGE_ID); + get isInstructions() { + return (this._languageId === INSTRUCTIONS_LANGUAGE_ID); } private _enabled = true; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts index c4f16a957f9..c5b075e470e 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts @@ -9,28 +9,21 @@ import { CHAT_CATEGORY } from '../../actions/chatActions.js'; import { IChatWidget, IChatWidgetService } from '../../chat.js'; import { ChatContextKeys } from '../../../common/chatContextKeys.js'; import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; -import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; -import { runAttachPromptAction } from '../../actions/reusablePromptActions/index.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { runAttachInstructionsAction } from '../../actions/reusablePromptActions/index.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MenuId, MenuRegistry } from '../../../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../../editor/browser/editorBrowser.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; /** * Command ID of the "Attach Instructions" command. */ export const INSTRUCTIONS_COMMAND_ID = 'workbench.command.instructions.use'; -/** - * Command ID of the "Run Prompt" command. - */ -export const RUN_PROMPT_COMMAND_ID = 'workbench.command.prompt.run'; - /** * Keybinding of the "Use Instructions" command. * The `cmd + /` is the current keybinding for 'attachment', so we use @@ -61,14 +54,14 @@ const command = async ( ): Promise => { const commandService = accessor.get(ICommandService); - await runAttachPromptAction({ - resource: getActivePromptUri(accessor), + await runAttachInstructionsAction({ + resource: getActiveInstructionsFileUri(accessor), widget: getFocusedChatWidget(accessor), }, commandService); }; /** - * Get chat widget reference to attach prompt to. + * Get chat widget reference to attach instructions to. */ export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined { const chatWidgetService = accessor.get(IChatWidgetService); @@ -87,53 +80,21 @@ export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | } /** - * Gets active editor instance, if any. + * Gets `URI` of a instructions file open in an active editor instance, if any. */ -export function getActiveCodeEditor(accessor: ServicesAccessor): IActiveCodeEditor | undefined { - const editorService = accessor.get(IEditorService); - const { activeTextEditorControl } = editorService; - - if (isCodeEditor(activeTextEditorControl) && activeTextEditorControl.hasModel()) { - return activeTextEditorControl; - } - - if (isDiffEditor(activeTextEditorControl)) { - const originalEditor = activeTextEditorControl.getOriginalEditor(); - if (!originalEditor.hasModel()) { - return undefined; - } - - return originalEditor; - } - - return undefined; -} - -/** - * Gets `URI` of a prompt file open in an active editor instance, if any. - */ -export const getActivePromptUri = ( +export const getActiveInstructionsFileUri = ( accessor: ServicesAccessor, ): URI | undefined => { - const activeEditor = getActiveCodeEditor(accessor); - if (!activeEditor) { - return undefined; - } - - const model = activeEditor.getModel(); - if (model.getLanguageId() === PROMPT_LANGUAGE_ID) { + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) { return model.uri; } - - if (isPromptFile(model.uri)) { - return model.uri; - } - return undefined; }; /** - * Register the "Use Prompt" command with its keybinding. + * Register the "Attach Instructions" command with its keybinding. */ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: INSTRUCTIONS_COMMAND_ID, @@ -149,7 +110,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: INSTRUCTIONS_COMMAND_ID, - title: localize('commands.prompts.use.title', "Use Instructions"), + title: localize('commands.prompts.use.title', "Attach Instructions"), category: CHAT_CATEGORY }, when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 78e65838760..3e1eacdc23e 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -76,7 +76,7 @@ export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVaria readonly isFile: true; readonly value: URI | Location | undefined; readonly isSelection: boolean; - readonly isPrompt: boolean; + readonly isInstructions: boolean; enabled: boolean; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts index f3a390da4ae..881b554288c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LanguageFilter } from '../../../../../editor/common/languageSelector.js'; +import { LanguageSelector } from '../../../../../editor/common/languageSelector.js'; /** * Documentation link for the reusable prompts feature. @@ -17,8 +17,11 @@ export const INSTRUCTIONS_DOCUMENTATION_URL = PROMPT_DOCUMENTATION_URL; // TODO: export const PROMPT_LANGUAGE_ID = 'prompt'; /** - * Prompt files language selector. + * Language ID for instructions syntax. */ -export const LANGUAGE_SELECTOR: LanguageFilter = Object.freeze({ - language: PROMPT_LANGUAGE_ID, -}); +export const INSTRUCTIONS_LANGUAGE_ID = 'instructions'; + +/** + * Prompt and instructions files language selector. + */ +export const PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR: LanguageSelector = [PROMPT_LANGUAGE_ID, INSTRUCTIONS_LANGUAGE_ID]; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts index e4e9c612292..044862a39bf 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LANGUAGE_SELECTOR } from '../../constants.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../constants.js'; import { IPromptsService } from '../../service/types.js'; import { assert } from '../../../../../../../base/common/assert.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; @@ -25,7 +25,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { ) { super(); - this._register(this.languageService.linkProvider.register(LANGUAGE_SELECTOR, this)); + this._register(this.languageService.linkProvider.register(PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR, this)); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts index b3a3e919261..1ee50cc1f7f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts @@ -14,7 +14,7 @@ * - add `Windows` support */ -import { LANGUAGE_SELECTOR } from '../../constants.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../constants.js'; import { IPromptsService } from '../../service/types.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { extUri } from '../../../../../../../base/common/resources.js'; @@ -102,7 +102,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt ) { super(); - this._register(this.languageService.completionProvider.register(LANGUAGE_SELECTOR, this)); + this._register(this.languageService.completionProvider.register(PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR, this)); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts index 222fe6f97c8..6971439e6be 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PROMPT_LANGUAGE_ID } from '../../constants.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../constants.js'; import { ProviderInstanceBase } from './providerInstanceBase.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; @@ -96,15 +96,15 @@ export abstract class ProviderInstanceManagerBase { const { model, oldLanguageId } = event; - // if language is set to `prompt` language, handle that model + // if language is set to `prompt` or `instructions` language, handle that model if (isPromptFileModel(model)) { this.instances.get(model); return; } - // if the language is changed away from `prompt`, + // if the language is changed away from `prompt` or `instructions`, // remove and dispose provider for this model - if (oldLanguageId === PROMPT_LANGUAGE_ID) { + if (isPromptOrInstructionsFile(oldLanguageId)) { this.instances.remove(model, true); return; } @@ -133,6 +133,12 @@ export abstract class ProviderInstanceManagerBase { + return languageId === PROMPT_LANGUAGE_ID || languageId === INSTRUCTIONS_LANGUAGE_ID; +}; + /** * Check if a provided model is used for prompt files. */ @@ -148,7 +154,7 @@ const isPromptFileModel = ( return false; } - if (model.getLanguageId() !== PROMPT_LANGUAGE_ID) { + if (!isPromptOrInstructionsFile(model.getLanguageId())) { return false; }