Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentModel.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { URI } from '../../../../../base/common/uri.js';
7
import { Emitter } from '../../../../../base/common/event.js';
8
import { basename } from '../../../../../base/common/resources.js';
9
import { IRange } from '../../../../../editor/common/core/range.js';
10
import { combinedDisposable, Disposable, DisposableMap, IDisposable } from '../../../../../base/common/lifecycle.js';
11
import { IChatRequestFileEntry, IChatRequestVariableEntry, isPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';
12
import { FileChangeType, IFileService } from '../../../../../platform/files/common/files.js';
13
import { ISharedWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';
14
import { Schemas } from '../../../../../base/common/network.js';
15
import { IChatAttachmentResolveService } from './chatAttachmentResolveService.js';
16
import { CancellationToken } from '../../../../../base/common/cancellation.js';
17
import { equals } from '../../../../../base/common/objects.js';
18
import { Iterable } from '../../../../../base/common/iterator.js';
19
20
export interface IChatAttachmentChangeEvent {
21
readonly deleted: readonly string[];
22
readonly added: readonly IChatRequestVariableEntry[];
23
readonly updated: readonly IChatRequestVariableEntry[];
24
}
25
26
export class ChatAttachmentModel extends Disposable {
27
28
private readonly _attachments = new Map<string, IChatRequestVariableEntry>();
29
private readonly _fileWatchers = this._register(new DisposableMap<IChatRequestFileEntry['id'], IDisposable>());
30
31
private _onDidChange = this._register(new Emitter<IChatAttachmentChangeEvent>());
32
readonly onDidChange = this._onDidChange.event;
33
34
constructor(
35
@IFileService private readonly fileService: IFileService,
36
@ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService,
37
@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService,
38
) {
39
super();
40
}
41
42
get attachments(): ReadonlyArray<IChatRequestVariableEntry> {
43
return Array.from(this._attachments.values());
44
}
45
46
get size(): number {
47
return this._attachments.size;
48
}
49
50
get fileAttachments(): URI[] {
51
return this.attachments.filter(file => file.kind === 'file' && URI.isUri(file.value))
52
.map(file => file.value as URI);
53
}
54
55
getAttachmentIDs() {
56
return new Set(this._attachments.keys());
57
}
58
59
async addFile(uri: URI, range?: IRange) {
60
if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(uri.path)) {
61
const context = await this.asImageVariableEntry(uri);
62
if (context) {
63
this.addContext(context);
64
}
65
return;
66
} else {
67
this.addContext(this.asFileVariableEntry(uri, range));
68
}
69
}
70
71
addFolder(uri: URI) {
72
this.addContext({
73
kind: 'directory',
74
value: uri,
75
id: uri.toString(),
76
name: basename(uri),
77
});
78
}
79
80
clear(clearStickyAttachments: boolean = false): void {
81
if (clearStickyAttachments) {
82
const deleted = Array.from(this._attachments.keys());
83
this._attachments.clear();
84
this._fileWatchers.clearAndDisposeAll();
85
this._onDidChange.fire({ deleted, added: [], updated: [] });
86
} else {
87
const deleted: string[] = [];
88
const allIds = Array.from(this._attachments.keys());
89
for (const id of allIds) {
90
const entry = this._attachments.get(id);
91
if (entry && !isPromptFileVariableEntry(entry)) {
92
this._attachments.delete(id);
93
this._fileWatchers.deleteAndDispose(id);
94
deleted.push(id);
95
}
96
}
97
this._onDidChange.fire({ deleted, added: [], updated: [] });
98
}
99
}
100
101
addContext(...attachments: IChatRequestVariableEntry[]) {
102
attachments = attachments.filter(attachment => !this._attachments.has(attachment.id));
103
this.updateContext(Iterable.empty(), attachments);
104
}
105
106
clearAndSetContext(...attachments: IChatRequestVariableEntry[]) {
107
this.updateContext(Array.from(this._attachments.keys()), attachments);
108
}
109
110
delete(...variableEntryIds: string[]) {
111
this.updateContext(variableEntryIds, Iterable.empty());
112
}
113
114
updateContext(toDelete: Iterable<string>, upsert: Iterable<IChatRequestVariableEntry>) {
115
const deleted: string[] = [];
116
const added: IChatRequestVariableEntry[] = [];
117
const updated: IChatRequestVariableEntry[] = [];
118
119
for (const id of toDelete) {
120
const item = this._attachments.get(id);
121
if (item) {
122
this._attachments.delete(id);
123
deleted.push(id);
124
this._fileWatchers.deleteAndDispose(id);
125
}
126
}
127
128
for (const item of upsert) {
129
const oldItem = this._attachments.get(item.id);
130
if (!oldItem) {
131
this._attachments.set(item.id, item);
132
added.push(item);
133
this._watchAttachment(item);
134
} else if (!equals(oldItem, item)) {
135
this._fileWatchers.deleteAndDispose(item.id);
136
this._attachments.set(item.id, item);
137
updated.push(item);
138
this._watchAttachment(item);
139
}
140
}
141
142
if (deleted.length > 0 || added.length > 0 || updated.length > 0) {
143
this._onDidChange.fire({ deleted, added, updated });
144
}
145
}
146
147
private _watchAttachment(attachment: IChatRequestVariableEntry): void {
148
const uri = IChatRequestVariableEntry.toUri(attachment);
149
if (!uri || uri.scheme !== Schemas.file) {
150
return;
151
}
152
153
const watcher = this.fileService.createWatcher(uri, { recursive: false, excludes: [] });
154
const onDidChangeListener = watcher.onDidChange(e => {
155
if (e.contains(uri, FileChangeType.DELETED)) {
156
this.updateContext([attachment.id], Iterable.empty());
157
}
158
});
159
160
this._fileWatchers.set(attachment.id, combinedDisposable(onDidChangeListener, watcher));
161
}
162
163
// ---- create utils
164
165
asFileVariableEntry(uri: URI, range?: IRange): IChatRequestFileEntry {
166
return {
167
kind: 'file',
168
value: range ? { uri, range } : uri,
169
id: uri.toString() + (range?.toString() ?? ''),
170
name: basename(uri),
171
};
172
}
173
174
// Gets an image variable for a given URI, which may be a file or a web URL
175
async asImageVariableEntry(uri: URI): Promise<IChatRequestVariableEntry | undefined> {
176
if (uri.scheme === Schemas.file && await this.fileService.canHandleResource(uri)) {
177
return await this.chatAttachmentResolveService.resolveImageEditorAttachContext(uri);
178
} else if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {
179
const extractedImages = await this.webContentExtractorService.readImage(uri, CancellationToken.None);
180
if (extractedImages) {
181
return await this.chatAttachmentResolveService.resolveImageEditorAttachContext(uri, extractedImages);
182
}
183
}
184
185
return undefined;
186
}
187
188
}
189
190