Path: blob/main/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.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 { URI } from '../../../../base/common/uri.js';6import { Emitter } from '../../../../base/common/event.js';7import { basename } from '../../../../base/common/resources.js';8import { IRange } from '../../../../editor/common/core/range.js';9import { Disposable } from '../../../../base/common/lifecycle.js';10import { IChatRequestFileEntry, IChatRequestVariableEntry, isPromptFileVariableEntry } from '../common/chatVariableEntries.js';11import { IFileService } from '../../../../platform/files/common/files.js';12import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';13import { Schemas } from '../../../../base/common/network.js';14import { IChatAttachmentResolveService } from './chatAttachmentResolveService.js';15import { CancellationToken } from '../../../../base/common/cancellation.js';16import { equals } from '../../../../base/common/objects.js';17import { Iterable } from '../../../../base/common/iterator.js';1819export interface IChatAttachmentChangeEvent {20readonly deleted: readonly string[];21readonly added: readonly IChatRequestVariableEntry[];22readonly updated: readonly IChatRequestVariableEntry[];23}2425export class ChatAttachmentModel extends Disposable {2627private readonly _attachments = new Map<string, IChatRequestVariableEntry>();2829private _onDidChange = this._register(new Emitter<IChatAttachmentChangeEvent>());30readonly onDidChange = this._onDidChange.event;3132constructor(33@IFileService private readonly fileService: IFileService,34@ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService,35@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService36) {37super();38}3940get attachments(): ReadonlyArray<IChatRequestVariableEntry> {41return Array.from(this._attachments.values());42}4344get size(): number {45return this._attachments.size;46}4748get fileAttachments(): URI[] {49return this.attachments.filter(file => file.kind === 'file' && URI.isUri(file.value))50.map(file => file.value as URI);51}5253getAttachmentIDs() {54return new Set(this._attachments.keys());55}5657async addFile(uri: URI, range?: IRange) {58if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(uri.path)) {59const context = await this.asImageVariableEntry(uri);60if (context) {61this.addContext(context);62}63return;64} else {65this.addContext(this.asFileVariableEntry(uri, range));66}67}6869addFolder(uri: URI) {70this.addContext({71kind: 'directory',72value: uri,73id: uri.toString(),74name: basename(uri),75});76}7778clear(clearStickyAttachments: boolean = false): void {79if (clearStickyAttachments) {80const deleted = Array.from(this._attachments.keys());81this._attachments.clear();82this._onDidChange.fire({ deleted, added: [], updated: [] });83} else {84const deleted: string[] = [];85const allIds = Array.from(this._attachments.keys());86for (const id of allIds) {87const entry = this._attachments.get(id);88if (entry && !isPromptFileVariableEntry(entry)) {89this._attachments.delete(id);90deleted.push(id);91}92}93this._onDidChange.fire({ deleted, added: [], updated: [] });94}95}9697addContext(...attachments: IChatRequestVariableEntry[]) {98attachments = attachments.filter(attachment => !this._attachments.has(attachment.id));99this.updateContext(Iterable.empty(), attachments);100}101102clearAndSetContext(...attachments: IChatRequestVariableEntry[]) {103this.updateContext(Array.from(this._attachments.keys()), attachments);104}105106delete(...variableEntryIds: string[]) {107this.updateContext(variableEntryIds, Iterable.empty());108}109110updateContext(toDelete: Iterable<string>, upsert: Iterable<IChatRequestVariableEntry>) {111const deleted: string[] = [];112const added: IChatRequestVariableEntry[] = [];113const updated: IChatRequestVariableEntry[] = [];114115for (const id of toDelete) {116const item = this._attachments.get(id);117if (item) {118this._attachments.delete(id);119deleted.push(id);120}121}122123for (const item of upsert) {124const oldItem = this._attachments.get(item.id);125if (!oldItem) {126this._attachments.set(item.id, item);127added.push(item);128} else if (!equals(oldItem, item)) {129this._attachments.set(item.id, item);130updated.push(item);131}132}133134if (deleted.length > 0 || added.length > 0 || updated.length > 0) {135this._onDidChange.fire({ deleted, added, updated });136}137}138139// ---- create utils140141asFileVariableEntry(uri: URI, range?: IRange): IChatRequestFileEntry {142return {143kind: 'file',144value: range ? { uri, range } : uri,145id: uri.toString() + (range?.toString() ?? ''),146name: basename(uri),147};148}149150// Gets an image variable for a given URI, which may be a file or a web URL151async asImageVariableEntry(uri: URI): Promise<IChatRequestVariableEntry | undefined> {152if (uri.scheme === Schemas.file && await this.fileService.canHandleResource(uri)) {153return await this.chatAttachmentResolveService.resolveImageEditorAttachContext(uri);154} else if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {155const extractedImages = await this.webContentExtractorService.readImage(uri, CancellationToken.None);156if (extractedImages) {157return await this.chatAttachmentResolveService.resolveImageEditorAttachContext(uri, extractedImages);158}159}160161return undefined;162}163164}165166167