Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../../base/browser/dom.js';6import { $ } from '../../../../../base/browser/dom.js';7import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';8import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';9import { Button } from '../../../../../base/browser/ui/button/button.js';10import { HoverStyle, IDelayedHoverOptions, type IHoverLifecycleOptions, type IHoverOptions } from '../../../../../base/browser/ui/hover/hover.js';11import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';12import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';13import { Codicon } from '../../../../../base/common/codicons.js';14import * as event from '../../../../../base/common/event.js';15import { MarkdownString } from '../../../../../base/common/htmlContent.js';16import { Iterable } from '../../../../../base/common/iterator.js';17import { KeyCode } from '../../../../../base/common/keyCodes.js';18import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';19import { Schemas } from '../../../../../base/common/network.js';20import { basename, dirname } from '../../../../../base/common/path.js';21import { ThemeIcon } from '../../../../../base/common/themables.js';22import { URI } from '../../../../../base/common/uri.js';23import { IRange } from '../../../../../editor/common/core/range.js';24import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';25import { LanguageFeatureRegistry } from '../../../../../editor/common/languageFeatureRegistry.js';26import { Location, SymbolKind } from '../../../../../editor/common/languages.js';27import { ILanguageService } from '../../../../../editor/common/languages/language.js';28import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';29import { IModelService } from '../../../../../editor/common/services/model.js';30import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';31import { localize } from '../../../../../nls.js';32import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';33import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';34import { ICommandService } from '../../../../../platform/commands/common/commands.js';35import { IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';36import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';37import { fillInSymbolsDragData } from '../../../../../platform/dnd/browser/dnd.js';38import { IOpenEditorOptions, registerOpenEditorListeners } from '../../../../../platform/editor/browser/editor.js';39import { ITextEditorOptions } from '../../../../../platform/editor/common/editor.js';40import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';41import { IHoverService } from '../../../../../platform/hover/browser/hover.js';42import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';43import { ILabelService } from '../../../../../platform/label/common/label.js';44import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';45import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js';46import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js';47import { fillEditorsDragData } from '../../../../browser/dnd.js';48import { IFileLabelOptions, IResourceLabel, ResourceLabels } from '../../../../browser/labels.js';49import { ResourceContextKey } from '../../../../common/contextkeys.js';50import { IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';51import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';52import { revealInSideBarCommand } from '../../../files/browser/fileActions.contribution.js';53import { CellUri } from '../../../notebook/common/notebookCommon.js';54import { INotebookService } from '../../../notebook/common/notebookService.js';55import { toHistoryItemHoverContent } from '../../../scm/browser/scmHistory.js';56import { getHistoryItemEditorTitle } from '../../../scm/browser/util.js';57import { ITerminalService } from '../../../terminal/browser/terminal.js';58import { IChatContentReference } from '../../common/chatService/chatService.js';59import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry } from '../../common/attachments/chatVariableEntries.js';60import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js';61import { ILanguageModelToolsService, ToolSet } from '../../common/tools/languageModelToolsService.js';62import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';6364const commonHoverOptions: Partial<IHoverOptions> = {65style: HoverStyle.Pointer,66position: {67hoverPosition: HoverPosition.BELOW68},69trapFocus: true,70};71const commonHoverLifecycleOptions: IHoverLifecycleOptions = {72groupId: 'chat-attachments',73};7475abstract class AbstractChatAttachmentWidget extends Disposable {76public readonly element: HTMLElement;77public readonly label: IResourceLabel;7879private readonly _onDidDelete: event.Emitter<Event> = this._register(new event.Emitter<Event>());80get onDidDelete(): event.Event<Event> {81return this._onDidDelete.event;82}8384private readonly _onDidOpen: event.Emitter<void> = this._register(new event.Emitter<void>());85get onDidOpen(): event.Event<void> {86return this._onDidOpen.event;87}8889constructor(90protected readonly attachment: IChatRequestVariableEntry,91private readonly options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },92container: HTMLElement,93contextResourceLabels: ResourceLabels,94protected readonly currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,95@ICommandService protected readonly commandService: ICommandService,96@IOpenerService protected readonly openerService: IOpenerService,97@ITerminalService protected readonly terminalService?: ITerminalService,98) {99super();100this.element = dom.append(container, $('.chat-attached-context-attachment.show-file-icons'));101this.attachClearButton();102this.label = contextResourceLabels.create(this.element, { supportIcons: true, hoverTargetOverride: this.element });103this._register(this.label);104this.element.tabIndex = 0;105this.element.role = 'button';106107// Add middle-click support for removal108this._register(dom.addDisposableListener(this.element, dom.EventType.AUXCLICK, (e: MouseEvent) => {109if (e.button === 1 /* Middle Button */ && this.options.supportsDeletion && !this.attachment.range) {110e.preventDefault();111e.stopPropagation();112this._onDidDelete.fire(e);113}114}));115}116117protected modelSupportsVision() {118return modelSupportsVision(this.currentLanguageModel);119}120121protected attachClearButton() {122123if (this.attachment.range || !this.options.supportsDeletion) {124// no clear button for attachments with ranges because range means125// referenced from prompt126return;127}128129const clearButton = new Button(this.element, {130supportIcons: true,131hoverDelegate: createInstantHoverDelegate(),132title: localize('chat.attachment.clearButton', "Remove from context")133});134clearButton.element.tabIndex = -1;135clearButton.icon = Codicon.close;136this._register(clearButton);137this._register(event.Event.once(clearButton.onDidClick)((e) => {138this._onDidDelete.fire(e);139}));140this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => {141if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) {142this._onDidDelete.fire(e.browserEvent);143}144}));145}146147protected addResourceOpenHandlers(resource: URI, range: IRange | undefined): void {148this.element.style.cursor = 'pointer';149150this._register(registerOpenEditorListeners(this.element, async options => {151if (this.attachment.kind === 'directory') {152await this.openResource(resource, options, true);153} else {154await this.openResource(resource, options, false, range);155}156}));157}158159protected async openResource(resource: URI, options: Partial<IOpenEditorOptions>, isDirectory: true): Promise<void>;160protected async openResource(resource: URI, options: Partial<IOpenEditorOptions>, isDirectory: false, range: IRange | undefined): Promise<void>;161protected async openResource(resource: URI, openOptions: Partial<IOpenEditorOptions>, isDirectory?: boolean, range?: IRange): Promise<void> {162if (isDirectory) {163// Reveal Directory in explorer164this.commandService.executeCommand(revealInSideBarCommand.id, resource);165return;166}167168if (resource.scheme === Schemas.vscodeTerminal) {169this.terminalService?.openResource(resource);170return;171}172173// Open file in editor174const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;175const options: OpenInternalOptions = {176fromUserGesture: true,177openToSide: openOptions.openToSide,178editorOptions: {179...openTextEditorOptions,180...openOptions.editorOptions181},182};183184await this.openerService.open(resource, options);185this._onDidOpen.fire();186this.element.focus();187}188}189190function modelSupportsVision(currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined) {191return currentLanguageModel?.metadata.capabilities?.vision ?? false;192}193194export class FileAttachmentWidget extends AbstractChatAttachmentWidget {195196constructor(197resource: URI,198range: IRange | undefined,199attachment: IChatRequestVariableEntry,200correspondingContentReference: IChatContentReference | undefined,201currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,202options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },203container: HTMLElement,204contextResourceLabels: ResourceLabels,205@ICommandService commandService: ICommandService,206@IOpenerService openerService: IOpenerService,207@IThemeService private readonly themeService: IThemeService,208@IHoverService private readonly hoverService: IHoverService,209@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,210@IInstantiationService private readonly instantiationService: IInstantiationService,211) {212super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);213214const fileBasename = basename(resource.path);215const fileDirname = dirname(resource.path);216const friendlyName = `${fileBasename} ${fileDirname}`;217let 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);218219if (attachment.omittedState === OmittedState.Full) {220ariaLabel = localize('chat.omittedFileAttachment', "Omitted this file: {0}", attachment.name);221this.renderOmittedWarning(friendlyName, ariaLabel);222} else {223const fileOptions: IFileLabelOptions = { hidePath: true, title: correspondingContentReference?.options?.status?.description };224this.label.setFile(resource, attachment.kind === 'file' ? {225...fileOptions,226fileKind: FileKind.FILE,227range,228} : {229...fileOptions,230fileKind: FileKind.FOLDER,231icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined232});233}234235this.element.ariaLabel = ariaLabel;236237this.instantiationService.invokeFunction(accessor => {238this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));239});240this.addResourceOpenHandlers(resource, range);241}242243private renderOmittedWarning(friendlyName: string, ariaLabel: string) {244const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-warning'));245const textLabel = dom.$('span.chat-attached-context-custom-text', {}, friendlyName);246this.element.appendChild(pillIcon);247this.element.appendChild(textLabel);248249const hoverElement = dom.$('div.chat-attached-context-hover');250hoverElement.setAttribute('aria-label', ariaLabel);251this.element.classList.add('warning');252253hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this file type.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name : this.currentLanguageModel ?? 'This model');254this._register(this.hoverService.setupDelayedHover(this.element, {255...commonHoverOptions,256content: hoverElement,257}, commonHoverLifecycleOptions));258}259}260261262export class TerminalCommandAttachmentWidget extends AbstractChatAttachmentWidget {263264constructor(265attachment: ITerminalVariableEntry,266currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,267options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },268container: HTMLElement,269contextResourceLabels: ResourceLabels,270@ICommandService commandService: ICommandService,271@IOpenerService openerService: IOpenerService,272@IHoverService private readonly hoverService: IHoverService,273@ITerminalService protected override readonly terminalService: ITerminalService,274) {275super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, terminalService);276277const ariaLabel = localize('chat.terminalCommand', "Terminal command, {0}", attachment.command);278const clickHandler = () => this.openResource(attachment.resource, { editorOptions: { preserveFocus: true } }, false, undefined);279280this._register(createTerminalCommandElements(this.element, attachment, ariaLabel, this.hoverService, clickHandler));281282this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {283const event = new StandardKeyboardEvent(e);284if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {285dom.EventHelper.stop(e, true);286await clickHandler();287}288}));289}290}291292const enum TerminalConstants {293MaxAttachmentOutputLineCount = 5,294MaxAttachmentOutputLineLength = 80,295}296297function createTerminalCommandElements(298element: HTMLElement,299attachment: ITerminalVariableEntry,300ariaLabel: string,301hoverService: IHoverService,302clickHandler: () => Promise<void>303): IDisposable {304const disposable = new DisposableStore();305element.ariaLabel = ariaLabel;306element.style.cursor = 'pointer';307308const terminalIconSpan = dom.$('span');309terminalIconSpan.classList.add(...ThemeIcon.asClassNameArray(Codicon.terminal));310const pillIcon = dom.$('div.chat-attached-context-pill', {}, terminalIconSpan);311const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.command);312element.appendChild(pillIcon);313element.appendChild(textLabel);314315disposable.add(dom.addDisposableListener(element, dom.EventType.CLICK, e => {316e.preventDefault();317e.stopPropagation();318clickHandler();319}));320321disposable.add(hoverService.setupDelayedHover(element, () => getHoverContent(ariaLabel, attachment), commonHoverLifecycleOptions));322return disposable;323}324325function getHoverContent(ariaLabel: string, attachment: ITerminalVariableEntry): IDelayedHoverOptions {326{327const hoverElement = dom.$('div.chat-attached-context-hover');328hoverElement.setAttribute('aria-label', ariaLabel);329330const commandTitle = dom.$('div', {}, typeof attachment.exitCode === 'number'331? localize('chat.terminalCommandHoverCommandTitleExit', "Command: {0}, exit code: {1}", attachment.command, attachment.exitCode)332: localize('chat.terminalCommandHoverCommandTitle', "Command"));333commandTitle.classList.add('attachment-additional-info');334const commandBlock = dom.$('pre.chat-terminal-command-block');335hoverElement.append(commandTitle, commandBlock);336337if (attachment.output && attachment.output.trim().length > 0) {338const outputTitle = dom.$('div', {}, localize('chat.terminalCommandHoverOutputTitle', "Output:"));339outputTitle.classList.add('attachment-additional-info');340const outputBlock = dom.$('pre.chat-terminal-command-output');341const fullOutputLines = attachment.output.split('\n');342const hoverOutputLines = [];343for (const line of fullOutputLines) {344if (hoverOutputLines.length >= TerminalConstants.MaxAttachmentOutputLineCount) {345hoverOutputLines.push('...');346break;347}348const trimmed = line.trim();349if (trimmed.length === 0) {350continue;351}352if (trimmed.length > TerminalConstants.MaxAttachmentOutputLineLength) {353hoverOutputLines.push(`${trimmed.slice(0, TerminalConstants.MaxAttachmentOutputLineLength)}...`);354} else {355hoverOutputLines.push(trimmed);356}357}358outputBlock.textContent = hoverOutputLines.join('\n');359hoverElement.append(outputTitle, outputBlock);360}361362return {363...commonHoverOptions,364content: hoverElement,365};366}367}368369export class ImageAttachmentWidget extends AbstractChatAttachmentWidget {370371constructor(372resource: URI | undefined,373attachment: IChatRequestVariableEntry,374currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,375options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },376container: HTMLElement,377contextResourceLabels: ResourceLabels,378@ICommandService commandService: ICommandService,379@IOpenerService openerService: IOpenerService,380@IHoverService private readonly hoverService: IHoverService,381@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,382@IInstantiationService instantiationService: IInstantiationService,383@ILabelService private readonly labelService: ILabelService,384) {385super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);386387let ariaLabel: string;388if (attachment.omittedState === OmittedState.Full) {389ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name);390} else if (attachment.omittedState === OmittedState.Partial) {391ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name);392} else {393ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name);394}395396const ref = attachment.references?.[0]?.reference;397resource = ref && URI.isUri(ref) ? ref : undefined;398const clickHandler = async () => {399if (resource) {400await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined);401}402};403404const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : 'Current model';405406const fullName = resource ? this.labelService.getUriLabel(resource) : (attachment.fullName || attachment.name);407this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState));408409if (resource) {410this.addResourceOpenHandlers(resource, undefined);411instantiationService.invokeFunction(accessor => {412this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));413});414}415}416}417418function createImageElements(resource: URI | undefined, name: string, fullName: string,419element: HTMLElement,420buffer: ArrayBuffer | Uint8Array,421hoverService: IHoverService, ariaLabel: string,422currentLanguageModelName: string | undefined,423clickHandler: () => void,424currentLanguageModel?: ILanguageModelChatMetadataAndIdentifier,425omittedState?: OmittedState): IDisposable {426427const disposable = new DisposableStore();428if (omittedState === OmittedState.Partial) {429element.classList.add('partial-warning');430}431432element.ariaLabel = ariaLabel;433element.style.position = 'relative';434435if (resource) {436element.style.cursor = 'pointer';437disposable.add(dom.addDisposableListener(element, 'click', clickHandler));438}439const supportsVision = modelSupportsVision(currentLanguageModel);440const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning'));441const textLabel = dom.$('span.chat-attached-context-custom-text', {}, name);442element.appendChild(pillIcon);443element.appendChild(textLabel);444445const hoverElement = dom.$('div.chat-attached-context-hover');446hoverElement.setAttribute('aria-label', ariaLabel);447448if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) {449element.classList.add('warning');450hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", currentLanguageModelName ?? 'This model');451disposable.add(hoverService.setupDelayedHover(element, {452content: hoverElement,453style: HoverStyle.Pointer,454}));455} else {456disposable.add(hoverService.setupDelayedHover(element, {457content: hoverElement,458style: HoverStyle.Pointer,459}));460461const blob = new Blob([buffer as Uint8Array<ArrayBuffer>], { type: 'image/png' });462const url = URL.createObjectURL(blob);463const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' });464const pill = dom.$('div.chat-attached-context-pill', {}, pillImg);465466// eslint-disable-next-line no-restricted-syntax467const existingPill = element.querySelector('.chat-attached-context-pill');468if (existingPill) {469existingPill.replaceWith(pill);470}471472const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' });473const imageContainer = dom.$('div.chat-attached-context-image-container', {}, hoverImage);474hoverElement.appendChild(imageContainer);475476if (resource) {477const urlContainer = dom.$('a.chat-attached-context-url', {}, omittedState === OmittedState.Partial ? localize('chat.imageAttachmentWarning', "This GIF was partially omitted - current frame will be sent.") : fullName);478const separator = dom.$('div.chat-attached-context-url-separator');479disposable.add(dom.addDisposableListener(urlContainer, 'click', () => clickHandler()));480hoverElement.append(separator, urlContainer);481}482483hoverImage.onload = () => { URL.revokeObjectURL(url); };484hoverImage.onerror = () => {485// reset to original icon on error or invalid image486const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media'));487const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon);488// eslint-disable-next-line no-restricted-syntax489const existingPill = element.querySelector('.chat-attached-context-pill');490if (existingPill) {491existingPill.replaceWith(pill);492}493};494}495return disposable;496}497498export class PasteAttachmentWidget extends AbstractChatAttachmentWidget {499500constructor(501attachment: IChatRequestPasteVariableEntry,502currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,503options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },504container: HTMLElement,505contextResourceLabels: ResourceLabels,506@ICommandService commandService: ICommandService,507@IOpenerService openerService: IOpenerService,508@IHoverService private readonly hoverService: IHoverService,509@IInstantiationService private readonly instantiationService: IInstantiationService,510) {511super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);512513const ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);514this.element.ariaLabel = ariaLabel;515516const classNames = ['file-icon', `${attachment.language}-lang-file-icon`];517let resource: URI | undefined;518let range: IRange | undefined;519520if (attachment.copiedFrom) {521resource = attachment.copiedFrom.uri;522range = attachment.copiedFrom.range;523const filename = basename(resource.path);524this.label.setLabel(filename, undefined, { extraClasses: classNames });525} else {526this.label.setLabel(attachment.fileName, undefined, { extraClasses: classNames });527}528this.element.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`));529530this.element.style.position = 'relative';531532const sourceUri = attachment.copiedFrom?.uri;533const 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\`\`\``);534this._register(this.hoverService.setupDelayedHover(this.element, {535...commonHoverOptions,536content: hoverContent,537}, commonHoverLifecycleOptions));538539const copiedFromResource = attachment.copiedFrom?.uri;540if (copiedFromResource) {541this._register(this.instantiationService.invokeFunction(hookUpResourceAttachmentDragAndContextMenu, this.element, copiedFromResource));542this.addResourceOpenHandlers(copiedFromResource, range);543}544}545}546547export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {548constructor(549resource: URI | undefined,550range: IRange | undefined,551attachment: IChatRequestVariableEntry,552correspondingContentReference: IChatContentReference | undefined,553currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,554options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },555container: HTMLElement,556contextResourceLabels: ResourceLabels,557@ICommandService commandService: ICommandService,558@IOpenerService openerService: IOpenerService,559@IContextKeyService private readonly contextKeyService: IContextKeyService,560@IInstantiationService private readonly instantiationService: IInstantiationService,561) {562super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);563564const attachmentLabel = attachment.fullName ?? attachment.name;565const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;566this.label.setLabel(withIcon, correspondingContentReference?.options?.status?.description);567this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);568569if (attachment.kind === 'diagnostic') {570if (attachment.filterUri) {571resource = attachment.filterUri ? URI.revive(attachment.filterUri) : undefined;572range = attachment.filterRange;573} else {574this.element.style.cursor = 'pointer';575this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => {576this.commandService.executeCommand('workbench.panel.markers.view.focus');577}));578}579}580581if (attachment.kind === 'symbol') {582const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));583this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext));584}585586if (resource) {587this.addResourceOpenHandlers(resource, range);588}589}590}591592export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {593594private hintElement: HTMLElement;595596constructor(597attachment: IPromptFileVariableEntry,598currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,599options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },600container: HTMLElement,601contextResourceLabels: ResourceLabels,602@ICommandService commandService: ICommandService,603@IOpenerService openerService: IOpenerService,604@ILabelService private readonly labelService: ILabelService,605@IInstantiationService private readonly instantiationService: IInstantiationService,606) {607super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);608609610this.hintElement = dom.append(this.element, dom.$('span.prompt-type'));611612this.updateLabel(attachment);613614this.instantiationService.invokeFunction(accessor => {615this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, attachment.value));616});617this.addResourceOpenHandlers(attachment.value, undefined);618}619620private updateLabel(attachment: IPromptFileVariableEntry) {621const resource = attachment.value;622const fileBasename = basename(resource.path);623const fileDirname = dirname(resource.path);624const friendlyName = `${fileBasename} ${fileDirname}`;625const isPrompt = attachment.id.startsWith(PromptFileVariableKind.PromptFile);626const ariaLabel = isPrompt627? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName)628: localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName);629const typeLabel = isPrompt630? localize('prompt', "Prompt")631: localize('instructions', "Instructions");632633const title = this.labelService.getUriLabel(resource) + (attachment.originLabel ? `\n${attachment.originLabel}` : '');634635//const { topError } = this.promptFile;636this.element.classList.remove('warning', 'error');637638// if there are some errors/warning during the process of resolving639// attachment references (including all the nested child references),640// add the issue details in the hover title for the attachment, one641// error/warning at a time because there is a limited space available642// if (topError) {643// const { errorSubject: subject } = topError;644// const isError = (subject === 'root');645// this.element.classList.add((isError) ? 'error' : 'warning');646647// const severity = (isError)648// ? localize('error', "Error")649// : localize('warning', "Warning");650651// title += `\n[${severity}]: ${topError.localizedMessage}`;652// }653654const fileWithoutExtension = getCleanPromptName(resource);655this.label.setFile(URI.file(fileWithoutExtension), {656fileKind: FileKind.FILE,657hidePath: true,658range: undefined,659title,660icon: ThemeIcon.fromId(Codicon.bookmark.id),661extraClasses: [],662});663664this.hintElement.innerText = typeLabel;665666667this.element.ariaLabel = ariaLabel;668}669}670671export class PromptTextAttachmentWidget extends AbstractChatAttachmentWidget {672673constructor(674attachment: IPromptTextVariableEntry,675currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,676options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },677container: HTMLElement,678contextResourceLabels: ResourceLabels,679@ICommandService commandService: ICommandService,680@IOpenerService openerService: IOpenerService,681@IPreferencesService preferencesService: IPreferencesService,682@IHoverService hoverService: IHoverService683) {684super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);685686if (attachment.settingId) {687const openSettings = () => preferencesService.openSettings({ jsonEditor: false, query: `@id:${attachment.settingId}` });688689this.element.style.cursor = 'pointer';690this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async (e: MouseEvent) => {691dom.EventHelper.stop(e, true);692openSettings();693}));694695this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {696const event = new StandardKeyboardEvent(e);697if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {698dom.EventHelper.stop(e, true);699openSettings();700}701}));702}703this.label.setLabel(localize('instructions.label', 'Additional Instructions'), undefined, undefined);704705this._register(hoverService.setupDelayedHover(this.element, {706...commonHoverOptions,707content: attachment.value,708}, commonHoverLifecycleOptions));709}710}711712713export class ToolSetOrToolItemAttachmentWidget extends AbstractChatAttachmentWidget {714constructor(715attachment: ChatRequestToolReferenceEntry,716currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,717options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },718container: HTMLElement,719contextResourceLabels: ResourceLabels,720@ILanguageModelToolsService toolsService: ILanguageModelToolsService,721@ICommandService commandService: ICommandService,722@IOpenerService openerService: IOpenerService,723@IHoverService hoverService: IHoverService724) {725super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);726727728const toolOrToolSet = Iterable.find(toolsService.getTools(), tool => tool.id === attachment.id) ?? Iterable.find(toolsService.toolSets.get(), toolSet => toolSet.id === attachment.id);729730let name = attachment.name;731const icon = attachment.icon ?? Codicon.tools;732733if (toolOrToolSet instanceof ToolSet) {734name = toolOrToolSet.referenceName;735} else if (toolOrToolSet) {736name = toolOrToolSet.toolReferenceName ?? name;737}738739this.label.setLabel(`$(${icon.id})\u00A0${name}`, undefined);740741this.element.style.cursor = 'pointer';742this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", name);743744let hoverContent: string | undefined;745746if (toolOrToolSet instanceof ToolSet) {747hoverContent = localize('toolset', "{0} - {1}", toolOrToolSet.description ?? toolOrToolSet.referenceName, toolOrToolSet.source.label);748} else if (toolOrToolSet) {749hoverContent = localize('tool', "{0} - {1}", toolOrToolSet.userDescription ?? toolOrToolSet.modelDescription, toolOrToolSet.source.label);750}751752if (hoverContent) {753this._register(hoverService.setupDelayedHover(this.element, {754...commonHoverOptions,755content: hoverContent,756}, commonHoverLifecycleOptions));757}758}759760761}762763export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachmentWidget {764constructor(765resource: URI,766attachment: INotebookOutputVariableEntry,767currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,768options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },769container: HTMLElement,770contextResourceLabels: ResourceLabels,771@ICommandService commandService: ICommandService,772@IOpenerService openerService: IOpenerService,773@IHoverService private readonly hoverService: IHoverService,774@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,775@INotebookService private readonly notebookService: INotebookService,776@IInstantiationService private readonly instantiationService: IInstantiationService,777) {778super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);779780switch (attachment.mimeType) {781case 'application/vnd.code.notebook.error': {782this.renderErrorOutput(resource, attachment);783break;784}785case 'image/png':786case 'image/jpeg':787case 'image/svg': {788this.renderImageOutput(resource, attachment);789break;790}791default: {792this.renderGenericOutput(resource, attachment);793}794}795796this.instantiationService.invokeFunction(accessor => {797this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));798});799this.addResourceOpenHandlers(resource, undefined);800}801getAriaLabel(attachment: INotebookOutputVariableEntry): string {802return localize('chat.NotebookImageAttachment', "Attached Notebook output, {0}", attachment.name);803}804private renderErrorOutput(resource: URI, attachment: INotebookOutputVariableEntry) {805const attachmentLabel = attachment.name;806const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;807const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();808let title: string | undefined = undefined;809try {810const error = JSON.parse(new TextDecoder().decode(buffer)) as Error;811if (error.name && error.message) {812title = `${error.name}: ${error.message}`;813}814} catch {815//816}817this.label.setLabel(withIcon, undefined, { title });818this.element.ariaLabel = this.getAriaLabel(attachment);819}820private renderGenericOutput(resource: URI, attachment: INotebookOutputVariableEntry) {821this.element.ariaLabel = this.getAriaLabel(attachment);822this.label.setFile(resource, { hidePath: true, icon: ThemeIcon.fromId('output') });823}824private renderImageOutput(resource: URI, attachment: INotebookOutputVariableEntry) {825let ariaLabel: string;826if (attachment.omittedState === OmittedState.Full) {827ariaLabel = localize('chat.omittedNotebookImageAttachment', "Omitted this Notebook ouput: {0}", attachment.name);828} else if (attachment.omittedState === OmittedState.Partial) {829ariaLabel = localize('chat.partiallyOmittedNotebookImageAttachment', "Partially omitted this Notebook output: {0}", attachment.name);830} else {831ariaLabel = this.getAriaLabel(attachment);832}833834const clickHandler = async () => await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined);835const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : undefined;836const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();837this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState));838}839840private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) {841const parsedInfo = CellUri.parseCellOutputUri(resource);842if (!parsedInfo || typeof parsedInfo.cellHandle !== 'number' || typeof parsedInfo.outputIndex !== 'number') {843return undefined;844}845const notebook = this.notebookService.getNotebookTextModel(parsedInfo.notebook);846if (!notebook) {847return undefined;848}849const cell = notebook.cells.find(c => c.handle === parsedInfo.cellHandle);850if (!cell) {851return undefined;852}853const output = cell.outputs.length > parsedInfo.outputIndex ? cell.outputs[parsedInfo.outputIndex] : undefined;854return output?.outputs.find(o => o.mime === attachment.mimeType);855}856857}858859export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget {860constructor(861attachment: IElementVariableEntry,862currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,863options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },864container: HTMLElement,865contextResourceLabels: ResourceLabels,866@ICommandService commandService: ICommandService,867@IOpenerService openerService: IOpenerService,868@IEditorService editorService: IEditorService,869) {870super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);871872const ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name);873this.element.ariaLabel = ariaLabel;874875this.element.style.position = 'relative';876this.element.style.cursor = 'pointer';877const attachmentLabel = attachment.name;878const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;879this.label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) });880881this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {882const content = attachment.value?.toString() || '';883await editorService.openEditor({884resource: undefined,885contents: content,886options: {887pinned: true888}889});890}));891}892}893894export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget {895constructor(896attachment: ISCMHistoryItemVariableEntry,897currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,898options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },899container: HTMLElement,900contextResourceLabels: ResourceLabels,901@ICommandService commandService: ICommandService,902@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,903@IHoverService hoverService: IHoverService,904@IOpenerService openerService: IOpenerService,905@IThemeService themeService: IThemeService906) {907super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);908909this.label.setLabel(attachment.name, undefined);910911this.element.style.cursor = 'pointer';912this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);913914const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);915this._store.add(hoverService.setupDelayedHover(this.element, {916...commonHoverOptions,917content,918}, commonHoverLifecycleOptions));919this._store.add(disposables);920921this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => {922dom.EventHelper.stop(e, true);923this._openAttachment(attachment);924}));925926this._store.add(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {927const event = new StandardKeyboardEvent(e);928if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {929dom.EventHelper.stop(e, true);930this._openAttachment(attachment);931}932}));933}934935private async _openAttachment(attachment: ISCMHistoryItemVariableEntry): Promise<void> {936await this.commandService.executeCommand('_workbench.openMultiDiffEditor', {937title: getHistoryItemEditorTitle(attachment.historyItem), multiDiffSourceUri: attachment.value938});939}940}941942export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachmentWidget {943constructor(944attachment: ISCMHistoryItemChangeVariableEntry,945currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,946options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },947container: HTMLElement,948contextResourceLabels: ResourceLabels,949@ICommandService commandService: ICommandService,950@IHoverService hoverService: IHoverService,951@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,952@IOpenerService openerService: IOpenerService,953@IThemeService themeService: IThemeService,954@IEditorService private readonly editorService: IEditorService,955) {956super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);957958const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${attachment.historyItem.displayId ?? attachment.historyItem.id}`;959this.label.setFile(attachment.value, { fileKind: FileKind.FILE, hidePath: true, nameSuffix });960961this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);962963const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);964this._store.add(hoverService.setupDelayedHover(this.element, {965...commonHoverOptions, content,966}, commonHoverLifecycleOptions));967this._store.add(disposables);968969this.addResourceOpenHandlers(attachment.value, undefined);970}971972protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: true): Promise<void>;973protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: false, range: IRange | undefined): Promise<void>;974protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory?: boolean, range?: IRange): Promise<void> {975const attachment = this.attachment as ISCMHistoryItemChangeVariableEntry;976const historyItem = attachment.historyItem;977978await this.editorService.openEditor({979resource,980label: `${basename(resource.path)} (${historyItem.displayId ?? historyItem.id})`,981options: { ...options.editorOptions }982}, options.openToSide ? SIDE_GROUP : undefined);983}984}985986export class SCMHistoryItemChangeRangeAttachmentWidget extends AbstractChatAttachmentWidget {987constructor(988attachment: ISCMHistoryItemChangeRangeVariableEntry,989currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,990options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },991container: HTMLElement,992contextResourceLabels: ResourceLabels,993@ICommandService commandService: ICommandService,994@IOpenerService openerService: IOpenerService,995@IEditorService private readonly editorService: IEditorService,996) {997super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService);998999const historyItemStartId = attachment.historyItemChangeStart.historyItem.displayId ?? attachment.historyItemChangeStart.historyItem.id;1000const historyItemEndId = attachment.historyItemChangeEnd.historyItem.displayId ?? attachment.historyItemChangeEnd.historyItem.id;10011002const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${historyItemStartId}..${historyItemEndId}`;1003this.label.setFile(attachment.value, { fileKind: FileKind.FILE, hidePath: true, nameSuffix });10041005this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);10061007this.addResourceOpenHandlers(attachment.value, undefined);1008}10091010protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: true): Promise<void>;1011protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: false, range: IRange | undefined): Promise<void>;1012protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory?: boolean, range?: IRange): Promise<void> {1013const attachment = this.attachment as ISCMHistoryItemChangeRangeVariableEntry;1014const historyItemChangeStart = attachment.historyItemChangeStart;1015const historyItemChangeEnd = attachment.historyItemChangeEnd;10161017const originalUriTitle = `${basename(historyItemChangeStart.uri.fsPath)} (${historyItemChangeStart.historyItem.displayId ?? historyItemChangeStart.historyItem.id})`;1018const modifiedUriTitle = `${basename(historyItemChangeEnd.uri.fsPath)} (${historyItemChangeEnd.historyItem.displayId ?? historyItemChangeEnd.historyItem.id})`;10191020await this.editorService.openEditor({1021original: { resource: historyItemChangeStart.uri },1022modified: { resource: historyItemChangeEnd.uri },1023label: `${originalUriTitle} ↔ ${modifiedUriTitle}`,1024options: { ...options.editorOptions }1025}, options.openToSide ? SIDE_GROUP : undefined);1026}1027}10281029export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, resource: URI): IDisposable {1030const contextKeyService = accessor.get(IContextKeyService);1031const instantiationService = accessor.get(IInstantiationService);10321033const store = new DisposableStore();10341035// Context1036const scopedContextKeyService = store.add(contextKeyService.createScoped(widget));1037store.add(setResourceContext(accessor, scopedContextKeyService, resource));10381039// Drag and drop1040widget.draggable = true;1041store.add(dom.addDisposableListener(widget, 'dragstart', e => {1042instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [resource], e));1043e.dataTransfer?.setDragImage(widget, 0, 0);1044}));10451046// Context menu1047store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, MenuId.ChatInputResourceAttachmentContext, resource));10481049return store;1050}10511052export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable {1053const instantiationService = accessor.get(IInstantiationService);1054const languageFeaturesService = accessor.get(ILanguageFeaturesService);1055const textModelService = accessor.get(ITextModelService);10561057const store = new DisposableStore();10581059// Context1060store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri));10611062const chatResourceContext = chatAttachmentResourceContextKey.bindTo(scopedContextKeyService);1063chatResourceContext.set(attachment.value.uri.toString());10641065// Drag and drop1066widget.draggable = true;1067store.add(dom.addDisposableListener(widget, 'dragstart', e => {1068instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [{ resource: attachment.value.uri, selection: attachment.value.range }], e));10691070fillInSymbolsDragData([{1071fsPath: attachment.value.uri.fsPath,1072range: attachment.value.range,1073name: attachment.name,1074kind: attachment.kind,1075}], e);10761077e.dataTransfer?.setDragImage(widget, 0, 0);1078}));10791080// Context menu1081const providerContexts: ReadonlyArray<[IContextKey<boolean>, LanguageFeatureRegistry<unknown>]> = [1082[EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider],1083[EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider],1084[EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider],1085[EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider],1086];10871088const updateContextKeys = async () => {1089const modelRef = await textModelService.createModelReference(attachment.value.uri);1090try {1091const model = modelRef.object.textEditorModel;1092for (const [contextKey, registry] of providerContexts) {1093contextKey.set(registry.has(model));1094}1095} finally {1096modelRef.dispose();1097}1098};1099store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, contextMenuId, attachment.value, updateContextKeys));11001101return store;1102}11031104function setResourceContext(accessor: ServicesAccessor, scopedContextKeyService: IScopedContextKeyService, resource: URI) {1105const fileService = accessor.get(IFileService);1106const languageService = accessor.get(ILanguageService);1107const modelService = accessor.get(IModelService);11081109const resourceContextKey = new ResourceContextKey(scopedContextKeyService, fileService, languageService, modelService);1110resourceContextKey.set(resource);1111return resourceContextKey;1112}11131114function addBasicContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, menuId: MenuId, arg: unknown, updateContextKeys?: () => Promise<void>): IDisposable {1115const contextMenuService = accessor.get(IContextMenuService);1116const menuService = accessor.get(IMenuService);11171118return dom.addDisposableListener(widget, dom.EventType.CONTEXT_MENU, async domEvent => {1119const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);1120dom.EventHelper.stop(domEvent, true);11211122try {1123await updateContextKeys?.();1124} catch (e) {1125console.error(e);1126}11271128contextMenuService.showContextMenu({1129contextKeyService: scopedContextKeyService,1130getAnchor: () => event,1131getActions: () => {1132const menu = menuService.getMenuActions(menuId, scopedContextKeyService, { arg });1133return getFlatContextMenuActions(menu);1134},1135});1136});1137}11381139export const chatAttachmentResourceContextKey = new RawContextKey<string>('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") });114011411142