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
5297 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 { Codicon } from '../../../../../base/common/codicons.js';
6
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
7
import { isElectron } from '../../../../../base/common/platform.js';
8
import { ThemeIcon } from '../../../../../base/common/themables.js';
9
import { localize } from '../../../../../nls.js';
10
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
11
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
12
import { ILabelService } from '../../../../../platform/label/common/label.js';
13
import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
14
import { IWorkbenchContribution } from '../../../../common/contributions.js';
15
import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';
16
import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';
17
import { IEditorService } from '../../../../services/editor/common/editorService.js';
18
import { IHostService } from '../../../../services/host/browser/host.js';
19
import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js';
20
import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js';
21
import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';
22
import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../attachments/chatContextPickService.js';
23
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js';
24
import { isToolSet, ToolDataSource } from '../../common/tools/languageModelToolsService.js';
25
import { IChatWidget } from '../chat.js';
26
import { imageToHash, isImage } from '../widget/input/editor/chatPasteProviders.js';
27
import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js';
28
import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js';
29
import { ITerminalService } from '../../../terminal/browser/terminal.js';
30
import { URI } from '../../../../../base/common/uri.js';
31
import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.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(ClipboardImageContextValuePick)));
56
this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick)));
57
}
58
}
59
60
class ToolsContextPickerPick implements IChatContextPickerItem {
61
62
readonly type = 'pickerPick';
63
readonly label: string = localize('chatContext.tools', 'Tools...');
64
readonly icon: ThemeIcon = Codicon.tools;
65
readonly ordinal = -500;
66
67
isEnabled(widget: IChatWidget): boolean {
68
return !!widget.attachmentCapabilities.supportsToolAttachments;
69
}
70
71
asPicker(widget: IChatWidget): IChatContextPicker {
72
73
type Pick = IChatContextPickerPickItem & { toolInfo: { ordinal: number; label: string } };
74
const items: Pick[] = [];
75
76
for (const [entry, enabled] of widget.input.selectedToolsModel.entriesMap.get()) {
77
if (enabled) {
78
if (isToolSet(entry)) {
79
items.push({
80
toolInfo: ToolDataSource.classify(entry.source),
81
label: entry.referenceName,
82
description: entry.description,
83
asAttachment: (): IChatRequestToolSetEntry => toToolSetVariableEntry(entry)
84
});
85
} else {
86
items.push({
87
toolInfo: ToolDataSource.classify(entry.source),
88
label: entry.toolReferenceName ?? entry.displayName,
89
description: entry.userDescription ?? entry.modelDescription,
90
asAttachment: (): IChatRequestToolEntry => toToolVariableEntry(entry)
91
});
92
}
93
}
94
}
95
96
items.sort((a, b) => {
97
let res = a.toolInfo.ordinal - b.toolInfo.ordinal;
98
if (res === 0) {
99
res = a.toolInfo.label.localeCompare(b.toolInfo.label);
100
}
101
if (res === 0) {
102
res = a.label.localeCompare(b.label);
103
}
104
return res;
105
});
106
107
let lastGroupLabel: string | undefined;
108
const picks: (IQuickPickSeparator | Pick)[] = [];
109
110
for (const item of items) {
111
if (lastGroupLabel !== item.toolInfo.label) {
112
picks.push({ type: 'separator', label: item.toolInfo.label });
113
lastGroupLabel = item.toolInfo.label;
114
}
115
picks.push(item);
116
}
117
118
return {
119
placeholder: localize('chatContext.tools.placeholder', 'Select a tool'),
120
picks: Promise.resolve(picks)
121
};
122
}
123
124
125
}
126
127
128
129
class OpenEditorContextValuePick implements IChatContextValueItem {
130
131
readonly type = 'valuePick';
132
readonly label: string = localize('chatContext.editors', 'Open Editors');
133
readonly icon: ThemeIcon = Codicon.file;
134
readonly ordinal = 800;
135
136
constructor(
137
@IEditorService private _editorService: IEditorService,
138
@ILabelService private _labelService: ILabelService,
139
) { }
140
141
isEnabled(): Promise<boolean> | boolean {
142
return this._editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0;
143
}
144
145
async asAttachment(): Promise<IChatRequestVariableEntry[]> {
146
const result: IChatRequestVariableEntry[] = [];
147
for (const editor of this._editorService.editors) {
148
if (!(editor instanceof FileEditorInput || editor instanceof DiffEditorInput || editor instanceof UntitledTextEditorInput || editor instanceof NotebookEditorInput)) {
149
continue;
150
}
151
const uri = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });
152
if (!uri) {
153
continue;
154
}
155
result.push({
156
kind: 'file',
157
id: uri.toString(),
158
value: uri,
159
name: this._labelService.getUriBasenameLabel(uri),
160
});
161
}
162
return result;
163
}
164
165
}
166
167
168
class ClipboardImageContextValuePick implements IChatContextValueItem {
169
readonly type = 'valuePick';
170
readonly label = localize('imageFromClipboard', 'Image from Clipboard');
171
readonly icon = Codicon.fileMedia;
172
173
constructor(
174
@IClipboardService private readonly _clipboardService: IClipboardService,
175
) { }
176
177
async isEnabled(widget: IChatWidget) {
178
if (!widget.attachmentCapabilities.supportsImageAttachments) {
179
return false;
180
}
181
if (!widget.input.selectedLanguageModel.get()?.metadata.capabilities?.vision) {
182
return false;
183
}
184
const imageData = await this._clipboardService.readImage();
185
return isImage(imageData);
186
}
187
188
async asAttachment(): Promise<IImageVariableEntry> {
189
const fileBuffer = await this._clipboardService.readImage();
190
return {
191
id: await imageToHash(fileBuffer),
192
name: localize('pastedImage', 'Pasted Image'),
193
fullName: localize('pastedImage', 'Pasted Image'),
194
value: fileBuffer,
195
kind: 'image',
196
};
197
}
198
}
199
200
export class TerminalContext implements IChatContextValueItem {
201
202
readonly type = 'valuePick';
203
readonly icon = Codicon.terminal;
204
readonly label = localize('terminal', 'Terminal');
205
constructor(private readonly _resource: URI, @ITerminalService private readonly _terminalService: ITerminalService) {
206
207
}
208
isEnabled(widget: IChatWidget) {
209
const terminal = this._terminalService.getInstanceFromResource(this._resource);
210
return !!widget.attachmentCapabilities.supportsTerminalAttachments && terminal?.isDisposed === false;
211
}
212
async asAttachment(widget: IChatWidget): Promise<IChatRequestVariableEntry | undefined> {
213
const terminal = this._terminalService.getInstanceFromResource(this._resource);
214
if (!terminal) {
215
return;
216
}
217
const params = new URLSearchParams(this._resource.query);
218
const command = terminal.capabilities.get(TerminalCapability.CommandDetection)?.commands.find(cmd => cmd.id === params.get('command'));
219
if (!command) {
220
return;
221
}
222
const attachment: IChatRequestVariableEntry = {
223
kind: 'terminalCommand',
224
id: `terminalCommand:${Date.now()}}`,
225
value: this.asValue(command),
226
name: command.command,
227
command: command.command,
228
output: command.getOutput(),
229
exitCode: command.exitCode,
230
resource: this._resource
231
};
232
const cleanup = new DisposableStore();
233
let disposed = false;
234
const disposeCleanup = () => {
235
if (disposed) {
236
return;
237
}
238
disposed = true;
239
cleanup.dispose();
240
};
241
cleanup.add(widget.attachmentModel.onDidChange(e => {
242
if (e.deleted.includes(attachment.id)) {
243
disposeCleanup();
244
}
245
}));
246
cleanup.add(terminal.onDisposed(() => {
247
widget.attachmentModel.delete(attachment.id);
248
widget.refreshParsedInput();
249
disposeCleanup();
250
}));
251
return attachment;
252
}
253
254
private asValue(command: ITerminalCommand): string {
255
let value = `Command: ${command.command}`;
256
const output = command.getOutput();
257
if (output) {
258
value += `\nOutput:\n${output}`;
259
}
260
if (typeof command.exitCode === 'number') {
261
value += `\nExit Code: ${command.exitCode}`;
262
}
263
return value;
264
}
265
}
266
267
class ScreenshotContextValuePick implements IChatContextValueItem {
268
269
readonly type = 'valuePick';
270
readonly icon = Codicon.deviceCamera;
271
readonly label = (isElectron
272
? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window')
273
: localize('chatContext.attachScreenshot.labelWeb', 'Screenshot'));
274
275
constructor(
276
@IHostService private readonly _hostService: IHostService,
277
) { }
278
279
async isEnabled(widget: IChatWidget) {
280
return !!widget.attachmentCapabilities.supportsImageAttachments && !!widget.input.selectedLanguageModel.get()?.metadata.capabilities?.vision;
281
}
282
283
async asAttachment(): Promise<IChatRequestVariableEntry | undefined> {
284
const blob = await this._hostService.getScreenshot();
285
return blob && convertBufferToScreenshotVariable(blob);
286
}
287
}
288
289