Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts
5284 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 { asArray } from '../../../../../base/common/arrays.js';
7
import { DeferredPromise, isThenable } from '../../../../../base/common/async.js';
8
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
11
import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
12
import { Schemas } from '../../../../../base/common/network.js';
13
import { autorun, observableValue } from '../../../../../base/common/observable.js';
14
import { ThemeIcon } from '../../../../../base/common/themables.js';
15
import { isObject } from '../../../../../base/common/types.js';
16
import { URI } from '../../../../../base/common/uri.js';
17
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
18
import { Range } from '../../../../../editor/common/core/range.js';
19
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
20
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
21
import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js';
22
import { localize, localize2 } from '../../../../../nls.js';
23
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
24
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
25
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
26
import { IFileService } from '../../../../../platform/files/common/files.js';
27
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
28
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
29
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
30
import { IListService } from '../../../../../platform/list/browser/listService.js';
31
import { ILogService } from '../../../../../platform/log/common/log.js';
32
import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js';
33
import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
34
import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js';
35
import { ResourceContextKey } from '../../../../common/contextkeys.js';
36
import { EditorResourceAccessor, isEditorCommandsContext, SideBySideEditor } from '../../../../common/editor.js';
37
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
38
import { IEditorService } from '../../../../services/editor/common/editorService.js';
39
import { ExplorerFolderContext } from '../../../files/common/files.js';
40
import { CTX_INLINE_CHAT_V2_ENABLED } from '../../../inlineChat/common/inlineChat.js';
41
import { AnythingQuickAccessProvider } from '../../../search/browser/anythingQuickAccess.js';
42
import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browser/searchTreeModel/searchTreeCommon.js';
43
import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js';
44
import { SearchContext } from '../../../search/common/constants.js';
45
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
46
import { IChatRequestVariableEntry, OmittedState } from '../../common/attachments/chatVariableEntries.js';
47
import { ChatAgentLocation, isSupportedChatFileScheme } from '../../common/constants.js';
48
import { IChatWidget, IChatWidgetService, IQuickChatService } from '../chat.js';
49
import { IChatContextPickerItem, IChatContextPickService, IChatContextValueItem, isChatContextPickerPickItem } from '../attachments/chatContextPickService.js';
50
import { isQuickChat } from '../widget/chatWidget.js';
51
import { resizeImage } from '../chatImageUtils.js';
52
import { registerPromptActions } from '../promptSyntax/promptFileActions.js';
53
import { CHAT_CATEGORY } from './chatActions.js';
54
55
export function registerChatContextActions() {
56
registerAction2(AttachContextAction);
57
registerAction2(AttachFileToChatAction);
58
registerAction2(AttachFolderToChatAction);
59
registerAction2(AttachSelectionToChatAction);
60
registerAction2(AttachSearchResultAction);
61
registerAction2(AttachPinnedEditorsToChatAction);
62
registerPromptActions();
63
}
64
65
async function withChatView(accessor: ServicesAccessor): Promise<IChatWidget | undefined> {
66
const chatWidgetService = accessor.get(IChatWidgetService);
67
68
const lastFocusedWidget = chatWidgetService.lastFocusedWidget;
69
if (!lastFocusedWidget || lastFocusedWidget.location === ChatAgentLocation.Chat) {
70
return chatWidgetService.revealWidget(); // only show chat view if we either have no chat view or its located in view container
71
}
72
return lastFocusedWidget;
73
}
74
75
abstract class AttachResourceAction extends Action2 {
76
77
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
78
const instaService = accessor.get(IInstantiationService);
79
const widget = await instaService.invokeFunction(withChatView);
80
if (!widget) {
81
return;
82
}
83
return instaService.invokeFunction(this.runWithWidget.bind(this), widget, ...args);
84
}
85
86
abstract runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: unknown[]): Promise<void>;
87
88
protected _getResources(accessor: ServicesAccessor, ...args: unknown[]): URI[] {
89
const editorService = accessor.get(IEditorService);
90
91
const contexts = isEditorCommandsContext(args[1]) ? this._getEditorResources(accessor, args) : Array.isArray(args[1]) ? args[1] : [args[0]];
92
const files = [];
93
for (const context of contexts) {
94
let uri;
95
if (URI.isUri(context)) {
96
uri = context;
97
} else if (isSearchTreeFileMatch(context)) {
98
uri = context.resource;
99
} else if (isSearchTreeMatch(context)) {
100
uri = context.parent().resource;
101
} else if (!context && editorService.activeTextEditorControl) {
102
uri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
103
}
104
105
if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) {
106
files.push(uri);
107
}
108
}
109
110
return files;
111
}
112
113
private _getEditorResources(accessor: ServicesAccessor, ...args: unknown[]): URI[] {
114
const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService));
115
116
return resolvedContext.groupedEditors
117
.flatMap(groupedEditor => groupedEditor.editors)
118
.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }))
119
.filter(uri => uri !== undefined);
120
}
121
}
122
123
class AttachFileToChatAction extends AttachResourceAction {
124
125
static readonly ID = 'workbench.action.chat.attachFile';
126
127
constructor() {
128
super({
129
id: AttachFileToChatAction.ID,
130
title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"),
131
category: CHAT_CATEGORY,
132
precondition: ChatContextKeys.enabled,
133
f1: true,
134
menu: [{
135
id: MenuId.SearchContext,
136
group: 'z_chat',
137
order: 1,
138
when: ContextKeyExpr.and(ChatContextKeys.enabled, SearchContext.FileMatchOrMatchFocusKey, SearchContext.SearchResultHeaderFocused.negate()),
139
}, {
140
id: MenuId.ExplorerContext,
141
group: '5_chat',
142
order: 1,
143
when: ContextKeyExpr.and(
144
ChatContextKeys.enabled,
145
ExplorerFolderContext.negate(),
146
ContextKeyExpr.or(
147
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
148
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)
149
)
150
),
151
}, {
152
id: MenuId.EditorTitleContext,
153
group: '2_chat',
154
order: 1,
155
when: ContextKeyExpr.and(
156
ChatContextKeys.enabled,
157
ContextKeyExpr.or(
158
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
159
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)
160
)
161
),
162
}, {
163
id: MenuId.EditorContext,
164
group: '1_chat',
165
order: 2,
166
when: ContextKeyExpr.and(
167
ChatContextKeys.enabled,
168
ContextKeyExpr.or(
169
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
170
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote),
171
ResourceContextKey.Scheme.isEqualTo(Schemas.untitled),
172
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)
173
)
174
)
175
}, {
176
id: MenuId.ChatEditorInlineGutter,
177
group: '2_chat',
178
order: 2,
179
when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate())
180
}]
181
});
182
}
183
184
override async runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: unknown[]): Promise<void> {
185
const files = this._getResources(accessor, ...args);
186
if (!files.length) {
187
return;
188
}
189
if (widget) {
190
widget.focusInput();
191
for (const file of files) {
192
widget.attachmentModel.addFile(file);
193
}
194
}
195
}
196
}
197
198
class AttachFolderToChatAction extends AttachResourceAction {
199
200
static readonly ID = 'workbench.action.chat.attachFolder';
201
202
constructor() {
203
super({
204
id: AttachFolderToChatAction.ID,
205
title: localize2('workbench.action.chat.attachFolder.label', "Add Folder to Chat"),
206
category: CHAT_CATEGORY,
207
f1: false,
208
menu: {
209
id: MenuId.ExplorerContext,
210
group: '5_chat',
211
order: 1,
212
when: ContextKeyExpr.and(
213
ChatContextKeys.enabled,
214
ExplorerFolderContext,
215
ContextKeyExpr.or(
216
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
217
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)
218
)
219
)
220
}
221
});
222
}
223
224
override async runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: unknown[]): Promise<void> {
225
const folders = this._getResources(accessor, ...args);
226
if (!folders.length) {
227
return;
228
}
229
if (widget) {
230
widget.focusInput();
231
for (const folder of folders) {
232
widget.attachmentModel.addFolder(folder);
233
}
234
}
235
}
236
}
237
238
class AttachPinnedEditorsToChatAction extends Action2 {
239
240
static readonly ID = 'workbench.action.chat.attachPinnedEditors';
241
242
constructor() {
243
super({
244
id: AttachPinnedEditorsToChatAction.ID,
245
title: localize2('workbench.action.chat.attachPinnedEditors.label', "Add Pinned Editors to Chat"),
246
category: CHAT_CATEGORY,
247
precondition: ChatContextKeys.enabled,
248
f1: true,
249
});
250
}
251
252
override async run(accessor: ServicesAccessor): Promise<void> {
253
const editorGroupsService = accessor.get(IEditorGroupsService);
254
const instaService = accessor.get(IInstantiationService);
255
256
const widget = await instaService.invokeFunction(withChatView);
257
if (!widget) {
258
return;
259
}
260
261
const files: URI[] = [];
262
for (const group of editorGroupsService.groups) {
263
for (const editor of group.editors) {
264
if (group.isPinned(editor)) {
265
const uri = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });
266
if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) {
267
files.push(uri);
268
}
269
}
270
}
271
}
272
273
if (!files.length) {
274
return;
275
}
276
277
widget.focusInput();
278
for (const file of files) {
279
widget.attachmentModel.addFile(file);
280
}
281
}
282
}
283
284
class AttachSelectionToChatAction extends Action2 {
285
286
static readonly ID = 'workbench.action.chat.attachSelection';
287
288
constructor() {
289
super({
290
id: AttachSelectionToChatAction.ID,
291
title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"),
292
category: CHAT_CATEGORY,
293
f1: true,
294
precondition: ChatContextKeys.enabled,
295
menu: [{
296
id: MenuId.EditorContext,
297
group: '1_chat',
298
order: 1,
299
when: ContextKeyExpr.and(
300
ChatContextKeys.enabled,
301
EditorContextKeys.hasNonEmptySelection,
302
ContextKeyExpr.or(
303
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
304
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote),
305
ResourceContextKey.Scheme.isEqualTo(Schemas.untitled),
306
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)
307
)
308
)
309
}, {
310
id: MenuId.ChatEditorInlineGutter,
311
group: '2_chat',
312
order: 1,
313
when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection)
314
}]
315
});
316
}
317
318
// eslint-disable-next-line @typescript-eslint/no-explicit-any
319
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
320
const editorService = accessor.get(IEditorService);
321
322
const widget = await accessor.get(IInstantiationService).invokeFunction(withChatView);
323
if (!widget) {
324
return;
325
}
326
327
const [_, matches] = args;
328
// If we have search matches, it means this is coming from the search widget
329
if (matches && matches.length > 0) {
330
const uris = new Map<URI, Range | undefined>();
331
for (const match of matches) {
332
if (isSearchTreeFileMatch(match)) {
333
uris.set(match.resource, undefined);
334
} else {
335
const context = { uri: match._parent.resource, range: match._range };
336
const range = uris.get(context.uri);
337
if (!range ||
338
range.startLineNumber !== context.range.startLineNumber && range.endLineNumber !== context.range.endLineNumber) {
339
uris.set(context.uri, context.range);
340
widget.attachmentModel.addFile(context.uri, context.range);
341
}
342
}
343
}
344
// Add the root files for all of the ones that didn't have a match
345
for (const uri of uris) {
346
const [resource, range] = uri;
347
if (!range) {
348
widget.attachmentModel.addFile(resource);
349
}
350
}
351
} else {
352
const activeEditor = editorService.activeTextEditorControl;
353
const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
354
if (activeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) {
355
const selection = activeEditor.getSelection();
356
if (selection) {
357
widget.focusInput();
358
const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection;
359
widget.attachmentModel.addFile(activeUri, range);
360
}
361
}
362
}
363
}
364
}
365
366
export class AttachSearchResultAction extends Action2 {
367
368
private static readonly Name = 'searchResults';
369
370
constructor() {
371
super({
372
id: 'workbench.action.chat.insertSearchResults',
373
title: localize2('chat.insertSearchResults', 'Add Search Results to Chat'),
374
category: CHAT_CATEGORY,
375
f1: false,
376
menu: [{
377
id: MenuId.SearchContext,
378
group: 'z_chat',
379
order: 3,
380
when: ContextKeyExpr.and(
381
ChatContextKeys.enabled,
382
SearchContext.SearchResultHeaderFocused),
383
}]
384
});
385
}
386
async run(accessor: ServicesAccessor) {
387
const logService = accessor.get(ILogService);
388
const widget = await accessor.get(IInstantiationService).invokeFunction(withChatView);
389
390
if (!widget) {
391
logService.trace('InsertSearchResultAction: no chat view available');
392
return;
393
}
394
395
const editor = widget.inputEditor;
396
const originalRange = editor.getSelection() ?? editor.getModel()?.getFullModelRange().collapseToEnd();
397
398
if (!originalRange) {
399
logService.trace('InsertSearchResultAction: no selection');
400
return;
401
}
402
403
let insertText = `#${AttachSearchResultAction.Name}`;
404
const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startLineNumber + insertText.length);
405
// check character before the start of the range. If it's not a space, add a space
406
const model = editor.getModel();
407
if (model && model.getValueInRange(new Range(originalRange.startLineNumber, originalRange.startColumn - 1, originalRange.startLineNumber, originalRange.startColumn)) !== ' ') {
408
insertText = ' ' + insertText;
409
}
410
const success = editor.executeEdits('chatInsertSearch', [{ range: varRange, text: insertText + ' ' }]);
411
if (!success) {
412
logService.trace(`InsertSearchResultAction: failed to insert "${insertText}"`);
413
return;
414
}
415
}
416
}
417
418
/** This is our type */
419
interface IContextPickItemItem extends IQuickPickItem {
420
kind: 'contextPick';
421
item: IChatContextValueItem | IChatContextPickerItem;
422
}
423
424
/** These are the types we get from "platform QP" */
425
type IQuickPickServicePickItem = IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickPickItemWithResource;
426
427
function isIContextPickItemItem(obj: unknown): obj is IContextPickItemItem {
428
return (
429
isObject(obj)
430
&& typeof (<IContextPickItemItem>obj).kind === 'string'
431
&& (<IContextPickItemItem>obj).kind === 'contextPick'
432
);
433
}
434
435
function isIGotoSymbolQuickPickItem(obj: unknown): obj is IGotoSymbolQuickPickItem {
436
return (
437
isObject(obj)
438
&& typeof (obj as IGotoSymbolQuickPickItem).symbolName === 'string'
439
&& !!(obj as IGotoSymbolQuickPickItem).uri
440
&& !!(obj as IGotoSymbolQuickPickItem).range);
441
}
442
443
function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource {
444
return (
445
isObject(obj)
446
&& URI.isUri((obj as IQuickPickItemWithResource).resource));
447
}
448
449
450
export class AttachContextAction extends Action2 {
451
452
constructor() {
453
super({
454
id: 'workbench.action.chat.attachContext',
455
title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."),
456
icon: Codicon.attach,
457
category: CHAT_CATEGORY,
458
keybinding: {
459
when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),
460
primary: KeyMod.CtrlCmd | KeyCode.Slash,
461
weight: KeybindingWeight.EditorContrib
462
},
463
menu: {
464
when: ContextKeyExpr.and(
465
ContextKeyExpr.or(
466
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat),
467
ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditorInline), CTX_INLINE_CHAT_V2_ENABLED)
468
),
469
ContextKeyExpr.or(
470
ChatContextKeys.lockedToCodingAgent.negate(),
471
ChatContextKeys.agentSupportsAttachments
472
)
473
),
474
id: MenuId.ChatInputAttachmentToolbar,
475
group: 'navigation',
476
order: 3
477
},
478
479
});
480
}
481
482
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
483
484
const instantiationService = accessor.get(IInstantiationService);
485
const widgetService = accessor.get(IChatWidgetService);
486
const contextKeyService = accessor.get(IContextKeyService);
487
const keybindingService = accessor.get(IKeybindingService);
488
const contextPickService = accessor.get(IChatContextPickService);
489
490
const context = args[0] as { widget?: IChatWidget; placeholder?: string } | undefined;
491
const widget = context?.widget ?? widgetService.lastFocusedWidget;
492
if (!widget) {
493
return;
494
}
495
496
const quickPickItems: IContextPickItemItem[] = [];
497
498
for (const item of contextPickService.items) {
499
500
if (item.isEnabled && !await item.isEnabled(widget)) {
501
continue;
502
}
503
504
quickPickItems.push({
505
kind: 'contextPick',
506
item,
507
label: item.label,
508
iconClass: ThemeIcon.asClassName(item.icon),
509
keybinding: item.commandId ? keybindingService.lookupKeybinding(item.commandId, contextKeyService) : undefined,
510
});
511
}
512
513
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, context?.placeholder);
514
}
515
516
private _show(accessor: ServicesAccessor, widget: IChatWidget, additionPicks: IContextPickItemItem[] | undefined, placeholder?: string) {
517
const quickInputService = accessor.get(IQuickInputService);
518
const quickChatService = accessor.get(IQuickChatService);
519
const instantiationService = accessor.get(IInstantiationService);
520
const commandService = accessor.get(ICommandService);
521
522
const providerOptions: AnythingQuickAccessProviderRunOptions = {
523
filter: (pick) => {
524
if (isIQuickPickItemWithResource(pick) && pick.resource) {
525
return instantiationService.invokeFunction(accessor => isSupportedChatFileScheme(accessor, pick.resource!.scheme));
526
}
527
return true;
528
},
529
additionPicks,
530
handleAccept: async (item: IQuickPickServicePickItem | IContextPickItemItem, isBackgroundAccept: boolean) => {
531
532
if (isIContextPickItemItem(item)) {
533
534
let isDone = true;
535
if (item.item.type === 'valuePick') {
536
this._handleContextPick(item.item, widget);
537
538
} else if (item.item.type === 'pickerPick') {
539
isDone = await this._handleContextPickerItem(quickInputService, commandService, item.item, widget);
540
}
541
542
if (!isDone) {
543
// restart picker when sub-picker didn't return anything
544
instantiationService.invokeFunction(this._show.bind(this), widget, additionPicks, placeholder);
545
return;
546
}
547
548
} else {
549
instantiationService.invokeFunction(this._handleQPPick.bind(this), widget, isBackgroundAccept, item);
550
}
551
if (isQuickChat(widget)) {
552
quickChatService.open();
553
}
554
}
555
};
556
557
quickInputService.quickAccess.show('', {
558
enabledProviderPrefixes: [
559
AnythingQuickAccessProvider.PREFIX,
560
SymbolsQuickAccessProvider.PREFIX,
561
AbstractGotoSymbolQuickAccessProvider.PREFIX
562
],
563
placeholder: placeholder ?? localize('chatContext.attach.placeholder', 'Search attachments'),
564
providerOptions,
565
});
566
}
567
568
private async _handleQPPick(accessor: ServicesAccessor, widget: IChatWidget, isInBackground: boolean, pick: IQuickPickServicePickItem) {
569
const fileService = accessor.get(IFileService);
570
const textModelService = accessor.get(ITextModelService);
571
572
const toAttach: IChatRequestVariableEntry[] = [];
573
574
if (isIQuickPickItemWithResource(pick) && pick.resource) {
575
if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) {
576
// checks if the file is an image
577
if (URI.isUri(pick.resource)) {
578
// read the image and attach a new file context.
579
const readFile = await fileService.readFile(pick.resource);
580
const resizedImage = await resizeImage(readFile.value.buffer);
581
toAttach.push({
582
id: pick.resource.toString(),
583
name: pick.label,
584
fullName: pick.label,
585
value: resizedImage,
586
kind: 'image',
587
references: [{ reference: pick.resource, kind: 'reference' }]
588
});
589
}
590
} else {
591
let omittedState = OmittedState.NotOmitted;
592
try {
593
const createdModel = await textModelService.createModelReference(pick.resource);
594
createdModel.dispose();
595
} catch {
596
omittedState = OmittedState.Full;
597
}
598
599
toAttach.push({
600
kind: 'file',
601
id: pick.resource.toString(),
602
value: pick.resource,
603
name: pick.label,
604
omittedState
605
});
606
}
607
} else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) {
608
toAttach.push({
609
kind: 'generic',
610
id: JSON.stringify({ uri: pick.uri, range: pick.range.decoration }),
611
value: { uri: pick.uri, range: pick.range.decoration },
612
fullName: pick.label,
613
name: pick.symbolName!,
614
});
615
}
616
617
618
widget.attachmentModel.addContext(...toAttach);
619
620
if (!isInBackground) {
621
// Set focus back into the input once the user is done attaching items
622
// so that the user can start typing their message
623
widget.focusInput();
624
}
625
}
626
627
private async _handleContextPick(item: IChatContextValueItem, widget: IChatWidget) {
628
629
const value = await item.asAttachment(widget);
630
if (Array.isArray(value)) {
631
widget.attachmentModel.addContext(...value);
632
} else if (value) {
633
widget.attachmentModel.addContext(value);
634
}
635
}
636
637
private async _handleContextPickerItem(quickInputService: IQuickInputService, commandService: ICommandService, item: IChatContextPickerItem, widget: IChatWidget): Promise<boolean> {
638
639
const pickerConfig = item.asPicker(widget);
640
641
const store = new DisposableStore();
642
643
const goBackItem: IQuickPickItem = {
644
label: localize('goBack', 'Go back ↩'),
645
alwaysShow: true
646
};
647
const configureItem = pickerConfig.configure ? {
648
label: pickerConfig.configure.label,
649
commandId: pickerConfig.configure.commandId,
650
alwaysShow: true
651
} : undefined;
652
const extraPicks: QuickPickItem[] = [{ type: 'separator' }];
653
if (configureItem) {
654
extraPicks.push(configureItem);
655
}
656
extraPicks.push(goBackItem);
657
658
const qp = store.add(quickInputService.createQuickPick({ useSeparators: true }));
659
660
const cts = new CancellationTokenSource();
661
store.add(qp.onDidHide(() => cts.cancel()));
662
store.add(toDisposable(() => cts.dispose(true)));
663
664
qp.placeholder = pickerConfig.placeholder;
665
qp.matchOnDescription = true;
666
qp.matchOnDetail = true;
667
// qp.ignoreFocusOut = true;
668
qp.canAcceptInBackground = true;
669
qp.busy = true;
670
qp.show();
671
672
if (isThenable(pickerConfig.picks)) {
673
const items = await (pickerConfig.picks.then(value => {
674
return ([] as QuickPickItem[]).concat(value, extraPicks);
675
}));
676
677
qp.items = items;
678
qp.busy = false;
679
} else {
680
const query = observableValue<string>('attachContext.query', qp.value);
681
store.add(qp.onDidChangeValue(() => query.set(qp.value, undefined)));
682
683
const picksObservable = pickerConfig.picks(query, cts.token);
684
store.add(autorun(reader => {
685
const { busy, picks } = picksObservable.read(reader);
686
qp.items = ([] as QuickPickItem[]).concat(picks, extraPicks);
687
qp.busy = busy;
688
}));
689
}
690
691
if (cts.token.isCancellationRequested) {
692
pickerConfig.dispose?.();
693
return true; // picker got hidden already
694
}
695
696
const defer = new DeferredPromise<boolean>();
697
const addPromises: Promise<void>[] = [];
698
699
store.add(qp.onDidAccept(async e => {
700
const noop = 'noop';
701
const [selected] = qp.selectedItems;
702
if (isChatContextPickerPickItem(selected)) {
703
const attachment = selected.asAttachment();
704
if (!attachment || attachment === noop) {
705
return;
706
}
707
if (isThenable(attachment)) {
708
addPromises.push(attachment.then(v => {
709
if (v !== noop) {
710
widget.attachmentModel.addContext(...asArray(v));
711
}
712
}));
713
} else {
714
widget.attachmentModel.addContext(...asArray(attachment));
715
}
716
}
717
if (selected === goBackItem) {
718
if (pickerConfig.goBack?.()) {
719
// Custom goBack handled the navigation, stay in the picker
720
return; // Don't complete, keep picker open
721
}
722
// Default behavior: go back to main picker
723
defer.complete(false);
724
}
725
if (selected === configureItem) {
726
defer.complete(true);
727
commandService.executeCommand(configureItem.commandId);
728
}
729
if (!e.inBackground) {
730
defer.complete(true);
731
}
732
}));
733
734
store.add(qp.onDidHide(() => {
735
defer.complete(true);
736
pickerConfig.dispose?.();
737
}));
738
739
try {
740
const result = await defer.p;
741
qp.busy = true; // if still visible
742
await Promise.all(addPromises);
743
return result;
744
} finally {
745
store.dispose();
746
}
747
}
748
}
749
750