Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts
5297 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 * as dom from '../../../../../base/browser/dom.js';
7
import { $ } from '../../../../../base/browser/dom.js';
8
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
9
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
10
import { Button } from '../../../../../base/browser/ui/button/button.js';
11
import { HoverStyle, IDelayedHoverOptions, type IHoverLifecycleOptions, type IHoverOptions } from '../../../../../base/browser/ui/hover/hover.js';
12
import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
13
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
14
import { Codicon } from '../../../../../base/common/codicons.js';
15
import * as event from '../../../../../base/common/event.js';
16
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
17
import { Iterable } from '../../../../../base/common/iterator.js';
18
import { KeyCode } from '../../../../../base/common/keyCodes.js';
19
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
20
import { Schemas } from '../../../../../base/common/network.js';
21
import { basename, dirname } from '../../../../../base/common/path.js';
22
import { ThemeIcon } from '../../../../../base/common/themables.js';
23
import { URI } from '../../../../../base/common/uri.js';
24
import { IRange } from '../../../../../editor/common/core/range.js';
25
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
26
import { LanguageFeatureRegistry } from '../../../../../editor/common/languageFeatureRegistry.js';
27
import { Location, SymbolKind } from '../../../../../editor/common/languages.js';
28
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
29
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
30
import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';
31
import { IModelService } from '../../../../../editor/common/services/model.js';
32
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
33
import { localize } from '../../../../../nls.js';
34
import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
35
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
36
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
37
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
38
import { IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
39
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
40
import { fillInSymbolsDragData } from '../../../../../platform/dnd/browser/dnd.js';
41
import { IOpenEditorOptions, registerOpenEditorListeners } from '../../../../../platform/editor/browser/editor.js';
42
import { ITextEditorOptions } from '../../../../../platform/editor/common/editor.js';
43
import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';
44
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
45
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
46
import { ILabelService } from '../../../../../platform/label/common/label.js';
47
import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
48
import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js';
49
import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js';
50
import { fillEditorsDragData } from '../../../../browser/dnd.js';
51
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from '../../../../browser/labels.js';
52
import { ResourceContextKey } from '../../../../common/contextkeys.js';
53
import { IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
54
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
55
import { revealInSideBarCommand } from '../../../files/browser/fileActions.contribution.js';
56
import { CellUri } from '../../../notebook/common/notebookCommon.js';
57
import { INotebookService } from '../../../notebook/common/notebookService.js';
58
import { toHistoryItemHoverContent } from '../../../scm/browser/scmHistory.js';
59
import { getHistoryItemEditorTitle } from '../../../scm/browser/util.js';
60
import { ITerminalService } from '../../../terminal/browser/terminal.js';
61
import { IChatContentReference } from '../../common/chatService/chatService.js';
62
import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js';
63
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js';
64
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
65
import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js';
66
import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
67
import { IChatContextService } from '../contextContrib/chatContextService.js';
68
69
const commonHoverOptions: Partial<IHoverOptions> = {
70
style: HoverStyle.Pointer,
71
position: {
72
hoverPosition: HoverPosition.BELOW
73
},
74
trapFocus: true,
75
};
76
const commonHoverLifecycleOptions: IHoverLifecycleOptions = {
77
groupId: 'chat-attachments',
78
};
79
80
abstract class AbstractChatAttachmentWidget extends Disposable {
81
public readonly element: HTMLElement;
82
public readonly label: IResourceLabel;
83
84
private readonly _onDidDelete: event.Emitter<Event> = this._register(new event.Emitter<Event>());
85
get onDidDelete(): event.Event<Event> {
86
return this._onDidDelete.event;
87
}
88
89
private readonly _onDidOpen: event.Emitter<void> = this._register(new event.Emitter<void>());
90
get onDidOpen(): event.Event<void> {
91
return this._onDidOpen.event;
92
}
93
94
constructor(
95
protected readonly attachment: IChatRequestVariableEntry,
96
private readonly options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
97
container: HTMLElement,
98
contextResourceLabels: ResourceLabels,
99
protected readonly currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
100
@ICommandService protected readonly commandService: ICommandService,
101
@IOpenerService protected readonly openerService: IOpenerService,
102
@IConfigurationService protected readonly configurationService: IConfigurationService,
103
@ITerminalService protected readonly terminalService?: ITerminalService,
104
) {
105
super();
106
this.element = dom.append(container, $('.chat-attached-context-attachment.show-file-icons'));
107
this.attachClearButton();
108
this.label = contextResourceLabels.create(this.element, { supportIcons: true, hoverTargetOverride: this.element });
109
this._register(this.label);
110
this.element.tabIndex = 0;
111
this.element.role = 'button';
112
113
// Add middle-click support for removal
114
this._register(dom.addDisposableListener(this.element, dom.EventType.AUXCLICK, (e: MouseEvent) => {
115
if (e.button === 1 /* Middle Button */ && this.options.supportsDeletion && !this.attachment.range) {
116
e.preventDefault();
117
e.stopPropagation();
118
this._onDidDelete.fire(e);
119
}
120
}));
121
}
122
123
protected modelSupportsVision() {
124
return modelSupportsVision(this.currentLanguageModel);
125
}
126
127
protected attachClearButton() {
128
129
if (this.attachment.range || !this.options.supportsDeletion) {
130
// no clear button for attachments with ranges because range means
131
// referenced from prompt
132
return;
133
}
134
135
const clearButton = new Button(this.element, {
136
supportIcons: true,
137
hoverDelegate: createInstantHoverDelegate(),
138
title: localize('chat.attachment.clearButton', "Remove from context")
139
});
140
clearButton.element.tabIndex = -1;
141
clearButton.icon = Codicon.close;
142
this._register(clearButton);
143
this._register(event.Event.once(clearButton.onDidClick)((e) => {
144
this._onDidDelete.fire(e);
145
}));
146
this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => {
147
if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) {
148
this._onDidDelete.fire(e.browserEvent);
149
}
150
}));
151
}
152
153
protected addResourceOpenHandlers(resource: URI, range: IRange | undefined): void {
154
this.element.style.cursor = 'pointer';
155
156
this._register(registerOpenEditorListeners(this.element, async options => {
157
if (this.attachment.kind === 'directory') {
158
await this.openResource(resource, options, true);
159
} else {
160
await this.openResource(resource, options, false, range);
161
}
162
}));
163
}
164
165
protected async openResource(resource: URI, options: Partial<IOpenEditorOptions>, isDirectory: true): Promise<void>;
166
protected async openResource(resource: URI, options: Partial<IOpenEditorOptions>, isDirectory: false, range: IRange | undefined): Promise<void>;
167
protected async openResource(resource: URI, openOptions: Partial<IOpenEditorOptions>, isDirectory?: boolean, range?: IRange): Promise<void> {
168
if (isDirectory) {
169
// Reveal Directory in explorer
170
this.commandService.executeCommand(revealInSideBarCommand.id, resource);
171
return;
172
}
173
174
if (resource.scheme === Schemas.vscodeTerminal) {
175
this.terminalService?.openResource(resource);
176
return;
177
}
178
179
// Open file in editor
180
const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;
181
const options: OpenInternalOptions = {
182
fromUserGesture: true,
183
openToSide: openOptions.openToSide,
184
editorOptions: {
185
...openTextEditorOptions,
186
...openOptions.editorOptions
187
},
188
};
189
190
await this.openerService.open(resource, options);
191
this._onDidOpen.fire();
192
this.element.focus();
193
}
194
}
195
196
function modelSupportsVision(currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined) {
197
return currentLanguageModel?.metadata.capabilities?.vision ?? false;
198
}
199
200
201
export class FileAttachmentWidget extends AbstractChatAttachmentWidget {
202
203
constructor(
204
resource: URI,
205
range: IRange | undefined,
206
attachment: IChatRequestVariableEntry,
207
correspondingContentReference: IChatContentReference | undefined,
208
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
209
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
210
container: HTMLElement,
211
contextResourceLabels: ResourceLabels,
212
@ICommandService commandService: ICommandService,
213
@IOpenerService openerService: IOpenerService,
214
@IConfigurationService configurationService: IConfigurationService,
215
@IThemeService private readonly themeService: IThemeService,
216
@IHoverService private readonly hoverService: IHoverService,
217
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
218
@IInstantiationService private readonly instantiationService: IInstantiationService,
219
) {
220
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
221
222
const fileBasename = basename(resource.path);
223
const fileDirname = dirname(resource.path);
224
const friendlyName = `${fileBasename} ${fileDirname}`;
225
let ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName);
226
227
if (attachment.omittedState === OmittedState.Full) {
228
ariaLabel = localize('chat.omittedFileAttachment', "Omitted this file: {0}", attachment.name);
229
this.renderOmittedWarning(friendlyName, ariaLabel);
230
} else {
231
const fileOptions: IFileLabelOptions = { hidePath: true, title: correspondingContentReference?.options?.status?.description };
232
this.label.setFile(resource, attachment.kind === 'file' ? {
233
...fileOptions,
234
fileKind: FileKind.FILE,
235
range,
236
} : {
237
...fileOptions,
238
fileKind: FileKind.FOLDER,
239
icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined
240
});
241
}
242
243
this.element.ariaLabel = ariaLabel;
244
245
this.instantiationService.invokeFunction(accessor => {
246
this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));
247
});
248
this.addResourceOpenHandlers(resource, range);
249
}
250
251
private renderOmittedWarning(friendlyName: string, ariaLabel: string) {
252
const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-warning'));
253
const textLabel = dom.$('span.chat-attached-context-custom-text', {}, friendlyName);
254
this.element.appendChild(pillIcon);
255
this.element.appendChild(textLabel);
256
257
const hoverElement = dom.$('div.chat-attached-context-hover');
258
hoverElement.setAttribute('aria-label', ariaLabel);
259
this.element.classList.add('warning');
260
261
hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this file type.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name : this.currentLanguageModel ?? 'This model');
262
this._register(this.hoverService.setupDelayedHover(this.element, {
263
...commonHoverOptions,
264
content: hoverElement,
265
}, commonHoverLifecycleOptions));
266
}
267
}
268
269
270
export class TerminalCommandAttachmentWidget extends AbstractChatAttachmentWidget {
271
272
constructor(
273
attachment: ITerminalVariableEntry,
274
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
275
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
276
container: HTMLElement,
277
contextResourceLabels: ResourceLabels,
278
@ICommandService commandService: ICommandService,
279
@IOpenerService openerService: IOpenerService,
280
@IConfigurationService configurationService: IConfigurationService,
281
@IHoverService private readonly hoverService: IHoverService,
282
@ITerminalService protected override readonly terminalService: ITerminalService,
283
) {
284
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService, terminalService);
285
286
const ariaLabel = localize('chat.terminalCommand', "Terminal command, {0}", attachment.command);
287
const clickHandler = () => this.openResource(attachment.resource, { editorOptions: { preserveFocus: true } }, false, undefined);
288
289
this._register(createTerminalCommandElements(this.element, attachment, ariaLabel, this.hoverService, clickHandler));
290
291
this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {
292
const event = new StandardKeyboardEvent(e);
293
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
294
dom.EventHelper.stop(e, true);
295
await clickHandler();
296
}
297
}));
298
}
299
}
300
301
const enum TerminalConstants {
302
MaxAttachmentOutputLineCount = 5,
303
MaxAttachmentOutputLineLength = 80,
304
}
305
306
function createTerminalCommandElements(
307
element: HTMLElement,
308
attachment: ITerminalVariableEntry,
309
ariaLabel: string,
310
hoverService: IHoverService,
311
clickHandler: () => Promise<void>
312
): IDisposable {
313
const disposable = new DisposableStore();
314
element.ariaLabel = ariaLabel;
315
element.style.cursor = 'pointer';
316
317
const terminalIconSpan = dom.$('span');
318
terminalIconSpan.classList.add(...ThemeIcon.asClassNameArray(Codicon.terminal));
319
const pillIcon = dom.$('div.chat-attached-context-pill', {}, terminalIconSpan);
320
const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.command);
321
element.appendChild(pillIcon);
322
element.appendChild(textLabel);
323
324
disposable.add(dom.addDisposableListener(element, dom.EventType.CLICK, e => {
325
e.preventDefault();
326
e.stopPropagation();
327
clickHandler();
328
}));
329
330
disposable.add(hoverService.setupDelayedHover(element, () => getHoverContent(ariaLabel, attachment), commonHoverLifecycleOptions));
331
return disposable;
332
}
333
334
function getHoverContent(ariaLabel: string, attachment: ITerminalVariableEntry): IDelayedHoverOptions {
335
{
336
const hoverElement = dom.$('div.chat-attached-context-hover');
337
hoverElement.setAttribute('aria-label', ariaLabel);
338
339
const commandTitle = dom.$('div', {}, typeof attachment.exitCode === 'number'
340
? localize('chat.terminalCommandHoverCommandTitleExit', "Command: {0}, exit code: {1}", attachment.command, attachment.exitCode)
341
: localize('chat.terminalCommandHoverCommandTitle', "Command"));
342
commandTitle.classList.add('attachment-additional-info');
343
const commandBlock = dom.$('pre.chat-terminal-command-block');
344
hoverElement.append(commandTitle, commandBlock);
345
346
if (attachment.output && attachment.output.trim().length > 0) {
347
const outputTitle = dom.$('div', {}, localize('chat.terminalCommandHoverOutputTitle', "Output:"));
348
outputTitle.classList.add('attachment-additional-info');
349
const outputBlock = dom.$('pre.chat-terminal-command-output');
350
const fullOutputLines = attachment.output.split('\n');
351
const hoverOutputLines = [];
352
for (const line of fullOutputLines) {
353
if (hoverOutputLines.length >= TerminalConstants.MaxAttachmentOutputLineCount) {
354
hoverOutputLines.push('...');
355
break;
356
}
357
const trimmed = line.trim();
358
if (trimmed.length === 0) {
359
continue;
360
}
361
if (trimmed.length > TerminalConstants.MaxAttachmentOutputLineLength) {
362
hoverOutputLines.push(`${trimmed.slice(0, TerminalConstants.MaxAttachmentOutputLineLength)}...`);
363
} else {
364
hoverOutputLines.push(trimmed);
365
}
366
}
367
outputBlock.textContent = hoverOutputLines.join('\n');
368
hoverElement.append(outputTitle, outputBlock);
369
}
370
371
return {
372
...commonHoverOptions,
373
content: hoverElement,
374
};
375
}
376
}
377
378
export class ImageAttachmentWidget extends AbstractChatAttachmentWidget {
379
380
constructor(
381
resource: URI | undefined,
382
attachment: IChatRequestVariableEntry,
383
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
384
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
385
container: HTMLElement,
386
contextResourceLabels: ResourceLabels,
387
@ICommandService commandService: ICommandService,
388
@IOpenerService openerService: IOpenerService,
389
@IConfigurationService configurationService: IConfigurationService,
390
@IHoverService private readonly hoverService: IHoverService,
391
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
392
@IInstantiationService instantiationService: IInstantiationService,
393
@ILabelService private readonly labelService: ILabelService,
394
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
395
) {
396
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
397
398
let ariaLabel: string;
399
if (attachment.omittedState === OmittedState.Full) {
400
ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name);
401
} else if (attachment.omittedState === OmittedState.Partial) {
402
ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name);
403
} else {
404
ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name);
405
}
406
407
const ref = attachment.references?.[0]?.reference;
408
resource = ref && URI.isUri(ref) ? ref : undefined;
409
const clickHandler = async () => {
410
if (resource) {
411
await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined);
412
}
413
};
414
415
const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : 'Current model';
416
417
const fullName = resource ? this.labelService.getUriLabel(resource) : (attachment.fullName || attachment.name);
418
this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState, this.chatEntitlementService.previewFeaturesDisabled));
419
420
if (resource) {
421
this.addResourceOpenHandlers(resource, undefined);
422
instantiationService.invokeFunction(accessor => {
423
this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));
424
});
425
}
426
}
427
}
428
429
function createImageElements(resource: URI | undefined, name: string, fullName: string,
430
element: HTMLElement,
431
buffer: ArrayBuffer | Uint8Array,
432
hoverService: IHoverService, ariaLabel: string,
433
currentLanguageModelName: string | undefined,
434
clickHandler: () => void,
435
currentLanguageModel?: ILanguageModelChatMetadataAndIdentifier,
436
omittedState?: OmittedState,
437
previewFeaturesDisabled?: boolean): IDisposable {
438
439
const disposable = new DisposableStore();
440
if (omittedState === OmittedState.Partial) {
441
element.classList.add('partial-warning');
442
}
443
444
element.ariaLabel = ariaLabel;
445
element.style.position = 'relative';
446
447
if (resource) {
448
element.style.cursor = 'pointer';
449
disposable.add(dom.addDisposableListener(element, 'click', clickHandler));
450
}
451
const supportsVision = modelSupportsVision(currentLanguageModel);
452
const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$((supportsVision && !previewFeaturesDisabled) ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning'));
453
const textLabel = dom.$('span.chat-attached-context-custom-text', {}, name);
454
element.appendChild(pillIcon);
455
element.appendChild(textLabel);
456
457
const hoverElement = dom.$('div.chat-attached-context-hover');
458
hoverElement.setAttribute('aria-label', ariaLabel);
459
460
if (previewFeaturesDisabled) {
461
element.classList.add('warning');
462
hoverElement.textContent = localize('chat.imageAttachmentPreviewFeaturesDisabled', "Vision is disabled by your organization.");
463
disposable.add(hoverService.setupDelayedHover(element, {
464
content: hoverElement,
465
style: HoverStyle.Pointer,
466
}));
467
} else if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) {
468
element.classList.add('warning');
469
hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", currentLanguageModelName ?? 'This model');
470
disposable.add(hoverService.setupDelayedHover(element, {
471
content: hoverElement,
472
style: HoverStyle.Pointer,
473
}));
474
} else {
475
disposable.add(hoverService.setupDelayedHover(element, {
476
content: hoverElement,
477
style: HoverStyle.Pointer,
478
}));
479
480
const blob = new Blob([buffer as Uint8Array<ArrayBuffer>], { type: 'image/png' });
481
const url = URL.createObjectURL(blob);
482
const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' });
483
const pill = dom.$('div.chat-attached-context-pill', {}, pillImg);
484
485
// eslint-disable-next-line no-restricted-syntax
486
const existingPill = element.querySelector('.chat-attached-context-pill');
487
if (existingPill) {
488
existingPill.replaceWith(pill);
489
}
490
491
const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' });
492
const imageContainer = dom.$('div.chat-attached-context-image-container', {}, hoverImage);
493
hoverElement.appendChild(imageContainer);
494
495
if (resource) {
496
const urlContainer = dom.$('a.chat-attached-context-url', {}, omittedState === OmittedState.Partial ? localize('chat.imageAttachmentWarning', "This GIF was partially omitted - current frame will be sent.") : fullName);
497
const separator = dom.$('div.chat-attached-context-url-separator');
498
disposable.add(dom.addDisposableListener(urlContainer, 'click', () => clickHandler()));
499
hoverElement.append(separator, urlContainer);
500
}
501
502
hoverImage.onload = () => { URL.revokeObjectURL(url); };
503
hoverImage.onerror = () => {
504
// reset to original icon on error or invalid image
505
const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media'));
506
const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon);
507
// eslint-disable-next-line no-restricted-syntax
508
const existingPill = element.querySelector('.chat-attached-context-pill');
509
if (existingPill) {
510
existingPill.replaceWith(pill);
511
}
512
};
513
}
514
return disposable;
515
}
516
517
export class PasteAttachmentWidget extends AbstractChatAttachmentWidget {
518
519
constructor(
520
attachment: IChatRequestPasteVariableEntry,
521
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
522
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
523
container: HTMLElement,
524
contextResourceLabels: ResourceLabels,
525
@ICommandService commandService: ICommandService,
526
@IOpenerService openerService: IOpenerService,
527
@IConfigurationService configurationService: IConfigurationService,
528
@IHoverService private readonly hoverService: IHoverService,
529
@IInstantiationService private readonly instantiationService: IInstantiationService,
530
) {
531
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
532
533
const ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
534
this.element.ariaLabel = ariaLabel;
535
536
const classNames = ['file-icon', `${attachment.language}-lang-file-icon`];
537
let resource: URI | undefined;
538
let range: IRange | undefined;
539
540
if (attachment.copiedFrom) {
541
resource = attachment.copiedFrom.uri;
542
range = attachment.copiedFrom.range;
543
const filename = basename(resource.path);
544
this.label.setLabel(filename, undefined, { extraClasses: classNames });
545
} else {
546
this.label.setLabel(attachment.fileName, undefined, { extraClasses: classNames });
547
}
548
this.element.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`));
549
550
this.element.style.position = 'relative';
551
552
const sourceUri = attachment.copiedFrom?.uri;
553
const hoverContent = new MarkdownString(`${sourceUri ? this.instantiationService.invokeFunction(accessor => accessor.get(ILabelService).getUriLabel(sourceUri, { relative: true })) : attachment.fileName}\n\n---\n\n\`\`\`${attachment.language}\n\n${attachment.code}\n\`\`\``);
554
this._register(this.hoverService.setupDelayedHover(this.element, {
555
...commonHoverOptions,
556
content: hoverContent,
557
}, commonHoverLifecycleOptions));
558
559
const copiedFromResource = attachment.copiedFrom?.uri;
560
if (copiedFromResource) {
561
this._register(this.instantiationService.invokeFunction(hookUpResourceAttachmentDragAndContextMenu, this.element, copiedFromResource));
562
this.addResourceOpenHandlers(copiedFromResource, range);
563
}
564
}
565
}
566
567
export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {
568
569
private readonly _tooltipHover: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
570
571
constructor(
572
resource: URI | undefined,
573
range: IRange | undefined,
574
attachment: IChatRequestVariableEntry,
575
correspondingContentReference: IChatContentReference | undefined,
576
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
577
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
578
container: HTMLElement,
579
contextResourceLabels: ResourceLabels,
580
@ICommandService commandService: ICommandService,
581
@IOpenerService openerService: IOpenerService,
582
@IConfigurationService configurationService: IConfigurationService,
583
@IContextKeyService private readonly contextKeyService: IContextKeyService,
584
@IInstantiationService private readonly instantiationService: IInstantiationService,
585
@IHoverService private readonly hoverService: IHoverService,
586
@IModelService private readonly modelService: IModelService,
587
@ILanguageService private readonly languageService: ILanguageService,
588
) {
589
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
590
591
const attachmentLabel = attachment.fullName ?? attachment.name;
592
593
// Derive icon classes from resourceUri for file/folder icons
594
if (isStringVariableEntry(attachment) && attachment.icon && (ThemeIcon.isFile(attachment.icon) || ThemeIcon.isFolder(attachment.icon)) && attachment.resourceUri) {
595
const fileKind = ThemeIcon.isFolder(attachment.icon) ? FileKind.FOLDER : FileKind.FILE;
596
const iconClasses = getIconClasses(this.modelService, this.languageService, attachment.resourceUri, fileKind);
597
this.label.setLabel(attachmentLabel, correspondingContentReference?.options?.status?.description, { extraClasses: iconClasses });
598
} else {
599
const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;
600
this.label.setLabel(withIcon, correspondingContentReference?.options?.status?.description);
601
}
602
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
603
604
if (attachment.kind === 'diagnostic') {
605
if (attachment.filterUri) {
606
resource = attachment.filterUri ? URI.revive(attachment.filterUri) : undefined;
607
range = attachment.filterRange;
608
} else {
609
this.element.style.cursor = 'pointer';
610
this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => {
611
this.commandService.executeCommand('workbench.panel.markers.view.focus');
612
}));
613
}
614
}
615
616
if (attachment.kind === 'symbol') {
617
const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));
618
this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext));
619
}
620
621
// Handle click for string context attachments with context commands
622
if (isStringVariableEntry(attachment) && attachment.commandId) {
623
this.element.style.cursor = 'pointer';
624
const contextItemHandle = attachment.handle;
625
this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {
626
const chatContextService = this.instantiationService.invokeFunction(accessor => accessor.get(IChatContextService));
627
await chatContextService.executeChatContextItemCommand(contextItemHandle);
628
}));
629
}
630
631
// Setup tooltip hover for string context attachments
632
if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) {
633
this._setupTooltipHover(attachment.tooltip);
634
}
635
636
if (resource) {
637
this.addResourceOpenHandlers(resource, range);
638
}
639
}
640
641
private _setupTooltipHover(tooltip: IMarkdownString): void {
642
this._tooltipHover.value = this.hoverService.setupDelayedHover(this.element, {
643
content: tooltip,
644
appearance: { showPointer: true },
645
});
646
}
647
}
648
649
export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
650
651
private hintElement: HTMLElement;
652
653
constructor(
654
attachment: IPromptFileVariableEntry,
655
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
656
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
657
container: HTMLElement,
658
contextResourceLabels: ResourceLabels,
659
@ICommandService commandService: ICommandService,
660
@IOpenerService openerService: IOpenerService,
661
@IConfigurationService configurationService: IConfigurationService,
662
@ILabelService private readonly labelService: ILabelService,
663
@IInstantiationService private readonly instantiationService: IInstantiationService,
664
) {
665
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
666
667
668
this.hintElement = dom.append(this.element, dom.$('span.prompt-type'));
669
670
this.updateLabel(attachment);
671
672
this.instantiationService.invokeFunction(accessor => {
673
this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, attachment.value));
674
});
675
this.addResourceOpenHandlers(attachment.value, undefined);
676
}
677
678
private updateLabel(attachment: IPromptFileVariableEntry) {
679
const resource = attachment.value;
680
const fileBasename = basename(resource.path);
681
const fileDirname = dirname(resource.path);
682
const friendlyName = `${fileBasename} ${fileDirname}`;
683
const isPrompt = attachment.id.startsWith(PromptFileVariableKind.PromptFile);
684
const ariaLabel = isPrompt
685
? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName)
686
: localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName);
687
const typeLabel = isPrompt
688
? localize('prompt', "Prompt")
689
: localize('instructions', "Instructions");
690
691
const title = this.labelService.getUriLabel(resource) + (attachment.originLabel ? `\n${attachment.originLabel}` : '');
692
693
//const { topError } = this.promptFile;
694
this.element.classList.remove('warning', 'error');
695
696
// if there are some errors/warning during the process of resolving
697
// attachment references (including all the nested child references),
698
// add the issue details in the hover title for the attachment, one
699
// error/warning at a time because there is a limited space available
700
// if (topError) {
701
// const { errorSubject: subject } = topError;
702
// const isError = (subject === 'root');
703
// this.element.classList.add((isError) ? 'error' : 'warning');
704
705
// const severity = (isError)
706
// ? localize('error', "Error")
707
// : localize('warning', "Warning");
708
709
// title += `\n[${severity}]: ${topError.localizedMessage}`;
710
// }
711
712
const fileWithoutExtension = getCleanPromptName(resource);
713
this.label.setFile(URI.file(fileWithoutExtension), {
714
fileKind: FileKind.FILE,
715
hidePath: true,
716
range: undefined,
717
title,
718
icon: ThemeIcon.fromId(Codicon.bookmark.id),
719
extraClasses: [],
720
});
721
722
this.hintElement.innerText = typeLabel;
723
724
725
this.element.ariaLabel = ariaLabel;
726
}
727
}
728
729
export class PromptTextAttachmentWidget extends AbstractChatAttachmentWidget {
730
731
constructor(
732
attachment: IPromptTextVariableEntry,
733
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
734
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
735
container: HTMLElement,
736
contextResourceLabels: ResourceLabels,
737
@ICommandService commandService: ICommandService,
738
@IOpenerService openerService: IOpenerService,
739
@IConfigurationService configurationService: IConfigurationService,
740
@IPreferencesService preferencesService: IPreferencesService,
741
@IHoverService hoverService: IHoverService
742
) {
743
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
744
745
if (attachment.settingId) {
746
const openSettings = () => preferencesService.openSettings({ jsonEditor: false, query: `@id:${attachment.settingId}` });
747
748
this.element.style.cursor = 'pointer';
749
this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async (e: MouseEvent) => {
750
dom.EventHelper.stop(e, true);
751
openSettings();
752
}));
753
754
this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {
755
const event = new StandardKeyboardEvent(e);
756
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
757
dom.EventHelper.stop(e, true);
758
openSettings();
759
}
760
}));
761
}
762
this.label.setLabel(localize('instructions.label', 'Additional Instructions'), undefined, undefined);
763
764
this._register(hoverService.setupDelayedHover(this.element, {
765
...commonHoverOptions,
766
content: attachment.value,
767
}, commonHoverLifecycleOptions));
768
}
769
}
770
771
772
export class ToolSetOrToolItemAttachmentWidget extends AbstractChatAttachmentWidget {
773
constructor(
774
attachment: ChatRequestToolReferenceEntry,
775
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
776
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
777
container: HTMLElement,
778
contextResourceLabels: ResourceLabels,
779
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
780
@ICommandService commandService: ICommandService,
781
@IOpenerService openerService: IOpenerService,
782
@IConfigurationService configurationService: IConfigurationService,
783
@IHoverService hoverService: IHoverService,
784
) {
785
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
786
787
788
const toolOrToolSet = Iterable.find(toolsService.getTools(currentLanguageModel?.metadata), tool => tool.id === attachment.id) ?? Iterable.find(toolsService.getToolSetsForModel(currentLanguageModel?.metadata), toolSet => toolSet.id === attachment.id);
789
790
let name = attachment.name;
791
const icon = attachment.icon ?? Codicon.tools;
792
793
if (isToolSet(toolOrToolSet)) {
794
name = toolOrToolSet.referenceName;
795
} else if (toolOrToolSet) {
796
name = toolOrToolSet.toolReferenceName ?? name;
797
}
798
799
this.label.setLabel(`$(${icon.id})\u00A0${name}`, undefined);
800
801
this.element.style.cursor = 'pointer';
802
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", name);
803
804
let hoverContent: string | undefined;
805
806
if (isToolSet(toolOrToolSet)) {
807
hoverContent = localize('toolset', "{0} - {1}", toolOrToolSet.description ?? toolOrToolSet.referenceName, toolOrToolSet.source.label);
808
} else if (toolOrToolSet) {
809
hoverContent = localize('tool', "{0} - {1}", toolOrToolSet.userDescription ?? toolOrToolSet.modelDescription, toolOrToolSet.source.label);
810
}
811
812
if (hoverContent) {
813
this._register(hoverService.setupDelayedHover(this.element, {
814
...commonHoverOptions,
815
content: hoverContent,
816
}, commonHoverLifecycleOptions));
817
}
818
}
819
820
821
}
822
823
export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachmentWidget {
824
constructor(
825
resource: URI,
826
attachment: INotebookOutputVariableEntry,
827
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
828
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
829
container: HTMLElement,
830
contextResourceLabels: ResourceLabels,
831
@ICommandService commandService: ICommandService,
832
@IOpenerService openerService: IOpenerService,
833
@IConfigurationService configurationService: IConfigurationService,
834
@IHoverService private readonly hoverService: IHoverService,
835
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
836
@INotebookService private readonly notebookService: INotebookService,
837
@IInstantiationService private readonly instantiationService: IInstantiationService,
838
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
839
) {
840
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
841
842
switch (attachment.mimeType) {
843
case 'application/vnd.code.notebook.error': {
844
this.renderErrorOutput(resource, attachment);
845
break;
846
}
847
case 'image/png':
848
case 'image/jpeg':
849
case 'image/svg': {
850
this.renderImageOutput(resource, attachment);
851
break;
852
}
853
default: {
854
this.renderGenericOutput(resource, attachment);
855
}
856
}
857
858
this.instantiationService.invokeFunction(accessor => {
859
this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));
860
});
861
this.addResourceOpenHandlers(resource, undefined);
862
}
863
getAriaLabel(attachment: INotebookOutputVariableEntry): string {
864
return localize('chat.NotebookImageAttachment', "Attached Notebook output, {0}", attachment.name);
865
}
866
private renderErrorOutput(resource: URI, attachment: INotebookOutputVariableEntry) {
867
const attachmentLabel = attachment.name;
868
const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;
869
const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();
870
let title: string | undefined = undefined;
871
try {
872
const error = JSON.parse(new TextDecoder().decode(buffer)) as Error;
873
if (error.name && error.message) {
874
title = `${error.name}: ${error.message}`;
875
}
876
} catch {
877
//
878
}
879
this.label.setLabel(withIcon, undefined, { title });
880
this.element.ariaLabel = this.getAriaLabel(attachment);
881
}
882
private renderGenericOutput(resource: URI, attachment: INotebookOutputVariableEntry) {
883
this.element.ariaLabel = this.getAriaLabel(attachment);
884
this.label.setFile(resource, { hidePath: true, icon: ThemeIcon.fromId('output') });
885
}
886
private renderImageOutput(resource: URI, attachment: INotebookOutputVariableEntry) {
887
let ariaLabel: string;
888
if (attachment.omittedState === OmittedState.Full) {
889
ariaLabel = localize('chat.omittedNotebookImageAttachment', "Omitted this Notebook ouput: {0}", attachment.name);
890
} else if (attachment.omittedState === OmittedState.Partial) {
891
ariaLabel = localize('chat.partiallyOmittedNotebookImageAttachment', "Partially omitted this Notebook output: {0}", attachment.name);
892
} else {
893
ariaLabel = this.getAriaLabel(attachment);
894
}
895
896
const clickHandler = async () => await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined);
897
const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : undefined;
898
const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();
899
this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState, this.chatEntitlementService.previewFeaturesDisabled));
900
}
901
902
private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) {
903
const parsedInfo = CellUri.parseCellOutputUri(resource);
904
if (!parsedInfo || typeof parsedInfo.cellHandle !== 'number' || typeof parsedInfo.outputIndex !== 'number') {
905
return undefined;
906
}
907
const notebook = this.notebookService.getNotebookTextModel(parsedInfo.notebook);
908
if (!notebook) {
909
return undefined;
910
}
911
const cell = notebook.cells.find(c => c.handle === parsedInfo.cellHandle);
912
if (!cell) {
913
return undefined;
914
}
915
const output = cell.outputs.length > parsedInfo.outputIndex ? cell.outputs[parsedInfo.outputIndex] : undefined;
916
return output?.outputs.find(o => o.mime === attachment.mimeType);
917
}
918
919
}
920
921
export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget {
922
constructor(
923
attachment: IElementVariableEntry,
924
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
925
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
926
container: HTMLElement,
927
contextResourceLabels: ResourceLabels,
928
@ICommandService commandService: ICommandService,
929
@IOpenerService openerService: IOpenerService,
930
@IConfigurationService configurationService: IConfigurationService,
931
@IEditorService editorService: IEditorService,
932
) {
933
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
934
935
const ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name);
936
this.element.ariaLabel = ariaLabel;
937
938
this.element.style.position = 'relative';
939
this.element.style.cursor = 'pointer';
940
const attachmentLabel = attachment.name;
941
const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;
942
this.label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) });
943
944
this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {
945
const content = attachment.value?.toString() || '';
946
await editorService.openEditor({
947
resource: undefined,
948
contents: content,
949
options: {
950
pinned: true
951
}
952
});
953
}));
954
}
955
}
956
957
export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget {
958
constructor(
959
attachment: ISCMHistoryItemVariableEntry,
960
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
961
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
962
container: HTMLElement,
963
contextResourceLabels: ResourceLabels,
964
@ICommandService commandService: ICommandService,
965
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
966
@IHoverService hoverService: IHoverService,
967
@IOpenerService openerService: IOpenerService,
968
@IConfigurationService configurationService: IConfigurationService,
969
@IThemeService themeService: IThemeService
970
) {
971
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
972
973
this.label.setLabel(attachment.name, undefined);
974
975
this.element.style.cursor = 'pointer';
976
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
977
978
const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);
979
this._store.add(hoverService.setupDelayedHover(this.element, {
980
...commonHoverOptions,
981
content,
982
}, commonHoverLifecycleOptions));
983
this._store.add(disposables);
984
985
this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => {
986
dom.EventHelper.stop(e, true);
987
this._openAttachment(attachment);
988
}));
989
990
this._store.add(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
991
const event = new StandardKeyboardEvent(e);
992
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
993
dom.EventHelper.stop(e, true);
994
this._openAttachment(attachment);
995
}
996
}));
997
}
998
999
private async _openAttachment(attachment: ISCMHistoryItemVariableEntry): Promise<void> {
1000
await this.commandService.executeCommand('_workbench.openMultiDiffEditor', {
1001
title: getHistoryItemEditorTitle(attachment.historyItem), multiDiffSourceUri: attachment.value
1002
});
1003
}
1004
}
1005
1006
export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachmentWidget {
1007
constructor(
1008
attachment: ISCMHistoryItemChangeVariableEntry,
1009
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
1010
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
1011
container: HTMLElement,
1012
contextResourceLabels: ResourceLabels,
1013
@ICommandService commandService: ICommandService,
1014
@IHoverService hoverService: IHoverService,
1015
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
1016
@IOpenerService openerService: IOpenerService,
1017
@IConfigurationService configurationService: IConfigurationService,
1018
@IThemeService themeService: IThemeService,
1019
@IEditorService private readonly editorService: IEditorService,
1020
) {
1021
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
1022
1023
const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${attachment.historyItem.displayId ?? attachment.historyItem.id}`;
1024
this.label.setFile(attachment.value, { fileKind: FileKind.FILE, hidePath: true, nameSuffix });
1025
1026
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
1027
1028
const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);
1029
this._store.add(hoverService.setupDelayedHover(this.element, {
1030
...commonHoverOptions, content,
1031
}, commonHoverLifecycleOptions));
1032
this._store.add(disposables);
1033
1034
this.addResourceOpenHandlers(attachment.value, undefined);
1035
}
1036
1037
protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: true): Promise<void>;
1038
protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: false, range: IRange | undefined): Promise<void>;
1039
protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory?: boolean, range?: IRange): Promise<void> {
1040
const attachment = this.attachment as ISCMHistoryItemChangeVariableEntry;
1041
const historyItem = attachment.historyItem;
1042
1043
await this.editorService.openEditor({
1044
resource,
1045
label: `${basename(resource.path)} (${historyItem.displayId ?? historyItem.id})`,
1046
options: { ...options.editorOptions }
1047
}, options.openToSide ? SIDE_GROUP : undefined);
1048
}
1049
}
1050
1051
export class SCMHistoryItemChangeRangeAttachmentWidget extends AbstractChatAttachmentWidget {
1052
constructor(
1053
attachment: ISCMHistoryItemChangeRangeVariableEntry,
1054
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
1055
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
1056
container: HTMLElement,
1057
contextResourceLabels: ResourceLabels,
1058
@ICommandService commandService: ICommandService,
1059
@IOpenerService openerService: IOpenerService,
1060
@IConfigurationService configurationService: IConfigurationService,
1061
@IEditorService private readonly editorService: IEditorService,
1062
) {
1063
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);
1064
1065
const historyItemStartId = attachment.historyItemChangeStart.historyItem.displayId ?? attachment.historyItemChangeStart.historyItem.id;
1066
const historyItemEndId = attachment.historyItemChangeEnd.historyItem.displayId ?? attachment.historyItemChangeEnd.historyItem.id;
1067
1068
const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${historyItemStartId}..${historyItemEndId}`;
1069
this.label.setFile(attachment.value, { fileKind: FileKind.FILE, hidePath: true, nameSuffix });
1070
1071
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
1072
1073
this.addResourceOpenHandlers(attachment.value, undefined);
1074
}
1075
1076
protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: true): Promise<void>;
1077
protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: false, range: IRange | undefined): Promise<void>;
1078
protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory?: boolean, range?: IRange): Promise<void> {
1079
const attachment = this.attachment as ISCMHistoryItemChangeRangeVariableEntry;
1080
const historyItemChangeStart = attachment.historyItemChangeStart;
1081
const historyItemChangeEnd = attachment.historyItemChangeEnd;
1082
1083
const originalUriTitle = `${basename(historyItemChangeStart.uri.fsPath)} (${historyItemChangeStart.historyItem.displayId ?? historyItemChangeStart.historyItem.id})`;
1084
const modifiedUriTitle = `${basename(historyItemChangeEnd.uri.fsPath)} (${historyItemChangeEnd.historyItem.displayId ?? historyItemChangeEnd.historyItem.id})`;
1085
1086
await this.editorService.openEditor({
1087
original: { resource: historyItemChangeStart.uri },
1088
modified: { resource: historyItemChangeEnd.uri },
1089
label: `${originalUriTitle} ↔ ${modifiedUriTitle}`,
1090
options: { ...options.editorOptions }
1091
}, options.openToSide ? SIDE_GROUP : undefined);
1092
}
1093
}
1094
1095
export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, resource: URI): IDisposable {
1096
const contextKeyService = accessor.get(IContextKeyService);
1097
const instantiationService = accessor.get(IInstantiationService);
1098
1099
const store = new DisposableStore();
1100
1101
// Context
1102
const scopedContextKeyService = store.add(contextKeyService.createScoped(widget));
1103
store.add(setResourceContext(accessor, scopedContextKeyService, resource));
1104
1105
// Drag and drop
1106
widget.draggable = true;
1107
store.add(dom.addDisposableListener(widget, 'dragstart', e => {
1108
instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [resource], e));
1109
e.dataTransfer?.setDragImage(widget, 0, 0);
1110
}));
1111
1112
// Context menu
1113
store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, MenuId.ChatInputResourceAttachmentContext, resource));
1114
1115
return store;
1116
}
1117
1118
export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable {
1119
const instantiationService = accessor.get(IInstantiationService);
1120
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
1121
const textModelService = accessor.get(ITextModelService);
1122
1123
const store = new DisposableStore();
1124
1125
// Context
1126
store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri));
1127
1128
const chatResourceContext = chatAttachmentResourceContextKey.bindTo(scopedContextKeyService);
1129
chatResourceContext.set(attachment.value.uri.toString());
1130
1131
// Drag and drop
1132
widget.draggable = true;
1133
store.add(dom.addDisposableListener(widget, 'dragstart', e => {
1134
instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [{ resource: attachment.value.uri, selection: attachment.value.range }], e));
1135
1136
fillInSymbolsDragData([{
1137
fsPath: attachment.value.uri.fsPath,
1138
range: attachment.value.range,
1139
name: attachment.name,
1140
kind: attachment.kind,
1141
}], e);
1142
1143
e.dataTransfer?.setDragImage(widget, 0, 0);
1144
}));
1145
1146
// Context menu
1147
const providerContexts: ReadonlyArray<[IContextKey<boolean>, LanguageFeatureRegistry<unknown>]> = [
1148
[EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider],
1149
[EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider],
1150
[EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider],
1151
[EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider],
1152
];
1153
1154
const updateContextKeys = async () => {
1155
const modelRef = await textModelService.createModelReference(attachment.value.uri);
1156
try {
1157
const model = modelRef.object.textEditorModel;
1158
for (const [contextKey, registry] of providerContexts) {
1159
contextKey.set(registry.has(model));
1160
}
1161
} finally {
1162
modelRef.dispose();
1163
}
1164
};
1165
store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, contextMenuId, attachment.value, updateContextKeys));
1166
1167
return store;
1168
}
1169
1170
function setResourceContext(accessor: ServicesAccessor, scopedContextKeyService: IScopedContextKeyService, resource: URI) {
1171
const fileService = accessor.get(IFileService);
1172
const languageService = accessor.get(ILanguageService);
1173
const modelService = accessor.get(IModelService);
1174
1175
const resourceContextKey = new ResourceContextKey(scopedContextKeyService, fileService, languageService, modelService);
1176
resourceContextKey.set(resource);
1177
return resourceContextKey;
1178
}
1179
1180
function addBasicContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, menuId: MenuId, arg: unknown, updateContextKeys?: () => Promise<void>): IDisposable {
1181
const contextMenuService = accessor.get(IContextMenuService);
1182
const menuService = accessor.get(IMenuService);
1183
1184
return dom.addDisposableListener(widget, dom.EventType.CONTEXT_MENU, async domEvent => {
1185
const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
1186
dom.EventHelper.stop(domEvent, true);
1187
1188
try {
1189
await updateContextKeys?.();
1190
} catch (e) {
1191
console.error(e);
1192
}
1193
1194
contextMenuService.showContextMenu({
1195
contextKeyService: scopedContextKeyService,
1196
getAnchor: () => event,
1197
getActions: () => {
1198
const menu = menuService.getMenuActions(menuId, scopedContextKeyService, { arg });
1199
return getFlatContextMenuActions(menu);
1200
},
1201
});
1202
});
1203
}
1204
1205
export const chatAttachmentResourceContextKey = new RawContextKey<string>('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") });
1206
1207