Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostChatContext.ts
5240 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 type * as vscode from 'vscode';
7
import { CancellationToken } from '../../../base/common/cancellation.js';
8
import { URI, UriComponents } from '../../../base/common/uri.js';
9
import { ExtHostChatContextShape, MainContext, MainThreadChatContextShape } from './extHost.protocol.js';
10
import { DocumentSelector, MarkdownString } from './extHostTypeConverters.js';
11
import { IExtHostRpcService } from './extHostRpcService.js';
12
import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js';
13
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
14
import { IExtHostCommands } from './extHostCommands.js';
15
16
type ProviderType = 'workspace' | 'explicit' | 'resource';
17
18
interface ProviderEntry {
19
type: ProviderType;
20
provider: vscode.ChatWorkspaceContextProvider | vscode.ChatExplicitContextProvider | vscode.ChatResourceContextProvider;
21
disposables: DisposableStore;
22
}
23
24
export class ExtHostChatContext extends Disposable implements ExtHostChatContextShape {
25
declare _serviceBrand: undefined;
26
27
private _proxy: MainThreadChatContextShape;
28
private _handlePool: number = 0;
29
private _providers: Map<number, ProviderEntry> = new Map();
30
private _itemPool: number = 0;
31
/** Global map of itemHandle -> original item for command execution with reference equality */
32
private _globalItems: Map<number, vscode.ChatContextItem> = new Map();
33
/** Track which items belong to which provider for cleanup */
34
private _providerItems: Map<number, Set<number>> = new Map(); // providerHandle -> Set<itemHandle>
35
36
constructor(
37
@IExtHostRpcService extHostRpc: IExtHostRpcService,
38
@IExtHostCommands private readonly _commands: IExtHostCommands,
39
) {
40
super();
41
this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext);
42
}
43
44
// Workspace context provider methods
45
46
async $provideWorkspaceChatContext(handle: number, token: CancellationToken): Promise<IChatContextItem[]> {
47
this._clearProviderItems(handle);
48
const entry = this._providers.get(handle);
49
if (!entry || entry.type !== 'workspace') {
50
throw new Error('Workspace context provider not found');
51
}
52
const provider = entry.provider as vscode.ChatWorkspaceContextProvider;
53
const result = (await provider.provideChatContext(token)) ?? [];
54
return this._convertItems(handle, result);
55
}
56
57
// Explicit context provider methods
58
59
async $provideExplicitChatContext(handle: number, token: CancellationToken): Promise<IChatContextItem[]> {
60
this._clearProviderItems(handle);
61
const entry = this._providers.get(handle);
62
if (!entry || entry.type !== 'explicit') {
63
throw new Error('Explicit context provider not found');
64
}
65
const provider = entry.provider as vscode.ChatExplicitContextProvider;
66
const result = (await provider.provideChatContext(token)) ?? [];
67
return this._convertItems(handle, result);
68
}
69
70
async $resolveExplicitChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise<IChatContextItem> {
71
const entry = this._providers.get(handle);
72
if (!entry || entry.type !== 'explicit') {
73
throw new Error('Explicit context provider not found');
74
}
75
const provider = entry.provider as vscode.ChatExplicitContextProvider;
76
const extItem = this._globalItems.get(context.handle);
77
if (!extItem) {
78
throw new Error('Chat context item not found');
79
}
80
return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token);
81
}
82
83
// Resource context provider methods
84
85
async $provideResourceChatContext(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise<IChatContextItem | undefined> {
86
const entry = this._providers.get(handle);
87
if (!entry || entry.type !== 'resource') {
88
throw new Error('Resource context provider not found');
89
}
90
const provider = entry.provider as vscode.ChatResourceContextProvider;
91
92
const result = await provider.provideChatContext({ resource: URI.revive(options.resource) }, token);
93
if (!result) {
94
return undefined;
95
}
96
if (result.label === undefined && result.resourceUri === undefined) {
97
throw new Error('ChatContextItem must have either a label or a resourceUri');
98
}
99
const itemHandle = this._addTrackedItem(handle, result);
100
101
const item: IChatContextItem = {
102
handle: itemHandle,
103
icon: result.icon,
104
label: result.label,
105
resourceUri: result.resourceUri,
106
modelDescription: result.modelDescription,
107
tooltip: result.tooltip ? MarkdownString.from(result.tooltip) : undefined,
108
value: options.withValue ? result.value : undefined,
109
command: result.command ? { id: result.command.command } : undefined
110
};
111
if (options.withValue && !item.value) {
112
const resolved = await provider.resolveChatContext(result, token);
113
item.value = resolved?.value;
114
item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip;
115
}
116
117
return item;
118
}
119
120
async $resolveResourceChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise<IChatContextItem> {
121
const entry = this._providers.get(handle);
122
if (!entry || entry.type !== 'resource') {
123
throw new Error('Resource context provider not found');
124
}
125
const provider = entry.provider as vscode.ChatResourceContextProvider;
126
const extItem = this._globalItems.get(context.handle);
127
if (!extItem) {
128
throw new Error('Chat context item not found');
129
}
130
return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token);
131
}
132
133
// Command execution
134
135
async $executeChatContextItemCommand(itemHandle: number): Promise<void> {
136
const extItem = this._globalItems.get(itemHandle);
137
if (!extItem) {
138
throw new Error('Chat context item not found');
139
}
140
if (!extItem.command) {
141
throw new Error('Chat context item has no command');
142
}
143
// Execute the command with the original extension item as an argument (reference equality)
144
const args = extItem.command.arguments ? [extItem, ...extItem.command.arguments] : [extItem];
145
await this._commands.executeCommand(extItem.command.command, ...args);
146
}
147
148
// Registration methods
149
150
registerChatWorkspaceContextProvider(id: string, provider: vscode.ChatWorkspaceContextProvider): vscode.Disposable {
151
const handle = this._handlePool++;
152
const disposables = new DisposableStore();
153
this._providers.set(handle, { type: 'workspace', provider, disposables });
154
this._listenForWorkspaceContextChanges(handle, provider, disposables);
155
this._proxy.$registerChatWorkspaceContextProvider(handle, id);
156
157
return {
158
dispose: () => {
159
this._providers.delete(handle);
160
this._clearProviderItems(handle);
161
this._providerItems.delete(handle);
162
this._proxy.$unregisterChatContextProvider(handle);
163
disposables.dispose();
164
}
165
};
166
}
167
168
registerChatExplicitContextProvider(id: string, provider: vscode.ChatExplicitContextProvider): vscode.Disposable {
169
const handle = this._handlePool++;
170
const disposables = new DisposableStore();
171
this._providers.set(handle, { type: 'explicit', provider, disposables });
172
this._proxy.$registerChatExplicitContextProvider(handle, id);
173
174
return {
175
dispose: () => {
176
this._providers.delete(handle);
177
this._clearProviderItems(handle);
178
this._providerItems.delete(handle);
179
this._proxy.$unregisterChatContextProvider(handle);
180
disposables.dispose();
181
}
182
};
183
}
184
185
registerChatResourceContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatResourceContextProvider): vscode.Disposable {
186
const handle = this._handlePool++;
187
const disposables = new DisposableStore();
188
this._providers.set(handle, { type: 'resource', provider, disposables });
189
this._proxy.$registerChatResourceContextProvider(handle, id, DocumentSelector.from(selector));
190
191
return {
192
dispose: () => {
193
this._providers.delete(handle);
194
this._clearProviderItems(handle);
195
this._providerItems.delete(handle);
196
this._proxy.$unregisterChatContextProvider(handle);
197
disposables.dispose();
198
}
199
};
200
}
201
202
/**
203
* @deprecated Use registerChatWorkspaceContextProvider, registerChatExplicitContextProvider, or registerChatResourceContextProvider instead.
204
*/
205
registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable {
206
const disposables: vscode.Disposable[] = [];
207
208
// Register workspace context provider if the provider supports it
209
if (provider.provideWorkspaceChatContext) {
210
const workspaceProvider: vscode.ChatWorkspaceContextProvider = {
211
onDidChangeWorkspaceChatContext: provider.onDidChangeWorkspaceChatContext,
212
provideChatContext: (token) => provider.provideWorkspaceChatContext!(token)
213
};
214
disposables.push(this.registerChatWorkspaceContextProvider(id, workspaceProvider));
215
}
216
217
// Register explicit context provider if the provider supports it
218
if (provider.provideChatContextExplicit) {
219
const explicitProvider: vscode.ChatExplicitContextProvider = {
220
provideChatContext: (token) => provider.provideChatContextExplicit!(token),
221
resolveChatContext: provider.resolveChatContext
222
? (context, token) => provider.resolveChatContext!(context, token)
223
: (context) => context
224
};
225
disposables.push(this.registerChatExplicitContextProvider(id, explicitProvider));
226
}
227
228
// Register resource context provider if the provider supports it and has a selector
229
if (provider.provideChatContextForResource && selector) {
230
const resourceProvider: vscode.ChatResourceContextProvider = {
231
provideChatContext: (options, token) => provider.provideChatContextForResource!(options, token),
232
resolveChatContext: provider.resolveChatContext
233
? (context, token) => provider.resolveChatContext!(context, token)
234
: (context) => context
235
};
236
disposables.push(this.registerChatResourceContextProvider(selector, id, resourceProvider));
237
}
238
239
return {
240
dispose: () => {
241
for (const disposable of disposables) {
242
disposable.dispose();
243
}
244
}
245
};
246
}
247
248
// Helper methods
249
250
private _clearProviderItems(handle: number): void {
251
const itemHandles = this._providerItems.get(handle);
252
if (itemHandles) {
253
for (const itemHandle of itemHandles) {
254
this._globalItems.delete(itemHandle);
255
}
256
itemHandles.clear();
257
}
258
}
259
260
private _addTrackedItem(providerHandle: number, item: vscode.ChatContextItem): number {
261
const itemHandle = this._itemPool++;
262
this._globalItems.set(itemHandle, item);
263
if (!this._providerItems.has(providerHandle)) {
264
this._providerItems.set(providerHandle, new Set());
265
}
266
this._providerItems.get(providerHandle)!.add(itemHandle);
267
return itemHandle;
268
}
269
270
private _convertItems(handle: number, items: vscode.ChatContextItem[]): IChatContextItem[] {
271
const result: IChatContextItem[] = [];
272
for (const item of items) {
273
if (item.label === undefined && item.resourceUri === undefined) {
274
throw new Error('ChatContextItem must have either a label or a resourceUri');
275
}
276
const itemHandle = this._addTrackedItem(handle, item);
277
result.push({
278
handle: itemHandle,
279
icon: item.icon,
280
label: item.label,
281
resourceUri: item.resourceUri,
282
modelDescription: item.modelDescription,
283
tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined,
284
value: item.value,
285
command: item.command ? { id: item.command.command } : undefined
286
});
287
}
288
return result;
289
}
290
291
private async _doResolve(
292
resolveFn: (item: vscode.ChatContextItem, token: CancellationToken) => vscode.ProviderResult<vscode.ChatContextItem>,
293
context: IChatContextItem,
294
extItem: vscode.ChatContextItem,
295
token: CancellationToken
296
): Promise<IChatContextItem> {
297
const extResult = await resolveFn(extItem, token);
298
if (extResult) {
299
return {
300
handle: context.handle,
301
icon: extResult.icon,
302
label: extResult.label,
303
resourceUri: extResult.resourceUri,
304
modelDescription: extResult.modelDescription,
305
tooltip: extResult.tooltip ? MarkdownString.from(extResult.tooltip) : undefined,
306
value: extResult.value,
307
command: extResult.command ? { id: extResult.command.command } : undefined
308
};
309
}
310
return context;
311
}
312
313
private _listenForWorkspaceContextChanges(handle: number, provider: vscode.ChatWorkspaceContextProvider, disposables: DisposableStore): void {
314
if (!provider.onDidChangeWorkspaceChatContext) {
315
return;
316
}
317
const provideWorkspaceContext = async () => {
318
const workspaceContexts = await provider.provideChatContext(CancellationToken.None);
319
const resolvedContexts = this._convertItems(handle, workspaceContexts ?? []);
320
return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts);
321
};
322
323
disposables.add(provider.onDidChangeWorkspaceChatContext(async () => provideWorkspaceContext()));
324
// kick off initial workspace context fetch
325
provideWorkspaceContext();
326
}
327
328
public override dispose(): void {
329
super.dispose();
330
for (const { disposables } of this._providers.values()) {
331
disposables.dispose();
332
}
333
}
334
}
335
336