Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/chatArtifactExtraction.ts
13579 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 { match as globMatch } from '../../../../base/common/glob.js';
7
import { getExtensionForMimeType } from '../../../../base/common/mime.js';
8
import { basename as pathBasename } from '../../../../base/common/path.js';
9
import { basename } from '../../../../base/common/resources.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { IChatToolInvocation, IToolResultOutputDetailsSerialized } from './chatService/chatService.js';
12
import { ChatResponseResource, IResponse } from './model/chatModel.js';
13
import { IArtifactGroupConfig, IChatArtifact } from './tools/chatArtifactsService.js';
14
import { isToolResultInputOutputDetails } from './tools/languageModelToolsService.js';
15
16
const CHAT_MEMORY_FILE_SCHEME = 'chat-memory-file';
17
const MEMORY_TOOL_ID = 'copilot_memory';
18
19
export namespace ChatMemoryFileResource {
20
export function createUri(memoryPath: string, sessionResource: URI): URI {
21
return URI.from({
22
scheme: CHAT_MEMORY_FILE_SCHEME,
23
path: memoryPath,
24
query: sessionResource.toString(),
25
});
26
}
27
28
export function isChatMemoryFileUri(uri: URI): boolean {
29
return uri.scheme === CHAT_MEMORY_FILE_SCHEME;
30
}
31
32
export function parse(uri: URI): { memoryPath: string; sessionResource: string } {
33
return {
34
memoryPath: uri.path,
35
sessionResource: uri.query,
36
};
37
}
38
}
39
40
/**
41
* Matches a MIME type against a pattern supporting wildcards.
42
* E.g. `image/*` matches `image/png`, `image/jpeg`, etc.
43
*/
44
function matchMimeType(pattern: string, mimeType: string): boolean {
45
if (pattern === mimeType) {
46
return true;
47
}
48
const [patternType, patternSubtype] = pattern.split('/');
49
const [type] = mimeType.split('/');
50
return patternSubtype === '*' && patternType === type;
51
}
52
53
/**
54
* Finds the first matching rule for a file path from byFilePath rules.
55
*/
56
function findFilePathRule(
57
filePath: string,
58
byFilePath: Record<string, IArtifactGroupConfig>
59
): IArtifactGroupConfig | undefined {
60
const fileBasename = pathBasename(filePath);
61
for (const [pattern, config] of Object.entries(byFilePath)) {
62
if (globMatch(pattern, filePath) || globMatch(pattern, fileBasename)) {
63
return config;
64
}
65
}
66
return undefined;
67
}
68
69
/**
70
* Finds the first matching rule for a MIME type from byMimeType rules.
71
*/
72
function findMimeTypeRule(
73
mimeType: string,
74
byMimeType: Record<string, IArtifactGroupConfig>
75
): IArtifactGroupConfig | undefined {
76
for (const [pattern, config] of Object.entries(byMimeType)) {
77
if (matchMimeType(pattern, mimeType)) {
78
return config;
79
}
80
}
81
return undefined;
82
}
83
84
function isToolResultOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized {
85
return typeof obj === 'object' && obj !== null
86
&& 'output' in obj && typeof (obj as IToolResultOutputDetailsSerialized).output === 'object'
87
&& (obj as IToolResultOutputDetailsSerialized).output?.type === 'data'
88
&& typeof (obj as IToolResultOutputDetailsSerialized).output?.mimeType === 'string';
89
}
90
91
function getMemoryPathFromParams(params: unknown): string | undefined {
92
if (typeof params !== 'object' || params === null) {
93
return undefined;
94
}
95
const path = (params as Record<string, unknown>)['path'];
96
return typeof path === 'string' ? path : undefined;
97
}
98
99
const memoryWriteCommands = new Set(['create', 'str_replace', 'insert']);
100
101
function isMemoryWriteCommand(params: unknown): boolean {
102
if (typeof params !== 'object' || params === null) {
103
return false;
104
}
105
const command = (params as Record<string, unknown>)['command'];
106
return typeof command === 'string' && memoryWriteCommands.has(command);
107
}
108
109
/**
110
* Extracts artifacts from a single response's content parts, applying the given rules.
111
* Pure function, no side effects.
112
*/
113
export function extractArtifactsFromResponse(
114
response: IResponse,
115
sessionResource: URI,
116
byMimeType: Record<string, IArtifactGroupConfig>,
117
byFilePath: Record<string, IArtifactGroupConfig>,
118
byMemoryFilePath: Record<string, IArtifactGroupConfig> = {},
119
): IChatArtifact[] {
120
const artifacts: IChatArtifact[] = [];
121
const seenUris = new Set<string>();
122
123
for (const part of response.value) {
124
// File writes: codeblockUri
125
if (part.kind === 'codeblockUri') {
126
const uri = part.uri;
127
const uriStr = uri.toString();
128
if (seenUris.has(uriStr)) {
129
continue;
130
}
131
const rule = findFilePathRule(uri.path, byFilePath);
132
if (rule) {
133
seenUris.add(uriStr);
134
artifacts.push({
135
label: basename(uri),
136
uri: uriStr,
137
type: 'plan',
138
groupName: rule.groupName,
139
onlyShowGroup: rule.onlyShowGroup,
140
});
141
}
142
}
143
144
// File writes: textEditGroup
145
if (part.kind === 'textEditGroup') {
146
const uri = part.uri;
147
const uriStr = uri.toString();
148
if (seenUris.has(uriStr)) {
149
continue;
150
}
151
const rule = findFilePathRule(uri.path, byFilePath);
152
if (rule) {
153
seenUris.add(uriStr);
154
artifacts.push({
155
label: basename(uri),
156
uri: uriStr,
157
type: 'plan',
158
groupName: rule.groupName,
159
onlyShowGroup: rule.onlyShowGroup,
160
});
161
}
162
}
163
164
// File writes: workspaceEdit
165
if (part.kind === 'workspaceEdit') {
166
for (const edit of part.edits) {
167
const uri = edit.newResource ?? edit.oldResource;
168
if (!uri) {
169
continue;
170
}
171
const uriStr = uri.toString();
172
if (seenUris.has(uriStr)) {
173
continue;
174
}
175
const rule = findFilePathRule(uri.path, byFilePath);
176
if (rule) {
177
seenUris.add(uriStr);
178
artifacts.push({
179
label: basename(uri),
180
uri: uriStr,
181
type: 'plan',
182
groupName: rule.groupName,
183
onlyShowGroup: rule.onlyShowGroup,
184
});
185
}
186
}
187
}
188
189
// Memory tool invocations
190
if ((part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && part.toolId === MEMORY_TOOL_ID) {
191
const params = IChatToolInvocation.getParameters(part);
192
const memoryPath = getMemoryPathFromParams(params);
193
if (memoryPath && isMemoryWriteCommand(params)) {
194
const rule = findFilePathRule(memoryPath, byMemoryFilePath);
195
if (rule) {
196
const key = `memory:${part.toolCallId}:${memoryPath}`;
197
if (!seenUris.has(key)) {
198
seenUris.add(key);
199
artifacts.push({
200
label: pathBasename(memoryPath),
201
uri: ChatMemoryFileResource.createUri(memoryPath, sessionResource).toString(),
202
type: 'plan',
203
groupName: rule.groupName,
204
onlyShowGroup: rule.onlyShowGroup,
205
});
206
}
207
}
208
}
209
}
210
211
// Image results from tool invocations
212
if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') {
213
const details = IChatToolInvocation.resultDetails(part);
214
if (!details) {
215
continue;
216
}
217
218
// IToolResultInputOutputDetails — has output array with embedded data parts
219
if (isToolResultInputOutputDetails(details)) {
220
for (let i = 0; i < details.output.length; i++) {
221
const outputPart = details.output[i];
222
if (outputPart.type === 'embed' && !outputPart.isText && outputPart.mimeType) {
223
const rule = findMimeTypeRule(outputPart.mimeType, byMimeType);
224
if (rule) {
225
const key = `${part.toolCallId}:${i}`;
226
if (!seenUris.has(key)) {
227
seenUris.add(key);
228
const ext = getExtensionForMimeType(outputPart.mimeType);
229
const permalinkBasename = ext ? `file${ext}` : 'file.bin';
230
const artifactUri = ChatResponseResource.createUri(sessionResource, part.toolCallId, i, permalinkBasename);
231
artifacts.push({
232
label: outputPart.uri?.path.split('/').pop() ?? `${rule.groupName} ${i + 1}`,
233
uri: artifactUri.toString(),
234
toolCallId: part.toolCallId,
235
dataPartIndex: i,
236
type: 'screenshot',
237
groupName: rule.groupName,
238
onlyShowGroup: rule.onlyShowGroup,
239
});
240
}
241
}
242
}
243
}
244
}
245
246
// IToolResultOutputDetailsSerialized — single output with mimeType + base64Data
247
if (isToolResultOutputDetailsSerialized(details)) {
248
const rule = findMimeTypeRule(details.output.mimeType, byMimeType);
249
if (rule) {
250
const key = `${part.toolCallId}:0`;
251
if (!seenUris.has(key)) {
252
seenUris.add(key);
253
const ext = getExtensionForMimeType(details.output.mimeType);
254
const permalinkBasename = ext ? `file${ext}` : 'file.bin';
255
const artifactUri = ChatResponseResource.createUri(sessionResource, part.toolCallId, 0, permalinkBasename);
256
artifacts.push({
257
label: `${rule.groupName}`,
258
uri: artifactUri.toString(),
259
toolCallId: part.toolCallId,
260
dataPartIndex: 0,
261
type: 'screenshot',
262
groupName: rule.groupName,
263
onlyShowGroup: rule.onlyShowGroup,
264
});
265
}
266
}
267
}
268
}
269
}
270
271
return artifacts;
272
}
273
274