Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/attachInstructionsAction.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ChatViewId, IChatWidget, IChatWidgetService, showChatView } from '../chat.js';
7
import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from '../actions/chatActions.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { localize, localize2 } from '../../../../../nls.js';
10
import { ChatContextKeys } from '../../common/chatContextKeys.js';
11
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
12
import { PromptsConfig } from '../../common/promptSyntax/config/config.js';
13
import { IViewsService } from '../../../../services/views/common/viewsService.js';
14
import { PromptFilePickers } from './pickers/promptFilePickers.js';
15
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
16
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
17
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
18
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
19
import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../chatContextPickService.js';
20
import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
21
import { Codicon } from '../../../../../base/common/codicons.js';
22
import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
23
import { INSTRUCTIONS_LANGUAGE_ID, PromptsType } from '../../common/promptSyntax/promptTypes.js';
24
import { compare } from '../../../../../base/common/strings.js';
25
import { ILabelService } from '../../../../../platform/label/common/label.js';
26
import { dirname } from '../../../../../base/common/resources.js';
27
import { IPromptFileVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/chatVariableEntries.js';
28
import { KeyMod, KeyCode } from '../../../../../base/common/keyCodes.js';
29
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
30
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
31
import { CancellationToken } from '../../../../../base/common/cancellation.js';
32
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
33
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
34
35
/**
36
* Action ID for the `Attach Instruction` action.
37
*/
38
const ATTACH_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.attach.instructions';
39
40
/**
41
* Action ID for the `Configure Instruction` action.
42
*/
43
const CONFIGURE_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.configure.instructions';
44
45
46
/**
47
* Options for the {@link AttachInstructionsAction} action.
48
*/
49
export interface IAttachInstructionsActionOptions {
50
51
/**
52
* Target chat widget reference to attach the instruction to. If the reference is
53
* provided, the command will attach the instruction as attachment of the widget.
54
* Otherwise, the command will re-use an existing one.
55
*/
56
readonly widget?: IChatWidget;
57
58
/**
59
* Instruction resource `URI` to attach to the chat input, if any.
60
* If provided the resource will be pre-selected in the prompt picker dialog,
61
* otherwise the dialog will show the prompts list without any pre-selection.
62
*/
63
readonly resource?: URI;
64
65
/**
66
* Whether to skip the instructions files selection dialog.
67
*
68
* Note! if this option is set to `true`, the {@link resource}
69
* option `must be defined`.
70
*/
71
readonly skipSelectionDialog?: boolean;
72
}
73
74
/**
75
* Action to attach a prompt to a chat widget input.
76
*/
77
class AttachInstructionsAction extends Action2 {
78
constructor() {
79
super({
80
id: ATTACH_INSTRUCTIONS_ACTION_ID,
81
title: localize2('attach-instructions.capitalized.ellipses', "Attach Instructions..."),
82
f1: false,
83
precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled),
84
category: CHAT_CATEGORY,
85
keybinding: {
86
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Slash,
87
weight: KeybindingWeight.WorkbenchContrib
88
},
89
menu: {
90
id: MenuId.CommandPalette,
91
when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled)
92
}
93
});
94
}
95
96
public override async run(
97
accessor: ServicesAccessor,
98
options?: IAttachInstructionsActionOptions,
99
): Promise<void> {
100
const viewsService = accessor.get(IViewsService);
101
const instaService = accessor.get(IInstantiationService);
102
103
if (!options) {
104
options = {
105
resource: getActiveInstructionsFileUri(accessor),
106
widget: getFocusedChatWidget(accessor),
107
};
108
}
109
110
const pickers = instaService.createInstance(PromptFilePickers);
111
112
const { skipSelectionDialog, resource } = options;
113
114
115
const widget = options.widget ?? (await showChatView(viewsService));
116
if (!widget) {
117
return;
118
}
119
120
if (skipSelectionDialog && resource) {
121
widget.attachmentModel.addContext(toPromptFileVariableEntry(resource, PromptFileVariableKind.Instruction));
122
widget.focusInput();
123
return;
124
}
125
126
const placeholder = localize(
127
'commands.instructions.select-dialog.placeholder',
128
'Select instructions files to attach',
129
);
130
131
const result = await pickers.selectPromptFile({ resource, placeholder, type: PromptsType.instructions });
132
133
if (result !== undefined) {
134
widget.attachmentModel.addContext(toPromptFileVariableEntry(result.promptFile, PromptFileVariableKind.Instruction));
135
widget.focusInput();
136
}
137
}
138
}
139
140
class ManageInstructionsFilesAction extends Action2 {
141
constructor() {
142
super({
143
id: CONFIGURE_INSTRUCTIONS_ACTION_ID,
144
title: localize2('configure-instructions', "Configure Instructions..."),
145
shortTitle: localize2('configure-instructions.short', "Instructions"),
146
icon: Codicon.bookmark,
147
f1: true,
148
precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled),
149
category: CHAT_CATEGORY,
150
menu: {
151
id: CHAT_CONFIG_MENU_ID,
152
when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),
153
order: 11,
154
group: '0_level'
155
}
156
});
157
}
158
159
public override async run(
160
accessor: ServicesAccessor,
161
): Promise<void> {
162
const openerService = accessor.get(IOpenerService);
163
const instaService = accessor.get(IInstantiationService);
164
165
const pickers = instaService.createInstance(PromptFilePickers);
166
167
const placeholder = localize(
168
'commands.prompt.manage-dialog.placeholder',
169
'Select the instructions file to open'
170
);
171
172
const result = await pickers.selectPromptFile({ placeholder, type: PromptsType.instructions, optionEdit: false });
173
if (result !== undefined) {
174
await openerService.open(result.promptFile);
175
}
176
177
}
178
}
179
180
181
function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined {
182
const chatWidgetService = accessor.get(IChatWidgetService);
183
184
const { lastFocusedWidget } = chatWidgetService;
185
if (!lastFocusedWidget) {
186
return undefined;
187
}
188
189
// the widget input `must` be focused at the time when command run
190
if (!lastFocusedWidget.hasInputFocus()) {
191
return undefined;
192
}
193
194
return lastFocusedWidget;
195
}
196
197
/**
198
* Gets `URI` of a instructions file open in an active editor instance, if any.
199
*/
200
function getActiveInstructionsFileUri(accessor: ServicesAccessor): URI | undefined {
201
const codeEditorService = accessor.get(ICodeEditorService);
202
const model = codeEditorService.getActiveCodeEditor()?.getModel();
203
if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) {
204
return model.uri;
205
}
206
return undefined;
207
}
208
209
/**
210
* Helper to register the `Attach Prompt` action.
211
*/
212
export function registerAttachPromptActions(): void {
213
registerAction2(AttachInstructionsAction);
214
registerAction2(ManageInstructionsFilesAction);
215
}
216
217
218
export class ChatInstructionsPickerPick implements IChatContextPickerItem {
219
220
readonly type = 'pickerPick';
221
readonly label = localize('chatContext.attach.instructions.label', 'Instructions...');
222
readonly icon = Codicon.bookmark;
223
readonly commandId = ATTACH_INSTRUCTIONS_ACTION_ID;
224
225
constructor(
226
@IPromptsService private readonly promptsService: IPromptsService,
227
@ILabelService private readonly labelService: ILabelService,
228
@IConfigurationService private readonly configurationService: IConfigurationService,
229
) { }
230
231
isEnabled(widget: IChatWidget): Promise<boolean> | boolean {
232
return PromptsConfig.enabled(this.configurationService);
233
}
234
235
asPicker(): IChatContextPicker {
236
237
const picks = this.promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None).then(value => {
238
239
const result: (IChatContextPickerPickItem | IQuickPickSeparator)[] = [];
240
241
value = value.slice(0).sort((a, b) => compare(a.storage, b.storage));
242
243
let storageType: string | undefined;
244
245
for (const { uri, storage } of value) {
246
247
if (storageType !== storage) {
248
storageType = storage;
249
result.push({
250
type: 'separator',
251
label: storage === 'user'
252
? localize('user-data-dir.capitalized', 'User data folder')
253
: this.labelService.getUriLabel(dirname(uri), { relative: true })
254
});
255
}
256
257
result.push({
258
label: getCleanPromptName(uri),
259
asAttachment: (): IPromptFileVariableEntry => {
260
return toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction);
261
}
262
});
263
}
264
return result;
265
});
266
267
return {
268
placeholder: localize('placeholder', 'Select instructions files to attach'),
269
picks,
270
configure: {
271
label: localize('configureInstructions', 'Configure Instructions...'),
272
commandId: CONFIGURE_INSTRUCTIONS_ACTION_ID
273
}
274
};
275
}
276
277
278
}
279
280