Path: blob/main/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts
3296 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 { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';11import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';12import { Codicon } from '../../../../base/common/codicons.js';13import * as event from '../../../../base/common/event.js';14import { Iterable } from '../../../../base/common/iterator.js';15import { KeyCode } from '../../../../base/common/keyCodes.js';16import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';17import { basename, dirname } from '../../../../base/common/path.js';18import { ThemeIcon } from '../../../../base/common/themables.js';19import { URI } from '../../../../base/common/uri.js';20import { IRange } from '../../../../editor/common/core/range.js';21import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';22import { LanguageFeatureRegistry } from '../../../../editor/common/languageFeatureRegistry.js';23import { Location, SymbolKind } from '../../../../editor/common/languages.js';24import { ILanguageService } from '../../../../editor/common/languages/language.js';25import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';26import { IModelService } from '../../../../editor/common/services/model.js';27import { ITextModelService } from '../../../../editor/common/services/resolverService.js';28import { localize } from '../../../../nls.js';29import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';30import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';31import { ICommandService } from '../../../../platform/commands/common/commands.js';32import { IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';33import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';34import { fillInSymbolsDragData } from '../../../../platform/dnd/browser/dnd.js';35import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';36import { FileKind, IFileService } from '../../../../platform/files/common/files.js';37import { IHoverService } from '../../../../platform/hover/browser/hover.js';38import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';39import { ILabelService } from '../../../../platform/label/common/label.js';40import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener/common/opener.js';41import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';42import { fillEditorsDragData } from '../../../browser/dnd.js';43import { IFileLabelOptions, IResourceLabel, ResourceLabels } from '../../../browser/labels.js';44import { ResourceContextKey } from '../../../common/contextkeys.js';45import { IEditorService } from '../../../services/editor/common/editorService.js';46import { IPreferencesService } from '../../../services/preferences/common/preferences.js';47import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js';48import { CellUri } from '../../notebook/common/notebookCommon.js';49import { INotebookService } from '../../notebook/common/notebookService.js';50import { getHistoryItemEditorTitle, getHistoryItemHoverContent } from '../../scm/browser/util.js';51import { IChatContentReference } from '../common/chatService.js';52import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry } from '../common/chatVariableEntries.js';53import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';54import { ILanguageModelToolsService, ToolSet } from '../common/languageModelToolsService.js';55import { getCleanPromptName } from '../common/promptSyntax/config/promptFileLocations.js';5657abstract class AbstractChatAttachmentWidget extends Disposable {58public readonly element: HTMLElement;59public readonly label: IResourceLabel;6061private readonly _onDidDelete: event.Emitter<Event> = this._register(new event.Emitter<Event>());62get onDidDelete(): event.Event<Event> {63return this._onDidDelete.event;64}6566private readonly _onDidOpen: event.Emitter<void> = this._register(new event.Emitter<void>());67get onDidOpen(): event.Event<void> {68return this._onDidOpen.event;69}7071constructor(72private readonly attachment: IChatRequestVariableEntry,73private readonly options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },74container: HTMLElement,75contextResourceLabels: ResourceLabels,76protected readonly hoverDelegate: IHoverDelegate,77protected readonly currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,78@ICommandService protected readonly commandService: ICommandService,79@IOpenerService protected readonly openerService: IOpenerService,80) {81super();82this.element = dom.append(container, $('.chat-attached-context-attachment.show-file-icons'));83this.label = contextResourceLabels.create(this.element, { supportIcons: true, hoverDelegate, hoverTargetOverride: this.element });84this._register(this.label);85this.element.tabIndex = 0;86this.element.role = 'button';8788// Add middle-click support for removal89this._register(dom.addDisposableListener(this.element, dom.EventType.AUXCLICK, (e: MouseEvent) => {90if (e.button === 1 /* Middle Button */ && this.options.supportsDeletion && !this.attachment.range) {91e.preventDefault();92e.stopPropagation();93this._onDidDelete.fire(e);94}95}));96}9798protected modelSupportsVision() {99return modelSupportsVision(this.currentLanguageModel);100}101102protected attachClearButton() {103104if (this.attachment.range || !this.options.supportsDeletion) {105// no clear button for attachments with ranges because range means106// referenced from prompt107return;108}109110const clearButton = new Button(this.element, {111supportIcons: true,112hoverDelegate: this.hoverDelegate,113title: localize('chat.attachment.clearButton', "Remove from context")114});115clearButton.element.tabIndex = -1;116clearButton.icon = Codicon.close;117this._register(clearButton);118this._register(event.Event.once(clearButton.onDidClick)((e) => {119this._onDidDelete.fire(e);120}));121this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => {122if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) {123this._onDidDelete.fire(e.browserEvent);124}125}));126}127128protected addResourceOpenHandlers(resource: URI, range: IRange | undefined): void {129this.element.style.cursor = 'pointer';130this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async (e: MouseEvent) => {131dom.EventHelper.stop(e, true);132if (this.attachment.kind === 'directory') {133await this.openResource(resource, true);134} else {135await this.openResource(resource, false, range);136}137}));138139this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {140const event = new StandardKeyboardEvent(e);141if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {142dom.EventHelper.stop(e, true);143if (this.attachment.kind === 'directory') {144await this.openResource(resource, true);145} else {146await this.openResource(resource, false, range);147}148}149}));150}151152protected async openResource(resource: URI, isDirectory: true): Promise<void>;153protected async openResource(resource: URI, isDirectory: false, range: IRange | undefined): Promise<void>;154protected async openResource(resource: URI, isDirectory?: boolean, range?: IRange): Promise<void> {155if (isDirectory) {156// Reveal Directory in explorer157this.commandService.executeCommand(revealInSideBarCommand.id, resource);158return;159}160161// Open file in editor162const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;163const options: OpenInternalOptions = {164fromUserGesture: true,165editorOptions: { ...openTextEditorOptions, preserveFocus: true },166};167await this.openerService.open(resource, options);168this._onDidOpen.fire();169this.element.focus();170}171}172173function modelSupportsVision(currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined) {174return currentLanguageModel?.metadata.capabilities?.vision ?? false;175}176177export class FileAttachmentWidget extends AbstractChatAttachmentWidget {178179constructor(180resource: URI,181range: IRange | undefined,182attachment: IChatRequestVariableEntry,183correspondingContentReference: IChatContentReference | undefined,184currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,185options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },186container: HTMLElement,187contextResourceLabels: ResourceLabels,188hoverDelegate: IHoverDelegate,189@ICommandService commandService: ICommandService,190@IOpenerService openerService: IOpenerService,191@IThemeService private readonly themeService: IThemeService,192@IHoverService private readonly hoverService: IHoverService,193@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,194@IInstantiationService private readonly instantiationService: IInstantiationService,195) {196super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);197198const fileBasename = basename(resource.path);199const fileDirname = dirname(resource.path);200const friendlyName = `${fileBasename} ${fileDirname}`;201let 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);202203if (attachment.omittedState === OmittedState.Full) {204ariaLabel = localize('chat.omittedFileAttachment', "Omitted this file: {0}", attachment.name);205this.renderOmittedWarning(friendlyName, ariaLabel, hoverDelegate);206} else {207const fileOptions: IFileLabelOptions = { hidePath: true, title: correspondingContentReference?.options?.status?.description };208this.label.setFile(resource, attachment.kind === 'file' ? {209...fileOptions,210fileKind: FileKind.FILE,211range,212} : {213...fileOptions,214fileKind: FileKind.FOLDER,215icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined216});217}218219this.element.ariaLabel = ariaLabel;220221this.instantiationService.invokeFunction(accessor => {222this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));223});224this.addResourceOpenHandlers(resource, range);225226this.attachClearButton();227}228229private renderOmittedWarning(friendlyName: string, ariaLabel: string, hoverDelegate: IHoverDelegate) {230const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-warning'));231const textLabel = dom.$('span.chat-attached-context-custom-text', {}, friendlyName);232this.element.appendChild(pillIcon);233this.element.appendChild(textLabel);234235const hoverElement = dom.$('div.chat-attached-context-hover');236hoverElement.setAttribute('aria-label', ariaLabel);237this.element.classList.add('warning');238239hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this file type.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name : this.currentLanguageModel ?? 'This model');240this._register(this.hoverService.setupManagedHover(hoverDelegate, this.element, hoverElement, { trapFocus: true }));241}242}243244export class ImageAttachmentWidget extends AbstractChatAttachmentWidget {245246constructor(247resource: URI | undefined,248attachment: IChatRequestVariableEntry,249currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,250options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },251container: HTMLElement,252contextResourceLabels: ResourceLabels,253hoverDelegate: IHoverDelegate,254@ICommandService commandService: ICommandService,255@IOpenerService openerService: IOpenerService,256@IHoverService private readonly hoverService: IHoverService,257@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,258@IInstantiationService instantiationService: IInstantiationService,259@ILabelService private readonly labelService: ILabelService,260) {261super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);262263let ariaLabel: string;264if (attachment.omittedState === OmittedState.Full) {265ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name);266} else if (attachment.omittedState === OmittedState.Partial) {267ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name);268} else {269ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name);270}271272const ref = attachment.references?.[0]?.reference;273resource = ref && URI.isUri(ref) ? ref : undefined;274const clickHandler = async () => {275if (resource) {276await this.openResource(resource, false, undefined);277}278};279280const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : 'Current model';281282const fullName = resource ? this.labelService.getUriLabel(resource) : (attachment.fullName || attachment.name);283this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState));284285if (resource) {286this.addResourceOpenHandlers(resource, undefined);287instantiationService.invokeFunction(accessor => {288this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));289});290}291292this.attachClearButton();293}294}295296function createImageElements(resource: URI | undefined, name: string, fullName: string,297element: HTMLElement,298buffer: ArrayBuffer | Uint8Array,299hoverService: IHoverService, ariaLabel: string,300currentLanguageModelName: string | undefined,301clickHandler: () => void,302currentLanguageModel?: ILanguageModelChatMetadataAndIdentifier,303omittedState?: OmittedState): IDisposable {304305const disposable = new DisposableStore();306if (omittedState === OmittedState.Partial) {307element.classList.add('partial-warning');308}309310element.ariaLabel = ariaLabel;311element.style.position = 'relative';312313if (resource) {314element.style.cursor = 'pointer';315disposable.add(dom.addDisposableListener(element, 'click', clickHandler));316}317const supportsVision = modelSupportsVision(currentLanguageModel);318const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning'));319const textLabel = dom.$('span.chat-attached-context-custom-text', {}, name);320element.appendChild(pillIcon);321element.appendChild(textLabel);322323const hoverElement = dom.$('div.chat-attached-context-hover');324hoverElement.setAttribute('aria-label', ariaLabel);325326if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) {327element.classList.add('warning');328hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", currentLanguageModelName ?? 'This model');329disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } }));330} else {331disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } }));332333const blob = new Blob([buffer as Uint8Array<ArrayBuffer>], { type: 'image/png' });334const url = URL.createObjectURL(blob);335const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' });336const pill = dom.$('div.chat-attached-context-pill', {}, pillImg);337338const existingPill = element.querySelector('.chat-attached-context-pill');339if (existingPill) {340existingPill.replaceWith(pill);341}342343const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' });344const imageContainer = dom.$('div.chat-attached-context-image-container', {}, hoverImage);345hoverElement.appendChild(imageContainer);346347if (resource) {348const urlContainer = dom.$('a.chat-attached-context-url', {}, omittedState === OmittedState.Partial ? localize('chat.imageAttachmentWarning', "This GIF was partially omitted - current frame will be sent.") : fullName);349const separator = dom.$('div.chat-attached-context-url-separator');350disposable.add(dom.addDisposableListener(urlContainer, 'click', () => clickHandler()));351hoverElement.append(separator, urlContainer);352}353354hoverImage.onload = () => { URL.revokeObjectURL(url); };355hoverImage.onerror = () => {356// reset to original icon on error or invalid image357const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media'));358const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon);359const existingPill = element.querySelector('.chat-attached-context-pill');360if (existingPill) {361existingPill.replaceWith(pill);362}363};364}365return disposable;366}367368export class PasteAttachmentWidget extends AbstractChatAttachmentWidget {369370constructor(371attachment: IChatRequestPasteVariableEntry,372currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,373options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },374container: HTMLElement,375contextResourceLabels: ResourceLabels,376hoverDelegate: IHoverDelegate,377@ICommandService commandService: ICommandService,378@IOpenerService openerService: IOpenerService,379@IHoverService private readonly hoverService: IHoverService,380@IInstantiationService private readonly instantiationService: IInstantiationService,381) {382super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);383384const ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);385this.element.ariaLabel = ariaLabel;386387const classNames = ['file-icon', `${attachment.language}-lang-file-icon`];388let resource: URI | undefined;389let range: IRange | undefined;390391if (attachment.copiedFrom) {392resource = attachment.copiedFrom.uri;393range = attachment.copiedFrom.range;394const filename = basename(resource.path);395this.label.setLabel(filename, undefined, { extraClasses: classNames });396} else {397this.label.setLabel(attachment.fileName, undefined, { extraClasses: classNames });398}399this.element.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`));400401this.element.style.position = 'relative';402403const sourceUri = attachment.copiedFrom?.uri;404const hoverContent: IManagedHoverTooltipMarkdownString = {405markdown: {406value: `${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\`\`\``,407},408markdownNotSupportedFallback: attachment.code,409};410this._register(this.hoverService.setupManagedHover(hoverDelegate, this.element, hoverContent, { trapFocus: true }));411412const copiedFromResource = attachment.copiedFrom?.uri;413if (copiedFromResource) {414this._register(this.instantiationService.invokeFunction(hookUpResourceAttachmentDragAndContextMenu, this.element, copiedFromResource));415this.addResourceOpenHandlers(copiedFromResource, range);416}417418this.attachClearButton();419}420}421422export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget {423constructor(424resource: URI | undefined,425range: IRange | undefined,426attachment: IChatRequestVariableEntry,427correspondingContentReference: IChatContentReference | undefined,428currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,429options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },430container: HTMLElement,431contextResourceLabels: ResourceLabels,432hoverDelegate: IHoverDelegate,433@ICommandService commandService: ICommandService,434@IOpenerService openerService: IOpenerService,435@IContextKeyService private readonly contextKeyService: IContextKeyService,436@IInstantiationService private readonly instantiationService: IInstantiationService,437) {438super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);439440const attachmentLabel = attachment.fullName ?? attachment.name;441const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;442this.label.setLabel(withIcon, correspondingContentReference?.options?.status?.description);443this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);444445if (attachment.kind === 'diagnostic') {446if (attachment.filterUri) {447resource = attachment.filterUri ? URI.revive(attachment.filterUri) : undefined;448range = attachment.filterRange;449} else {450this.element.style.cursor = 'pointer';451this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, () => {452this.commandService.executeCommand('workbench.panel.markers.view.focus');453}));454}455}456457if (attachment.kind === 'symbol') {458const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));459this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext));460}461462if (resource) {463this.addResourceOpenHandlers(resource, range);464}465466this.attachClearButton();467}468}469470export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {471472private hintElement: HTMLElement;473474constructor(475attachment: IPromptFileVariableEntry,476currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,477options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },478container: HTMLElement,479contextResourceLabels: ResourceLabels,480hoverDelegate: IHoverDelegate,481@ICommandService commandService: ICommandService,482@IOpenerService openerService: IOpenerService,483@ILabelService private readonly labelService: ILabelService,484@IInstantiationService private readonly instantiationService: IInstantiationService,485) {486super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);487488489this.hintElement = dom.append(this.element, dom.$('span.prompt-type'));490491this.updateLabel(attachment);492493this.instantiationService.invokeFunction(accessor => {494this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, attachment.value));495});496this.addResourceOpenHandlers(attachment.value, undefined);497498this.attachClearButton();499}500501private updateLabel(attachment: IPromptFileVariableEntry) {502const resource = attachment.value;503const fileBasename = basename(resource.path);504const fileDirname = dirname(resource.path);505const friendlyName = `${fileBasename} ${fileDirname}`;506const isPrompt = attachment.id.startsWith(PromptFileVariableKind.PromptFile);507const ariaLabel = isPrompt508? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName)509: localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName);510const typeLabel = isPrompt511? localize('prompt', "Prompt")512: localize('instructions', "Instructions");513514const title = this.labelService.getUriLabel(resource) + (attachment.originLabel ? `\n${attachment.originLabel}` : '');515516//const { topError } = this.promptFile;517this.element.classList.remove('warning', 'error');518519// if there are some errors/warning during the process of resolving520// attachment references (including all the nested child references),521// add the issue details in the hover title for the attachment, one522// error/warning at a time because there is a limited space available523// if (topError) {524// const { errorSubject: subject } = topError;525// const isError = (subject === 'root');526// this.element.classList.add((isError) ? 'error' : 'warning');527528// const severity = (isError)529// ? localize('error', "Error")530// : localize('warning', "Warning");531532// title += `\n[${severity}]: ${topError.localizedMessage}`;533// }534535const fileWithoutExtension = getCleanPromptName(resource);536this.label.setFile(URI.file(fileWithoutExtension), {537fileKind: FileKind.FILE,538hidePath: true,539range: undefined,540title,541icon: ThemeIcon.fromId(Codicon.bookmark.id),542extraClasses: [],543});544545this.hintElement.innerText = typeLabel;546547548this.element.ariaLabel = ariaLabel;549}550}551552export class PromptTextAttachmentWidget extends AbstractChatAttachmentWidget {553554constructor(555attachment: IPromptTextVariableEntry,556currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,557options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },558container: HTMLElement,559contextResourceLabels: ResourceLabels,560hoverDelegate: IHoverDelegate,561@ICommandService commandService: ICommandService,562@IOpenerService openerService: IOpenerService,563@IPreferencesService preferencesService: IPreferencesService,564@IHoverService hoverService: IHoverService565) {566super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);567568if (attachment.settingId) {569const openSettings = () => preferencesService.openSettings({ jsonEditor: false, query: `@id:${attachment.settingId}` });570571this.element.style.cursor = 'pointer';572this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async (e: MouseEvent) => {573dom.EventHelper.stop(e, true);574openSettings();575}));576577this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {578const event = new StandardKeyboardEvent(e);579if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {580dom.EventHelper.stop(e, true);581openSettings();582}583}));584}585this.label.setLabel(localize('instructions.label', 'Additional Instructions'), undefined, undefined);586587this._register(hoverService.setupManagedHover(hoverDelegate, this.element, attachment.value, { trapFocus: true }));588589}590}591592593export class ToolSetOrToolItemAttachmentWidget extends AbstractChatAttachmentWidget {594constructor(595attachment: ChatRequestToolReferenceEntry,596currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,597options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },598container: HTMLElement,599contextResourceLabels: ResourceLabels,600hoverDelegate: IHoverDelegate,601@ILanguageModelToolsService toolsService: ILanguageModelToolsService,602@ICommandService commandService: ICommandService,603@IOpenerService openerService: IOpenerService,604@IHoverService hoverService: IHoverService605) {606super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);607608609const toolOrToolSet = Iterable.find(toolsService.getTools(), tool => tool.id === attachment.id) ?? Iterable.find(toolsService.toolSets.get(), toolSet => toolSet.id === attachment.id);610611let name = attachment.name;612const icon = attachment.icon ?? Codicon.tools;613614if (toolOrToolSet instanceof ToolSet) {615name = toolOrToolSet.referenceName;616} else if (toolOrToolSet) {617name = toolOrToolSet.toolReferenceName ?? name;618}619620this.label.setLabel(`$(${icon.id})\u00A0${name}`, undefined);621622this.element.style.cursor = 'pointer';623this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", name);624625let hoverContent: string | undefined;626627if (toolOrToolSet instanceof ToolSet) {628hoverContent = localize('toolset', "{0} - {1}", toolOrToolSet.description ?? toolOrToolSet.referenceName, toolOrToolSet.source.label);629} else if (toolOrToolSet) {630hoverContent = localize('tool', "{0} - {1}", toolOrToolSet.userDescription ?? toolOrToolSet.modelDescription, toolOrToolSet.source.label);631}632633if (hoverContent) {634this._register(hoverService.setupManagedHover(hoverDelegate, this.element, hoverContent, { trapFocus: true }));635}636637this.attachClearButton();638}639640641}642643export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachmentWidget {644constructor(645resource: URI,646attachment: INotebookOutputVariableEntry,647currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,648options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },649container: HTMLElement,650contextResourceLabels: ResourceLabels,651hoverDelegate: IHoverDelegate,652@ICommandService commandService: ICommandService,653@IOpenerService openerService: IOpenerService,654@IHoverService private readonly hoverService: IHoverService,655@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,656@INotebookService private readonly notebookService: INotebookService,657@IInstantiationService private readonly instantiationService: IInstantiationService,658) {659super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);660661switch (attachment.mimeType) {662case 'application/vnd.code.notebook.error': {663this.renderErrorOutput(resource, attachment);664break;665}666case 'image/png':667case 'image/jpeg':668case 'image/svg': {669this.renderImageOutput(resource, attachment);670break;671}672default: {673this.renderGenericOutput(resource, attachment);674}675}676677this.instantiationService.invokeFunction(accessor => {678this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource));679});680this.addResourceOpenHandlers(resource, undefined);681this.attachClearButton();682}683getAriaLabel(attachment: INotebookOutputVariableEntry): string {684return localize('chat.NotebookImageAttachment', "Attached Notebook output, {0}", attachment.name);685}686private renderErrorOutput(resource: URI, attachment: INotebookOutputVariableEntry) {687const attachmentLabel = attachment.name;688const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;689const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();690let title: string | undefined = undefined;691try {692const error = JSON.parse(new TextDecoder().decode(buffer)) as Error;693if (error.name && error.message) {694title = `${error.name}: ${error.message}`;695}696} catch {697//698}699this.label.setLabel(withIcon, undefined, { title });700this.element.ariaLabel = this.getAriaLabel(attachment);701}702private renderGenericOutput(resource: URI, attachment: INotebookOutputVariableEntry) {703this.element.ariaLabel = this.getAriaLabel(attachment);704this.label.setFile(resource, { hidePath: true, icon: ThemeIcon.fromId('output') });705}706private renderImageOutput(resource: URI, attachment: INotebookOutputVariableEntry) {707let ariaLabel: string;708if (attachment.omittedState === OmittedState.Full) {709ariaLabel = localize('chat.omittedNotebookImageAttachment', "Omitted this Notebook ouput: {0}", attachment.name);710} else if (attachment.omittedState === OmittedState.Partial) {711ariaLabel = localize('chat.partiallyOmittedNotebookImageAttachment', "Partially omitted this Notebook output: {0}", attachment.name);712} else {713ariaLabel = this.getAriaLabel(attachment);714}715716const clickHandler = async () => await this.openResource(resource, false, undefined);717const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : undefined;718const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array();719this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState));720}721722private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) {723const parsedInfo = CellUri.parseCellOutputUri(resource);724if (!parsedInfo || typeof parsedInfo.cellHandle !== 'number' || typeof parsedInfo.outputIndex !== 'number') {725return undefined;726}727const notebook = this.notebookService.getNotebookTextModel(parsedInfo.notebook);728if (!notebook) {729return undefined;730}731const cell = notebook.cells.find(c => c.handle === parsedInfo.cellHandle);732if (!cell) {733return undefined;734}735const output = cell.outputs.length > parsedInfo.outputIndex ? cell.outputs[parsedInfo.outputIndex] : undefined;736return output?.outputs.find(o => o.mime === attachment.mimeType);737}738739}740741export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget {742constructor(743attachment: IElementVariableEntry,744currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,745options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },746container: HTMLElement,747contextResourceLabels: ResourceLabels,748hoverDelegate: IHoverDelegate,749@ICommandService commandService: ICommandService,750@IOpenerService openerService: IOpenerService,751@IEditorService editorService: IEditorService,752) {753super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);754755const ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name);756this.element.ariaLabel = ariaLabel;757758this.element.style.position = 'relative';759this.element.style.cursor = 'pointer';760const attachmentLabel = attachment.name;761const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;762this.label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) });763764this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {765const content = attachment.value?.toString() || '';766await editorService.openEditor({767resource: undefined,768contents: content,769options: {770pinned: true771}772});773}));774775this.attachClearButton();776}777}778779export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget {780constructor(781attachment: ISCMHistoryItemVariableEntry,782currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,783options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },784container: HTMLElement,785contextResourceLabels: ResourceLabels,786hoverDelegate: IHoverDelegate,787@ICommandService commandService: ICommandService,788@IHoverService hoverService: IHoverService,789@IOpenerService openerService: IOpenerService,790@IThemeService themeService: IThemeService791) {792super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);793794this.label.setLabel(attachment.name, undefined);795796this.element.style.cursor = 'pointer';797this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);798799this._store.add(hoverService.setupManagedHover(hoverDelegate, this.element, () => getHistoryItemHoverContent(themeService, attachment.historyItem), { trapFocus: true }));800801this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => {802dom.EventHelper.stop(e, true);803this._openAttachment(attachment);804}));805806this._store.add(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {807const event = new StandardKeyboardEvent(e);808if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {809dom.EventHelper.stop(e, true);810this._openAttachment(attachment);811}812}));813814this.attachClearButton();815}816817private async _openAttachment(attachment: ISCMHistoryItemVariableEntry): Promise<void> {818await this.commandService.executeCommand('_workbench.openMultiDiffEditor', {819title: getHistoryItemEditorTitle(attachment.historyItem), multiDiffSourceUri: attachment.value820});821}822}823824export function hookUpResourceAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, resource: URI): IDisposable {825const contextKeyService = accessor.get(IContextKeyService);826const instantiationService = accessor.get(IInstantiationService);827828const store = new DisposableStore();829830// Context831const scopedContextKeyService = store.add(contextKeyService.createScoped(widget));832store.add(setResourceContext(accessor, scopedContextKeyService, resource));833834// Drag and drop835widget.draggable = true;836store.add(dom.addDisposableListener(widget, 'dragstart', e => {837instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [resource], e));838e.dataTransfer?.setDragImage(widget, 0, 0);839}));840841// Context menu842store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, MenuId.ChatInputResourceAttachmentContext, resource));843844return store;845}846847export function hookUpSymbolAttachmentDragAndContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, attachment: { name: string; value: Location; kind: SymbolKind }, contextMenuId: MenuId): IDisposable {848const instantiationService = accessor.get(IInstantiationService);849const languageFeaturesService = accessor.get(ILanguageFeaturesService);850const textModelService = accessor.get(ITextModelService);851852const store = new DisposableStore();853854// Context855store.add(setResourceContext(accessor, scopedContextKeyService, attachment.value.uri));856857const chatResourceContext = chatAttachmentResourceContextKey.bindTo(scopedContextKeyService);858chatResourceContext.set(attachment.value.uri.toString());859860// Drag and drop861widget.draggable = true;862store.add(dom.addDisposableListener(widget, 'dragstart', e => {863instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [{ resource: attachment.value.uri, selection: attachment.value.range }], e));864865fillInSymbolsDragData([{866fsPath: attachment.value.uri.fsPath,867range: attachment.value.range,868name: attachment.name,869kind: attachment.kind,870}], e);871872e.dataTransfer?.setDragImage(widget, 0, 0);873}));874875// Context menu876const providerContexts: ReadonlyArray<[IContextKey<boolean>, LanguageFeatureRegistry<unknown>]> = [877[EditorContextKeys.hasDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.definitionProvider],878[EditorContextKeys.hasReferenceProvider.bindTo(scopedContextKeyService), languageFeaturesService.referenceProvider],879[EditorContextKeys.hasImplementationProvider.bindTo(scopedContextKeyService), languageFeaturesService.implementationProvider],880[EditorContextKeys.hasTypeDefinitionProvider.bindTo(scopedContextKeyService), languageFeaturesService.typeDefinitionProvider],881];882883const updateContextKeys = async () => {884const modelRef = await textModelService.createModelReference(attachment.value.uri);885try {886const model = modelRef.object.textEditorModel;887for (const [contextKey, registry] of providerContexts) {888contextKey.set(registry.has(model));889}890} finally {891modelRef.dispose();892}893};894store.add(addBasicContextMenu(accessor, widget, scopedContextKeyService, contextMenuId, attachment.value, updateContextKeys));895896return store;897}898899function setResourceContext(accessor: ServicesAccessor, scopedContextKeyService: IScopedContextKeyService, resource: URI) {900const fileService = accessor.get(IFileService);901const languageService = accessor.get(ILanguageService);902const modelService = accessor.get(IModelService);903904const resourceContextKey = new ResourceContextKey(scopedContextKeyService, fileService, languageService, modelService);905resourceContextKey.set(resource);906return resourceContextKey;907}908909function addBasicContextMenu(accessor: ServicesAccessor, widget: HTMLElement, scopedContextKeyService: IScopedContextKeyService, menuId: MenuId, arg: any, updateContextKeys?: () => Promise<void>): IDisposable {910const contextMenuService = accessor.get(IContextMenuService);911const menuService = accessor.get(IMenuService);912913return dom.addDisposableListener(widget, dom.EventType.CONTEXT_MENU, async domEvent => {914const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);915dom.EventHelper.stop(domEvent, true);916917try {918await updateContextKeys?.();919} catch (e) {920console.error(e);921}922923contextMenuService.showContextMenu({924contextKeyService: scopedContextKeyService,925getAnchor: () => event,926getActions: () => {927const menu = menuService.getMenuActions(menuId, scopedContextKeyService, { arg });928return getFlatContextMenuActions(menu);929},930});931});932}933934export const chatAttachmentResourceContextKey = new RawContextKey<string>('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") });935936937