This commit is contained in:
Martin Aeschlimann
2025-04-14 16:46:34 +02:00
committed by Oleg Solomko
parent 4097e0fdf7
commit e2f4bbb62c
19 changed files with 218 additions and 197 deletions

View File

@@ -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
}
}
},

View File

@@ -1,4 +1,4 @@
{
"displayName": "Prompt Language Basics",
"description": "Syntax highlighting for Prompt documents."
"description": "Syntax highlighting for Prompt and Instructions documents."
}

View File

@@ -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,
});

View File

@@ -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<void> {
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<void> => {
return await commandService.executeCommand(

View File

@@ -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.
*/

View File

@@ -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<void> => {
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<IQuickPickItem> | 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<void>(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,

View File

@@ -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<IAttachResult> => {
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<IRunPromptResult> => {
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.

View File

@@ -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,
);

View File

@@ -14,4 +14,4 @@ export const registerReusablePromptActions = () => {
registerAttachPromptActions();
};
export { runAttachPromptAction } from './chatAttachPromptAction.js';
export { runAttachInstructionsAction } from './chatAttachPromptAction.js';

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<void> => {
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)

View File

@@ -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;
}

View File

@@ -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];

View File

@@ -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));
}
/**

View File

@@ -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));
}
/**

View File

@@ -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<TInstance extends ProviderInst
modelService.onModelLanguageChanged((event) => {
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<TInstance extends ProviderInst
}
}
const isPromptOrInstructionsFile = (
languageId: string
): boolean => {
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;
}