Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts
3296 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
import { CancellationToken } from '../../../../../base/common/cancellation.js';
6
import { Codicon } from '../../../../../base/common/codicons.js';
7
import { Disposable } from '../../../../../base/common/lifecycle.js';
8
import { isElectron } from '../../../../../base/common/platform.js';
9
import { dirname } from '../../../../../base/common/resources.js';
10
import { ThemeIcon } from '../../../../../base/common/themables.js';
11
import { localize } from '../../../../../nls.js';
12
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
13
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
14
import { ILabelService } from '../../../../../platform/label/common/label.js';
15
import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
16
import { IWorkbenchContribution } from '../../../../common/contributions.js';
17
import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';
18
import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';
19
import { IEditorService } from '../../../../services/editor/common/editorService.js';
20
import { IHostService } from '../../../../services/host/browser/host.js';
21
import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js';
22
import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js';
23
import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';
24
import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../chatContextPickService.js';
25
import { IChatEditingService } from '../../common/chatEditingService.js';
26
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState, toToolSetVariableEntry, toToolVariableEntry } from '../../common/chatVariableEntries.js';
27
import { ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js';
28
import { IChatWidget } from '../chat.js';
29
import { imageToHash, isImage } from '../chatPasteProviders.js';
30
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
31
import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js';
32
33
34
export class ChatContextContributions extends Disposable implements IWorkbenchContribution {
35
36
static readonly ID = 'chat.contextContributions';
37
38
constructor(
39
@IInstantiationService instantiationService: IInstantiationService,
40
@IChatContextPickService contextPickService: IChatContextPickService,
41
) {
42
super();
43
44
// ###############################################################################################
45
//
46
// Default context picks/values which are "native" to chat. This is NOT the complete list
47
// and feature area specific context, like for notebooks, problems, etc, should be contributed
48
// by the feature area.
49
//
50
// ###############################################################################################
51
52
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ToolsContextPickerPick)));
53
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ChatInstructionsPickerPick)));
54
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(OpenEditorContextValuePick)));
55
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(RelatedFilesContextPickerPick)));
56
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ClipboardImageContextValuePick)));
57
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick)));
58
}
59
}
60
61
class ToolsContextPickerPick implements IChatContextPickerItem {
62
63
readonly type = 'pickerPick';
64
readonly label: string = localize('chatContext.tools', 'Tools...');
65
readonly icon: ThemeIcon = Codicon.tools;
66
readonly ordinal = -500;
67
68
asPicker(widget: IChatWidget): IChatContextPicker {
69
70
type Pick = IChatContextPickerPickItem & { toolInfo: { ordinal: number; label: string } };
71
const items: Pick[] = [];
72
73
for (const [entry, enabled] of widget.input.selectedToolsModel.entriesMap.get()) {
74
if (enabled) {
75
if (entry instanceof ToolSet) {
76
items.push({
77
toolInfo: ToolDataSource.classify(entry.source),
78
label: entry.referenceName,
79
description: entry.description,
80
asAttachment: (): IChatRequestToolSetEntry => toToolSetVariableEntry(entry)
81
});
82
} else {
83
items.push({
84
toolInfo: ToolDataSource.classify(entry.source),
85
label: entry.toolReferenceName ?? entry.displayName,
86
description: entry.userDescription ?? entry.modelDescription,
87
asAttachment: (): IChatRequestToolEntry => toToolVariableEntry(entry)
88
});
89
}
90
}
91
}
92
93
items.sort((a, b) => {
94
let res = a.toolInfo.ordinal - b.toolInfo.ordinal;
95
if (res === 0) {
96
res = a.toolInfo.label.localeCompare(b.toolInfo.label);
97
}
98
if (res === 0) {
99
res = a.label.localeCompare(b.label);
100
}
101
return res;
102
});
103
104
let lastGroupLabel: string | undefined;
105
const picks: (IQuickPickSeparator | Pick)[] = [];
106
107
for (const item of items) {
108
if (lastGroupLabel !== item.toolInfo.label) {
109
picks.push({ type: 'separator', label: item.toolInfo.label });
110
lastGroupLabel = item.toolInfo.label;
111
}
112
picks.push(item);
113
}
114
115
return {
116
placeholder: localize('chatContext.tools.placeholder', 'Select a tool'),
117
picks: Promise.resolve(picks)
118
};
119
}
120
121
122
}
123
124
125
126
class OpenEditorContextValuePick implements IChatContextValueItem {
127
128
readonly type = 'valuePick';
129
readonly label: string = localize('chatContext.editors', 'Open Editors');
130
readonly icon: ThemeIcon = Codicon.file;
131
readonly ordinal = 800;
132
133
constructor(
134
@IEditorService private _editorService: IEditorService,
135
@ILabelService private _labelService: ILabelService,
136
) { }
137
138
isEnabled(): Promise<boolean> | boolean {
139
return this._editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0;
140
}
141
142
async asAttachment(): Promise<IChatRequestVariableEntry[]> {
143
const result: IChatRequestVariableEntry[] = [];
144
for (const editor of this._editorService.editors) {
145
if (!(editor instanceof FileEditorInput || editor instanceof DiffEditorInput || editor instanceof UntitledTextEditorInput || editor instanceof NotebookEditorInput)) {
146
continue;
147
}
148
const uri = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });
149
if (!uri) {
150
continue;
151
}
152
result.push({
153
kind: 'file',
154
id: uri.toString(),
155
value: uri,
156
name: this._labelService.getUriBasenameLabel(uri),
157
});
158
}
159
return result;
160
}
161
162
}
163
164
class RelatedFilesContextPickerPick implements IChatContextPickerItem {
165
166
readonly type = 'pickerPick';
167
168
readonly label: string = localize('chatContext.relatedFiles', 'Related Files');
169
readonly icon: ThemeIcon = Codicon.sparkle;
170
readonly ordinal = 300;
171
172
constructor(
173
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
174
@ILabelService private readonly _labelService: ILabelService,
175
) { }
176
177
isEnabled(widget: IChatWidget): boolean {
178
return this._chatEditingService.hasRelatedFilesProviders() && (Boolean(widget.getInput()) || widget.attachmentModel.fileAttachments.length > 0);
179
}
180
181
asPicker(widget: IChatWidget): IChatContextPicker {
182
183
const picks = (async () => {
184
const chatSessionId = widget.viewModel?.sessionId;
185
if (!chatSessionId) {
186
return [];
187
}
188
const relatedFiles = await this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None);
189
if (!relatedFiles) {
190
return [];
191
}
192
const attachments = widget.attachmentModel.getAttachmentIDs();
193
return this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None)
194
.then((files) => (files ?? []).reduce<(IChatContextPickerPickItem | IQuickPickSeparator)[]>((acc, cur) => {
195
acc.push({ type: 'separator', label: cur.group });
196
for (const file of cur.files) {
197
const label = this._labelService.getUriBasenameLabel(file.uri);
198
acc.push({
199
label: label,
200
description: this._labelService.getUriLabel(dirname(file.uri), { relative: true }),
201
disabled: attachments.has(file.uri.toString()),
202
asAttachment: () => {
203
return {
204
kind: 'file',
205
id: file.uri.toString(),
206
value: file.uri,
207
name: label,
208
omittedState: OmittedState.NotOmitted
209
};
210
}
211
});
212
}
213
return acc;
214
}, []));
215
})();
216
217
return {
218
placeholder: localize('relatedFiles', 'Add related files to your working set'),
219
picks,
220
};
221
}
222
}
223
224
225
class ClipboardImageContextValuePick implements IChatContextValueItem {
226
readonly type = 'valuePick';
227
readonly label = localize('imageFromClipboard', 'Image from Clipboard');
228
readonly icon = Codicon.fileMedia;
229
230
constructor(
231
@IClipboardService private readonly _clipboardService: IClipboardService,
232
) { }
233
234
async isEnabled(widget: IChatWidget) {
235
if (!widget.input.selectedLanguageModel?.metadata.capabilities?.vision) {
236
return false;
237
}
238
const imageData = await this._clipboardService.readImage();
239
return isImage(imageData);
240
}
241
242
async asAttachment(): Promise<IImageVariableEntry> {
243
const fileBuffer = await this._clipboardService.readImage();
244
return {
245
id: await imageToHash(fileBuffer),
246
name: localize('pastedImage', 'Pasted Image'),
247
fullName: localize('pastedImage', 'Pasted Image'),
248
value: fileBuffer,
249
kind: 'image',
250
};
251
}
252
}
253
254
class ScreenshotContextValuePick implements IChatContextValueItem {
255
256
readonly type = 'valuePick';
257
readonly icon = Codicon.deviceCamera;
258
readonly label = (isElectron
259
? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window')
260
: localize('chatContext.attachScreenshot.labelWeb', 'Screenshot'));
261
262
constructor(
263
@IHostService private readonly _hostService: IHostService,
264
) { }
265
266
async isEnabled(widget: IChatWidget) {
267
return !!widget.input.selectedLanguageModel?.metadata.capabilities?.vision;
268
}
269
270
async asAttachment(): Promise<IChatRequestVariableEntry | undefined> {
271
const blob = await this._hostService.getScreenshot();
272
return blob && convertBufferToScreenshotVariable(blob);
273
}
274
}
275
276