Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentModel.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { 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 { combinedDisposable, Disposable, DisposableMap, IDisposable } from '../../../../../base/common/lifecycle.js';10import { IChatRequestFileEntry, IChatRequestVariableEntry, isPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';11import { FileChangeType, 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>();28private readonly _fileWatchers = this._register(new DisposableMap<IChatRequestFileEntry['id'], IDisposable>());2930private _onDidChange = this._register(new Emitter<IChatAttachmentChangeEvent>());31readonly onDidChange = this._onDidChange.event;3233constructor(34@IFileService private readonly fileService: IFileService,35@ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService,36@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService,37) {38super();39}4041get attachments(): ReadonlyArray<IChatRequestVariableEntry> {42return Array.from(this._attachments.values());43}4445get size(): number {46return this._attachments.size;47}4849get fileAttachments(): URI[] {50return this.attachments.filter(file => file.kind === 'file' && URI.isUri(file.value))51.map(file => file.value as URI);52}5354getAttachmentIDs() {55return new Set(this._attachments.keys());56}5758async addFile(uri: URI, range?: IRange) {59if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(uri.path)) {60const context = await this.asImageVariableEntry(uri);61if (context) {62this.addContext(context);63}64return;65} else {66this.addContext(this.asFileVariableEntry(uri, range));67}68}6970addFolder(uri: URI) {71this.addContext({72kind: 'directory',73value: uri,74id: uri.toString(),75name: basename(uri),76});77}7879clear(clearStickyAttachments: boolean = false): void {80if (clearStickyAttachments) {81const deleted = Array.from(this._attachments.keys());82this._attachments.clear();83this._fileWatchers.clearAndDisposeAll();84this._onDidChange.fire({ deleted, added: [], updated: [] });85} else {86const deleted: string[] = [];87const allIds = Array.from(this._attachments.keys());88for (const id of allIds) {89const entry = this._attachments.get(id);90if (entry && !isPromptFileVariableEntry(entry)) {91this._attachments.delete(id);92this._fileWatchers.deleteAndDispose(id);93deleted.push(id);94}95}96this._onDidChange.fire({ deleted, added: [], updated: [] });97}98}99100addContext(...attachments: IChatRequestVariableEntry[]) {101attachments = attachments.filter(attachment => !this._attachments.has(attachment.id));102this.updateContext(Iterable.empty(), attachments);103}104105clearAndSetContext(...attachments: IChatRequestVariableEntry[]) {106this.updateContext(Array.from(this._attachments.keys()), attachments);107}108109delete(...variableEntryIds: string[]) {110this.updateContext(variableEntryIds, Iterable.empty());111}112113updateContext(toDelete: Iterable<string>, upsert: Iterable<IChatRequestVariableEntry>) {114const deleted: string[] = [];115const added: IChatRequestVariableEntry[] = [];116const updated: IChatRequestVariableEntry[] = [];117118for (const id of toDelete) {119const item = this._attachments.get(id);120if (item) {121this._attachments.delete(id);122deleted.push(id);123this._fileWatchers.deleteAndDispose(id);124}125}126127for (const item of upsert) {128const oldItem = this._attachments.get(item.id);129if (!oldItem) {130this._attachments.set(item.id, item);131added.push(item);132this._watchAttachment(item);133} else if (!equals(oldItem, item)) {134this._fileWatchers.deleteAndDispose(item.id);135this._attachments.set(item.id, item);136updated.push(item);137this._watchAttachment(item);138}139}140141if (deleted.length > 0 || added.length > 0 || updated.length > 0) {142this._onDidChange.fire({ deleted, added, updated });143}144}145146private _watchAttachment(attachment: IChatRequestVariableEntry): void {147const uri = IChatRequestVariableEntry.toUri(attachment);148if (!uri || uri.scheme !== Schemas.file) {149return;150}151152const watcher = this.fileService.createWatcher(uri, { recursive: false, excludes: [] });153const onDidChangeListener = watcher.onDidChange(e => {154if (e.contains(uri, FileChangeType.DELETED)) {155this.updateContext([attachment.id], Iterable.empty());156}157});158159this._fileWatchers.set(attachment.id, combinedDisposable(onDidChangeListener, watcher));160}161162// ---- create utils163164asFileVariableEntry(uri: URI, range?: IRange): IChatRequestFileEntry {165return {166kind: 'file',167value: range ? { uri, range } : uri,168id: uri.toString() + (range?.toString() ?? ''),169name: basename(uri),170};171}172173// Gets an image variable for a given URI, which may be a file or a web URL174async asImageVariableEntry(uri: URI): Promise<IChatRequestVariableEntry | undefined> {175if (uri.scheme === Schemas.file && await this.fileService.canHandleResource(uri)) {176return await this.chatAttachmentResolveService.resolveImageEditorAttachContext(uri);177} else if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {178const extractedImages = await this.webContentExtractorService.readImage(uri, CancellationToken.None);179if (extractedImages) {180return await this.chatAttachmentResolveService.resolveImageEditorAttachContext(uri, extractedImages);181}182}183184return undefined;185}186187}188189190