Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts
5297 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 { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';16import { Iterable } from '../../../../../base/common/iterator.js';17import { KeyCode } from '../../../../../base/common/keyCodes.js';18import { Disposable, DisposableStore, IDisposable, MutableDisposable } 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 { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';30import { IModelService } from '../../../../../editor/common/services/model.js';31import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';32import { localize } from '../../../../../nls.js';33import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';34import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';35import { ICommandService } from '../../../../../platform/commands/common/commands.js';36import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';37import { IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';38import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';39import { fillInSymbolsDragData } from '../../../../../platform/dnd/browser/dnd.js';40import { IOpenEditorOptions, registerOpenEditorListeners } from '../../../../../platform/editor/browser/editor.js';41import { ITextEditorOptions } from '../../../../../platform/editor/common/editor.js';42import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';43import { IHoverService } from '../../../../../platform/hover/browser/hover.js';44import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';45import { ILabelService } from '../../../../../platform/label/common/label.js';46import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';47import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js';48import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js';49import { fillEditorsDragData } from '../../../../browser/dnd.js';50import { IFileLabelOptions, IResourceLabel, ResourceLabels } from '../../../../browser/labels.js';51import { ResourceContextKey } from '../../../../common/contextkeys.js';52import { IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';53import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';54import { revealInSideBarCommand } from '../../../files/browser/fileActions.contribution.js';55import { CellUri } from '../../../notebook/common/notebookCommon.js';56import { INotebookService } from '../../../notebook/common/notebookService.js';57import { toHistoryItemHoverContent } from '../../../scm/browser/scmHistory.js';58import { getHistoryItemEditorTitle } from '../../../scm/browser/util.js';59import { ITerminalService } from '../../../terminal/browser/terminal.js';60import { IChatContentReference } from '../../common/chatService/chatService.js';61import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js';62import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js';63import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';64import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js';65import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';66import { IChatContextService } from '../contextContrib/chatContextService.js';6768const commonHoverOptions: Partial<IHoverOptions> = {69style: HoverStyle.Pointer,70position: {71hoverPosition: HoverPosition.BELOW72},73trapFocus: true,74};75const commonHoverLifecycleOptions: IHoverLifecycleOptions = {76groupId: 'chat-attachments',77};7879abstract class AbstractChatAttachmentWidget extends Disposable {80public readonly element: HTMLElement;81public readonly label: IResourceLabel;8283private readonly _onDidDelete: event.Emitter<Event> = this._register(new event.Emitter<Event>());84get onDidDelete(): event.Event<Event> {85return this._onDidDelete.event;86}8788private readonly _onDidOpen: event.Emitter<void> = this._register(new event.Emitter<void>());89get onDidOpen(): event.Event<void> {90return this._onDidOpen.event;91}9293constructor(94protected readonly attachment: IChatRequestVariableEntry,95private readonly options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },96container: HTMLElement,97contextResourceLabels: ResourceLabels,98protected readonly currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,99@ICommandService protected readonly commandService: ICommandService,100@IOpenerService protected readonly openerService: IOpenerService,101@IConfigurationService protected readonly configurationService: IConfigurationService,102@ITerminalService protected readonly terminalService?: ITerminalService,103) {104super();105this.element = dom.append(container, $('.chat-attached-context-attachment.show-file-icons'));106this.attachClearButton();107this.label = contextResourceLabels.create(this.element, { supportIcons: true, hoverTargetOverride: this.element });108this._register(this.label);109this.element.tabIndex = 0;110this.element.role = 'button';111112// Add middle-click support for removal113this._register(dom.addDisposableListener(this.element, dom.EventType.AUXCLICK, (e: MouseEvent) => {114if (e.button === 1 /* Middle Button */ && this.options.supportsDeletion && !this.attachment.range) {115e.preventDefault();116e.stopPropagation();117this._onDidDelete.fire(e);118}119}));120}121122protected modelSupportsVision() {123return modelSupportsVision(this.currentLanguageModel);124}125126protected attachClearButton() {127128if (this.attachment.range || !this.options.supportsDeletion) {129// no clear button for attachments with ranges because range means130// referenced from prompt131return;132}133134const clearButton = new Button(this.element, {135supportIcons: true,136hoverDelegate: createInstantHoverDelegate(),137title: localize('chat.attachment.clearButton', "Remove from context")138});139clearButton.element.tabIndex = -1;140clearButton.icon = Codicon.close;141this._register(clearButton);142this._register(event.Event.once(clearButton.onDidClick)((e) => {143this._onDidDelete.fire(e);144}));145this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => {146if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) {147this._onDidDelete.fire(e.browserEvent);148}149}));150}151152protected addResourceOpenHandlers(resource: URI, range: IRange | undefined): void {153this.element.style.cursor = 'pointer';154155this._register(registerOpenEditorListeners(this.element, async options => {156if (this.attachment.kind === 'directory') {157await this.openResource(resource, options, true);158} else {159await this.openResource(resource, options, false, range);160}161}));162}163164protected async openResource(resource: URI, options: Partial<IOpenEditorOptions>, isDirectory: true): Promise<void>;165protected async openResource(resource: URI, options: Partial<IOpenEditorOptions>, isDirectory: false, range: IRange | undefined): Promise<void>;166protected async openResource(resource: URI, openOptions: Partial<IOpenEditorOptions>, isDirectory?: boolean, range?: IRange): Promise<void> {167if (isDirectory) {168// Reveal Directory in explorer169this.commandService.executeCommand(revealInSideBarCommand.id, resource);170return;171}172173if (resource.scheme === Schemas.vscodeTerminal) {174this.terminalService?.openResource(resource);175return;176}177178// Open file in editor179const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;180const options: OpenInternalOptions = {181fromUserGesture: true,182openToSide: openOptions.openToSide,183editorOptions: {184...openTextEditorOptions,185...openOptions.editorOptions186},187};188189await this.openerService.open(resource, options);190this._onDidOpen.fire();191this.element.focus();192}193}194195function modelSupportsVision(currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined) {196return currentLanguageModel?.metadata.capabilities?.vision ?? false;197}198199200export class FileAttachmentWidget extends AbstractChatAttachmentWidget {201202constructor(203resource: URI,204range: IRange | undefined,205attachment: IChatRequestVariableEntry,206correspondingContentReference: IChatContentReference | undefined,207currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,208options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },209container: HTMLElement,210contextResourceLabels: ResourceLabels,211@ICommandService commandService: ICommandService,212@IOpenerService openerService: IOpenerService,213@IConfigurationService configurationService: IConfigurationService,214@IThemeService private readonly themeService: IThemeService,215@IHoverService private readonly hoverService: IHoverService,216@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,217@IInstantiationService private readonly instantiationService: IInstantiationService,218) {219super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);220221const fileBasename = basename(resource.path);222const fileDirname = dirname(resource.path);223const friendlyName = `${fileBasename} ${fileDirname}`;224let 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);225226if (attachment.omittedState === OmittedState.Full) {227ariaLabel = localize('chat.omittedFileAttachment', "Omitted this file: {0}", attachment.name);228this.renderOmittedWarning(friendlyName, ariaLabel);229} else {230const fileOptions: IFileLabelOptions = { hidePath: true, title: correspondingContentReference?.options?.status?.description };231this.label.setFile(resource, attachment.kind === 'file' ? {232...fileOptions,233fileKind: FileKind.FILE,234range,235} : {236...fileOptions,237fileKind: FileKind.FOLDER,238icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined239});240}241242this.element.ariaLabel = ariaLabel;243244this.instantiationService.invokeFunction(accessor => {245this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));246});247this.addResourceOpenHandlers(resource, range);248}249250private renderOmittedWarning(friendlyName: string, ariaLabel: string) {251const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-warning'));252const textLabel = dom.$('span.chat-attached-context-custom-text', {}, friendlyName);253this.element.appendChild(pillIcon);254this.element.appendChild(textLabel);255256const hoverElement = dom.$('div.chat-attached-context-hover');257hoverElement.setAttribute('aria-label', ariaLabel);258this.element.classList.add('warning');259260hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this file type.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name : this.currentLanguageModel ?? 'This model');261this._register(this.hoverService.setupDelayedHover(this.element, {262...commonHoverOptions,263content: hoverElement,264}, commonHoverLifecycleOptions));265}266}267268269export class TerminalCommandAttachmentWidget extends AbstractChatAttachmentWidget {270271constructor(272attachment: ITerminalVariableEntry,273currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,274options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },275container: HTMLElement,276contextResourceLabels: ResourceLabels,277@ICommandService commandService: ICommandService,278@IOpenerService openerService: IOpenerService,279@IConfigurationService configurationService: IConfigurationService,280@IHoverService private readonly hoverService: IHoverService,281@ITerminalService protected override readonly terminalService: ITerminalService,282) {283super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService, terminalService);284285const ariaLabel = localize('chat.terminalCommand', "Terminal command, {0}", attachment.command);286const clickHandler = () => this.openResource(attachment.resource, { editorOptions: { preserveFocus: true } }, false, undefined);287288this._register(createTerminalCommandElements(this.element, attachment, ariaLabel, this.hoverService, clickHandler));289290this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {291const event = new StandardKeyboardEvent(e);292if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {293dom.EventHelper.stop(e, true);294await clickHandler();295}296}));297}298}299300const enum TerminalConstants {301MaxAttachmentOutputLineCount = 5,302MaxAttachmentOutputLineLength = 80,303}304305function createTerminalCommandElements(306element: HTMLElement,307attachment: ITerminalVariableEntry,308ariaLabel: string,309hoverService: IHoverService,310clickHandler: () => Promise<void>311): IDisposable {312const disposable = new DisposableStore();313element.ariaLabel = ariaLabel;314element.style.cursor = 'pointer';315316const terminalIconSpan = dom.$('span');317terminalIconSpan.classList.add(...ThemeIcon.asClassNameArray(Codicon.terminal));318const pillIcon = dom.$('div.chat-attached-context-pill', {}, terminalIconSpan);319const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.command);320element.appendChild(pillIcon);321element.appendChild(textLabel);322323disposable.add(dom.addDisposableListener(element, dom.EventType.CLICK, e => {324e.preventDefault();325e.stopPropagation();326clickHandler();327}));328329disposable.add(hoverService.setupDelayedHover(element, () => getHoverContent(ariaLabel, attachment), commonHoverLifecycleOptions));330return disposable;331}332333function getHoverContent(ariaLabel: string, attachment: ITerminalVariableEntry): IDelayedHoverOptions {334{335const hoverElement = dom.$('div.chat-attached-context-hover');336hoverElement.setAttribute('aria-label', ariaLabel);337338const commandTitle = dom.$('div', {}, typeof attachment.exitCode === 'number'339? localize('chat.terminalCommandHoverCommandTitleExit', "Command: {0}, exit code: {1}", attachment.command, attachment.exitCode)340: localize('chat.terminalCommandHoverCommandTitle', "Command"));341commandTitle.classList.add('attachment-additional-info');342const commandBlock = dom.$('pre.chat-terminal-command-block');343hoverElement.append(commandTitle, commandBlock);344345if (attachment.output && attachment.output.trim().length > 0) {346const outputTitle = dom.$('div', {}, localize('chat.terminalCommandHoverOutputTitle', "Output:"));347outputTitle.classList.add('attachment-additional-info');348const outputBlock = dom.$('pre.chat-terminal-command-output');349const fullOutputLines = attachment.output.split('\n');350const hoverOutputLines = [];351for (const line of fullOutputLines) {352if (hoverOutputLines.length >= TerminalConstants.MaxAttachmentOutputLineCount) {353hoverOutputLines.push('...');354break;355}356const trimmed = line.trim();357if (trimmed.length === 0) {358continue;359}360if (trimmed.length > TerminalConstants.MaxAttachmentOutputLineLength) {361hoverOutputLines.push(`${trimmed.slice(0, TerminalConstants.MaxAttachmentOutputLineLength)}...`);362} else {363hoverOutputLines.push(trimmed);364}365}366outputBlock.textContent = hoverOutputLines.join('\n');367hoverElement.append(outputTitle, outputBlock);368}369370return {371...commonHoverOptions,372content: hoverElement,373};374}375}376377export class ImageAttachmentWidget extends AbstractChatAttachmentWidget {378379constructor(380resource: URI | undefined,381attachment: IChatRequestVariableEntry,382currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,383options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },384container: HTMLElement,385contextResourceLabels: ResourceLabels,386@ICommandService commandService: ICommandService,387@IOpenerService openerService: IOpenerService,388@IConfigurationService configurationService: IConfigurationService,389@IHoverService private readonly hoverService: IHoverService,390@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,391@IInstantiationService instantiationService: IInstantiationService,392@ILabelService private readonly labelService: ILabelService,393@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,394) {395super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);396397let ariaLabel: string;398if (attachment.omittedState === OmittedState.Full) {399ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name);400} else if (attachment.omittedState === OmittedState.Partial) {401ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name);402} else {403ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name);404}405406const ref = attachment.references?.[0]?.reference;407resource = ref && URI.isUri(ref) ? ref : undefined;408const clickHandler = async () => {409if (resource) {410await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined);411}412};413414const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : 'Current model';415416const fullName = resource ? this.labelService.getUriLabel(resource) : (attachment.fullName || attachment.name);417this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState, this.chatEntitlementService.previewFeaturesDisabled));418419if (resource) {420this.addResourceOpenHandlers(resource, undefined);421instantiationService.invokeFunction(accessor => {422this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));423});424}425}426}427428function createImageElements(resource: URI | undefined, name: string, fullName: string,429element: HTMLElement,430buffer: ArrayBuffer | Uint8Array,431hoverService: IHoverService, ariaLabel: string,432currentLanguageModelName: string | undefined,433clickHandler: () => void,434currentLanguageModel?: ILanguageModelChatMetadataAndIdentifier,435omittedState?: OmittedState,436previewFeaturesDisabled?: boolean): IDisposable {437438const disposable = new DisposableStore();439if (omittedState === OmittedState.Partial) {440element.classList.add('partial-warning');441}442443element.ariaLabel = ariaLabel;444element.style.position = 'relative';445446if (resource) {447element.style.cursor = 'pointer';448disposable.add(dom.addDisposableListener(element, 'click', clickHandler));449}450const supportsVision = modelSupportsVision(currentLanguageModel);451const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$((supportsVision && !previewFeaturesDisabled) ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning'));452const textLabel = dom.$('span.chat-attached-context-custom-text', {}, name);453element.appendChild(pillIcon);454element.appendChild(textLabel);455456const hoverElement = dom.$('div.chat-attached-context-hover');457hoverElement.setAttribute('aria-label', ariaLabel);458459if (previewFeaturesDisabled) {460element.classList.add('warning');461hoverElement.textContent = localize('chat.imageAttachmentPreviewFeaturesDisabled', "Vision is disabled by your organization.");462disposable.add(hoverService.setupDelayedHover(element, {463content: hoverElement,464style: HoverStyle.Pointer,465}));466} else if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) {467element.classList.add('warning');468hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", currentLanguageModelName ?? 'This model');469disposable.add(hoverService.setupDelayedHover(element, {470content: hoverElement,471style: HoverStyle.Pointer,472}));473} else {474disposable.add(hoverService.setupDelayedHover(element, {475content: hoverElement,476style: HoverStyle.Pointer,477}));478479const blob = new Blob([buffer as Uint8Array<ArrayBuffer>], { type: 'image/png' });480const url = URL.createObjectURL(blob);481const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' });482const pill = dom.$('div.chat-attached-context-pill', {}, pillImg);483484// eslint-disable-next-line no-restricted-syntax485const existingPill = element.querySelector('.chat-attached-context-pill');486if (existingPill) {487existingPill.replaceWith(pill);488}489490const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' });491const imageContainer = dom.$('div.chat-attached-context-image-container', {}, hoverImage);492hoverElement.appendChild(imageContainer);493494if (resource) {495const urlContainer = dom.$('a.chat-attached-context-url', {}, omittedState === OmittedState.Partial ? localize('chat.imageAttachmentWarning', "This GIF was partially omitted - current frame will be sent.") : fullName);496const separator = dom.$('div.chat-attached-context-url-separator');497disposable.add(dom.addDisposableListener(urlContainer, 'click', () => clickHandler()));498hoverElement.append(separator, urlContainer);499}500501hoverImage.onload = () => { URL.revokeObjectURL(url); };502hoverImage.onerror = () => {503// reset to original icon on error or invalid image504const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media'));505const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon);506// eslint-disable-next-line no-restricted-syntax507const existingPill = element.querySelector('.chat-attached-context-pill');508if (existingPill) {509existingPill.replaceWith(pill);510}511};512}513return disposable;514}515516export class PasteAttachmentWidget extends AbstractChatAttachmentWidget {517518constructor(519attachment: IChatRequestPasteVariableEntry,520currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,521options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },522container: HTMLElement,523contextResourceLabels: ResourceLabels,524@ICommandService commandService: ICommandService,525@IOpenerService openerService: IOpenerService,526@IConfigurationService configurationService: IConfigurationService,527@IHoverService private readonly hoverService: IHoverService,528@IInstantiationService private readonly instantiationService: IInstantiationService,529) {530super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);531532const ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);533this.element.ariaLabel = ariaLabel;534535const classNames = ['file-icon', `${attachment.language}-lang-file-icon`];536let resource: URI | undefined;537let range: IRange | undefined;538539if (attachment.copiedFrom) {540resource = attachment.copiedFrom.uri;541range = attachment.copiedFrom.range;542const filename = basename(resource.path);543this.label.setLabel(filename, undefined, { extraClasses: classNames });544} else {545this.label.setLabel(attachment.fileName, undefined, { extraClasses: classNames });546}547this.element.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`));548549this.element.style.position = 'relative';550551const sourceUri = attachment.copiedFrom?.uri;552const 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\`\`\``);553this._register(this.hoverService.setupDelayedHover(this.element, {554...commonHoverOptions,555content: hoverContent,556}, commonHoverLifecycleOptions));557558const copiedFromResource = attachment.copiedFrom?.uri;559if (copiedFromResource) {560this._register(this.instantiationService.invokeFunction(hookUpResourceAttachmentDragAndContextMenu, this.element, copiedFromResource));561this.addResourceOpenHandlers(copiedFromResource, range);562}563}564}565566export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {567568private readonly _tooltipHover: MutableDisposable<IDisposable> = this._register(new MutableDisposable());569570constructor(571resource: URI | undefined,572range: IRange | undefined,573attachment: IChatRequestVariableEntry,574correspondingContentReference: IChatContentReference | undefined,575currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,576options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },577container: HTMLElement,578contextResourceLabels: ResourceLabels,579@ICommandService commandService: ICommandService,580@IOpenerService openerService: IOpenerService,581@IConfigurationService configurationService: IConfigurationService,582@IContextKeyService private readonly contextKeyService: IContextKeyService,583@IInstantiationService private readonly instantiationService: IInstantiationService,584@IHoverService private readonly hoverService: IHoverService,585@IModelService private readonly modelService: IModelService,586@ILanguageService private readonly languageService: ILanguageService,587) {588super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);589590const attachmentLabel = attachment.fullName ?? attachment.name;591592// Derive icon classes from resourceUri for file/folder icons593if (isStringVariableEntry(attachment) && attachment.icon && (ThemeIcon.isFile(attachment.icon) || ThemeIcon.isFolder(attachment.icon)) && attachment.resourceUri) {594const fileKind = ThemeIcon.isFolder(attachment.icon) ? FileKind.FOLDER : FileKind.FILE;595const iconClasses = getIconClasses(this.modelService, this.languageService, attachment.resourceUri, fileKind);596this.label.setLabel(attachmentLabel, correspondingContentReference?.options?.status?.description, { extraClasses: iconClasses });597} else {598const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;599this.label.setLabel(withIcon, correspondingContentReference?.options?.status?.description);600}601this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);602603if (attachment.kind === 'diagnostic') {604if (attachment.filterUri) {605resource = attachment.filterUri ? URI.revive(attachment.filterUri) : undefined;606range = attachment.filterRange;607} else {608this.element.style.cursor = 'pointer';609this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => {610this.commandService.executeCommand('workbench.panel.markers.view.focus');611}));612}613}614615if (attachment.kind === 'symbol') {616const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));617this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext));618}619620// Handle click for string context attachments with context commands621if (isStringVariableEntry(attachment) && attachment.commandId) {622this.element.style.cursor = 'pointer';623const contextItemHandle = attachment.handle;624this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {625const chatContextService = this.instantiationService.invokeFunction(accessor => accessor.get(IChatContextService));626await chatContextService.executeChatContextItemCommand(contextItemHandle);627}));628}629630// Setup tooltip hover for string context attachments631if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) {632this._setupTooltipHover(attachment.tooltip);633}634635if (resource) {636this.addResourceOpenHandlers(resource, range);637}638}639640private _setupTooltipHover(tooltip: IMarkdownString): void {641this._tooltipHover.value = this.hoverService.setupDelayedHover(this.element, {642content: tooltip,643appearance: { showPointer: true },644});645}646}647648export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {649650private hintElement: HTMLElement;651652constructor(653attachment: IPromptFileVariableEntry,654currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,655options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },656container: HTMLElement,657contextResourceLabels: ResourceLabels,658@ICommandService commandService: ICommandService,659@IOpenerService openerService: IOpenerService,660@IConfigurationService configurationService: IConfigurationService,661@ILabelService private readonly labelService: ILabelService,662@IInstantiationService private readonly instantiationService: IInstantiationService,663) {664super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);665666667this.hintElement = dom.append(this.element, dom.$('span.prompt-type'));668669this.updateLabel(attachment);670671this.instantiationService.invokeFunction(accessor => {672this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, attachment.value));673});674this.addResourceOpenHandlers(attachment.value, undefined);675}676677private updateLabel(attachment: IPromptFileVariableEntry) {678const resource = attachment.value;679const fileBasename = basename(resource.path);680const fileDirname = dirname(resource.path);681const friendlyName = `${fileBasename} ${fileDirname}`;682const isPrompt = attachment.id.startsWith(PromptFileVariableKind.PromptFile);683const ariaLabel = isPrompt684? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName)685: localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName);686const typeLabel = isPrompt687? localize('prompt', "Prompt")688: localize('instructions', "Instructions");689690const title = this.labelService.getUriLabel(resource) + (attachment.originLabel ? `\n${attachment.originLabel}` : '');691692//const { topError } = this.promptFile;693this.element.classList.remove('warning', 'error');694695// if there are some errors/warning during the process of resolving696// attachment references (including all the nested child references),697// add the issue details in the hover title for the attachment, one698// error/warning at a time because there is a limited space available699// if (topError) {700// const { errorSubject: subject } = topError;701// const isError = (subject === 'root');702// this.element.classList.add((isError) ? 'error' : 'warning');703704// const severity = (isError)705// ? localize('error', "Error")706// : localize('warning', "Warning");707708// title += `\n[${severity}]: ${topError.localizedMessage}`;709// }710711const fileWithoutExtension = getCleanPromptName(resource);712this.label.setFile(URI.file(fileWithoutExtension), {713fileKind: FileKind.FILE,714hidePath: true,715range: undefined,716title,717icon: ThemeIcon.fromId(Codicon.bookmark.id),718extraClasses: [],719});720721this.hintElement.innerText = typeLabel;722723724this.element.ariaLabel = ariaLabel;725}726}727728export class PromptTextAttachmentWidget extends AbstractChatAttachmentWidget {729730constructor(731attachment: IPromptTextVariableEntry,732currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,733options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },734container: HTMLElement,735contextResourceLabels: ResourceLabels,736@ICommandService commandService: ICommandService,737@IOpenerService openerService: IOpenerService,738@IConfigurationService configurationService: IConfigurationService,739@IPreferencesService preferencesService: IPreferencesService,740@IHoverService hoverService: IHoverService741) {742super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);743744if (attachment.settingId) {745const openSettings = () => preferencesService.openSettings({ jsonEditor: false, query: `@id:${attachment.settingId}` });746747this.element.style.cursor = 'pointer';748this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async (e: MouseEvent) => {749dom.EventHelper.stop(e, true);750openSettings();751}));752753this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {754const event = new StandardKeyboardEvent(e);755if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {756dom.EventHelper.stop(e, true);757openSettings();758}759}));760}761this.label.setLabel(localize('instructions.label', 'Additional Instructions'), undefined, undefined);762763this._register(hoverService.setupDelayedHover(this.element, {764...commonHoverOptions,765content: attachment.value,766}, commonHoverLifecycleOptions));767}768}769770771export class ToolSetOrToolItemAttachmentWidget extends AbstractChatAttachmentWidget {772constructor(773attachment: ChatRequestToolReferenceEntry,774currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,775options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },776container: HTMLElement,777contextResourceLabels: ResourceLabels,778@ILanguageModelToolsService toolsService: ILanguageModelToolsService,779@ICommandService commandService: ICommandService,780@IOpenerService openerService: IOpenerService,781@IConfigurationService configurationService: IConfigurationService,782@IHoverService hoverService: IHoverService,783) {784super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);785786787const toolOrToolSet = Iterable.find(toolsService.getTools(currentLanguageModel?.metadata), tool => tool.id === attachment.id) ?? Iterable.find(toolsService.getToolSetsForModel(currentLanguageModel?.metadata), toolSet => toolSet.id === attachment.id);788789let name = attachment.name;790const icon = attachment.icon ?? Codicon.tools;791792if (isToolSet(toolOrToolSet)) {793name = toolOrToolSet.referenceName;794} else if (toolOrToolSet) {795name = toolOrToolSet.toolReferenceName ?? name;796}797798this.label.setLabel(`$(${icon.id})\u00A0${name}`, undefined);799800this.element.style.cursor = 'pointer';801this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", name);802803let hoverContent: string | undefined;804805if (isToolSet(toolOrToolSet)) {806hoverContent = localize('toolset', "{0} - {1}", toolOrToolSet.description ?? toolOrToolSet.referenceName, toolOrToolSet.source.label);807} else if (toolOrToolSet) {808hoverContent = localize('tool', "{0} - {1}", toolOrToolSet.userDescription ?? toolOrToolSet.modelDescription, toolOrToolSet.source.label);809}810811if (hoverContent) {812this._register(hoverService.setupDelayedHover(this.element, {813...commonHoverOptions,814content: hoverContent,815}, commonHoverLifecycleOptions));816}817}818819820}821822export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachmentWidget {823constructor(824resource: URI,825attachment: INotebookOutputVariableEntry,826currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,827options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },828container: HTMLElement,829contextResourceLabels: ResourceLabels,830@ICommandService commandService: ICommandService,831@IOpenerService openerService: IOpenerService,832@IConfigurationService configurationService: IConfigurationService,833@IHoverService private readonly hoverService: IHoverService,834@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,835@INotebookService private readonly notebookService: INotebookService,836@IInstantiationService private readonly instantiationService: IInstantiationService,837@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,838) {839super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);840841switch (attachment.mimeType) {842case 'application/vnd.code.notebook.error': {843this.renderErrorOutput(resource, attachment);844break;845}846case 'image/png':847case 'image/jpeg':848case 'image/svg': {849this.renderImageOutput(resource, attachment);850break;851}852default: {853this.renderGenericOutput(resource, attachment);854}855}856857this.instantiationService.invokeFunction(accessor => {858this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));859});860this.addResourceOpenHandlers(resource, undefined);861}862getAriaLabel(attachment: INotebookOutputVariableEntry): string {863return localize('chat.NotebookImageAttachment', "Attached Notebook output, {0}", attachment.name);864}865private renderErrorOutput(resource: URI, attachment: INotebookOutputVariableEntry) {866const attachmentLabel = attachment.name;867const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;868const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();869let title: string | undefined = undefined;870try {871const error = JSON.parse(new TextDecoder().decode(buffer)) as Error;872if (error.name && error.message) {873title = `${error.name}: ${error.message}`;874}875} catch {876//877}878this.label.setLabel(withIcon, undefined, { title });879this.element.ariaLabel = this.getAriaLabel(attachment);880}881private renderGenericOutput(resource: URI, attachment: INotebookOutputVariableEntry) {882this.element.ariaLabel = this.getAriaLabel(attachment);883this.label.setFile(resource, { hidePath: true, icon: ThemeIcon.fromId('output') });884}885private renderImageOutput(resource: URI, attachment: INotebookOutputVariableEntry) {886let ariaLabel: string;887if (attachment.omittedState === OmittedState.Full) {888ariaLabel = localize('chat.omittedNotebookImageAttachment', "Omitted this Notebook ouput: {0}", attachment.name);889} else if (attachment.omittedState === OmittedState.Partial) {890ariaLabel = localize('chat.partiallyOmittedNotebookImageAttachment', "Partially omitted this Notebook output: {0}", attachment.name);891} else {892ariaLabel = this.getAriaLabel(attachment);893}894895const clickHandler = async () => await this.openResource(resource, { editorOptions: { preserveFocus: true } }, false, undefined);896const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : undefined;897const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();898this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState, this.chatEntitlementService.previewFeaturesDisabled));899}900901private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) {902const parsedInfo = CellUri.parseCellOutputUri(resource);903if (!parsedInfo || typeof parsedInfo.cellHandle !== 'number' || typeof parsedInfo.outputIndex !== 'number') {904return undefined;905}906const notebook = this.notebookService.getNotebookTextModel(parsedInfo.notebook);907if (!notebook) {908return undefined;909}910const cell = notebook.cells.find(c => c.handle === parsedInfo.cellHandle);911if (!cell) {912return undefined;913}914const output = cell.outputs.length > parsedInfo.outputIndex ? cell.outputs[parsedInfo.outputIndex] : undefined;915return output?.outputs.find(o => o.mime === attachment.mimeType);916}917918}919920export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget {921constructor(922attachment: IElementVariableEntry,923currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,924options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },925container: HTMLElement,926contextResourceLabels: ResourceLabels,927@ICommandService commandService: ICommandService,928@IOpenerService openerService: IOpenerService,929@IConfigurationService configurationService: IConfigurationService,930@IEditorService editorService: IEditorService,931) {932super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);933934const ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name);935this.element.ariaLabel = ariaLabel;936937this.element.style.position = 'relative';938this.element.style.cursor = 'pointer';939const attachmentLabel = attachment.name;940const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;941this.label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) });942943this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {944const content = attachment.value?.toString() || '';945await editorService.openEditor({946resource: undefined,947contents: content,948options: {949pinned: true950}951});952}));953}954}955956export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget {957constructor(958attachment: ISCMHistoryItemVariableEntry,959currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,960options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },961container: HTMLElement,962contextResourceLabels: ResourceLabels,963@ICommandService commandService: ICommandService,964@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,965@IHoverService hoverService: IHoverService,966@IOpenerService openerService: IOpenerService,967@IConfigurationService configurationService: IConfigurationService,968@IThemeService themeService: IThemeService969) {970super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);971972this.label.setLabel(attachment.name, undefined);973974this.element.style.cursor = 'pointer';975this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);976977const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);978this._store.add(hoverService.setupDelayedHover(this.element, {979...commonHoverOptions,980content,981}, commonHoverLifecycleOptions));982this._store.add(disposables);983984this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => {985dom.EventHelper.stop(e, true);986this._openAttachment(attachment);987}));988989this._store.add(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {990const event = new StandardKeyboardEvent(e);991if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {992dom.EventHelper.stop(e, true);993this._openAttachment(attachment);994}995}));996}997998private async _openAttachment(attachment: ISCMHistoryItemVariableEntry): Promise<void> {999await this.commandService.executeCommand('_workbench.openMultiDiffEditor', {1000title: getHistoryItemEditorTitle(attachment.historyItem), multiDiffSourceUri: attachment.value1001});1002}1003}10041005export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachmentWidget {1006constructor(1007attachment: ISCMHistoryItemChangeVariableEntry,1008currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,1009options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },1010container: HTMLElement,1011contextResourceLabels: ResourceLabels,1012@ICommandService commandService: ICommandService,1013@IHoverService hoverService: IHoverService,1014@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,1015@IOpenerService openerService: IOpenerService,1016@IConfigurationService configurationService: IConfigurationService,1017@IThemeService themeService: IThemeService,1018@IEditorService private readonly editorService: IEditorService,1019) {1020super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);10211022const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${attachment.historyItem.displayId ?? attachment.historyItem.id}`;1023this.label.setFile(attachment.value, { fileKind: FileKind.FILE, hidePath: true, nameSuffix });10241025this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);10261027const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);1028this._store.add(hoverService.setupDelayedHover(this.element, {1029...commonHoverOptions, content,1030}, commonHoverLifecycleOptions));1031this._store.add(disposables);10321033this.addResourceOpenHandlers(attachment.value, undefined);1034}10351036protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: true): Promise<void>;1037protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: false, range: IRange | undefined): Promise<void>;1038protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory?: boolean, range?: IRange): Promise<void> {1039const attachment = this.attachment as ISCMHistoryItemChangeVariableEntry;1040const historyItem = attachment.historyItem;10411042await this.editorService.openEditor({1043resource,1044label: `${basename(resource.path)} (${historyItem.displayId ?? historyItem.id})`,1045options: { ...options.editorOptions }1046}, options.openToSide ? SIDE_GROUP : undefined);1047}1048}10491050export class SCMHistoryItemChangeRangeAttachmentWidget extends AbstractChatAttachmentWidget {1051constructor(1052attachment: ISCMHistoryItemChangeRangeVariableEntry,1053currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,1054options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },1055container: HTMLElement,1056contextResourceLabels: ResourceLabels,1057@ICommandService commandService: ICommandService,1058@IOpenerService openerService: IOpenerService,1059@IConfigurationService configurationService: IConfigurationService,1060@IEditorService private readonly editorService: IEditorService,1061) {1062super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, configurationService);10631064const historyItemStartId = attachment.historyItemChangeStart.historyItem.displayId ?? attachment.historyItemChangeStart.historyItem.id;1065const historyItemEndId = attachment.historyItemChangeEnd.historyItem.displayId ?? attachment.historyItemChangeEnd.historyItem.id;10661067const nameSuffix = `\u00A0$(${Codicon.gitCommit.id})${historyItemStartId}..${historyItemEndId}`;1068this.label.setFile(attachment.value, { fileKind: FileKind.FILE, hidePath: true, nameSuffix });10691070this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);10711072this.addResourceOpenHandlers(attachment.value, undefined);1073}10741075protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: true): Promise<void>;1076protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory: false, range: IRange | undefined): Promise<void>;1077protected override async openResource(resource: URI, options: IOpenEditorOptions, isDirectory?: boolean, range?: IRange): Promise<void> {1078const attachment = this.attachment as ISCMHistoryItemChangeRangeVariableEntry;1079const historyItemChangeStart = attachment.historyItemChangeStart;1080const historyItemChangeEnd = attachment.historyItemChangeEnd;10811082const originalUriTitle = `${basename(historyItemChangeStart.uri.fsPath)} (${historyItemChangeStart.historyItem.displayId ?? historyItemChangeStart.historyItem.id})`;1083const modifiedUriTitle = `${basename(historyItemChangeEnd.uri.fsPath)} (${historyItemChangeEnd.historyItem.displayId ?? historyItemChangeEnd.historyItem.id})`;10841085await this.editorService.openEditor({1086original: { resource: historyItemChangeStart.uri },1087modified: { resource: historyItemChangeEnd.uri },1088label: `${originalUriTitle} ↔ ${modifiedUriTitle}`,1089options: { ...options.editorOptions }1090}, options.openToSide ? SIDE_GROUP : undefined);1091}1092}10931094export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, resource: URI): IDisposable {1095const contextKeyService = accessor.get(IContextKeyService);1096const instantiationService = accessor.get(IInstantiationService);10971098const store = new DisposableStore();10991100// Context1101const scopedContextKeyService = store.add(contextKeyService.createScoped(widget));1102store.add(setResourceContext(accessor, scopedContextKeyService, resource));11031104// Drag and drop1105widget.draggable = true;1106store.add(dom.addDisposableListener(widget, 'dragstart', e => {1107instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [resource], e));1108e.dataTransfer?.setDragImage(widget, 0, 0);1109}));11101111// Context menu1112store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, MenuId.ChatInputResourceAttachmentContext, resource));11131114return store;1115}11161117export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable {1118const instantiationService = accessor.get(IInstantiationService);1119const languageFeaturesService = accessor.get(ILanguageFeaturesService);1120const textModelService = accessor.get(ITextModelService);11211122const store = new DisposableStore();11231124// Context1125store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri));11261127const chatResourceContext = chatAttachmentResourceContextKey.bindTo(scopedContextKeyService);1128chatResourceContext.set(attachment.value.uri.toString());11291130// Drag and drop1131widget.draggable = true;1132store.add(dom.addDisposableListener(widget, 'dragstart', e => {1133instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [{ resource: attachment.value.uri, selection: attachment.value.range }], e));11341135fillInSymbolsDragData([{1136fsPath: attachment.value.uri.fsPath,1137range: attachment.value.range,1138name: attachment.name,1139kind: attachment.kind,1140}], e);11411142e.dataTransfer?.setDragImage(widget, 0, 0);1143}));11441145// Context menu1146const providerContexts: ReadonlyArray<[IContextKey<boolean>, LanguageFeatureRegistry<unknown>]> = [1147[EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider],1148[EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider],1149[EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider],1150[EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider],1151];11521153const updateContextKeys = async () => {1154const modelRef = await textModelService.createModelReference(attachment.value.uri);1155try {1156const model = modelRef.object.textEditorModel;1157for (const [contextKey, registry] of providerContexts) {1158contextKey.set(registry.has(model));1159}1160} finally {1161modelRef.dispose();1162}1163};1164store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, contextMenuId, attachment.value, updateContextKeys));11651166return store;1167}11681169function setResourceContext(accessor: ServicesAccessor, scopedContextKeyService: IScopedContextKeyService, resource: URI) {1170const fileService = accessor.get(IFileService);1171const languageService = accessor.get(ILanguageService);1172const modelService = accessor.get(IModelService);11731174const resourceContextKey = new ResourceContextKey(scopedContextKeyService, fileService, languageService, modelService);1175resourceContextKey.set(resource);1176return resourceContextKey;1177}11781179function addBasicContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, menuId: MenuId, arg: unknown, updateContextKeys?: () => Promise<void>): IDisposable {1180const contextMenuService = accessor.get(IContextMenuService);1181const menuService = accessor.get(IMenuService);11821183return dom.addDisposableListener(widget, dom.EventType.CONTEXT_MENU, async domEvent => {1184const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);1185dom.EventHelper.stop(domEvent, true);11861187try {1188await updateContextKeys?.();1189} catch (e) {1190console.error(e);1191}11921193contextMenuService.showContextMenu({1194contextKeyService: scopedContextKeyService,1195getAnchor: () => event,1196getActions: () => {1197const menu = menuService.getMenuActions(menuId, scopedContextKeyService, { arg });1198return getFlatContextMenuActions(menu);1199},1200});1201});1202}12031204export const chatAttachmentResourceContextKey = new RawContextKey<string>('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") });120512061207