Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostChatSessions.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
6
import type * as vscode from 'vscode';
7
import { coalesce } from '../../../base/common/arrays.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
9
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
10
import { MarshalledId } from '../../../base/common/marshallingIds.js';
11
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
12
import { ILogService } from '../../../platform/log/common/log.js';
13
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
14
import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js';
15
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
16
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
17
import { ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, MainContext, MainThreadChatSessionsShape } from './extHost.protocol.js';
18
import { ChatAgentResponseStream } from './extHostChatAgents2.js';
19
import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';
20
import { ExtHostLanguageModels } from './extHostLanguageModels.js';
21
import { IExtHostRpcService } from './extHostRpcService.js';
22
import * as typeConvert from './extHostTypeConverters.js';
23
import * as extHostTypes from './extHostTypes.js';
24
import { revive } from '../../../base/common/marshalling.js';
25
26
class ExtHostChatSession {
27
private _stream: ChatAgentResponseStream;
28
29
constructor(
30
public readonly session: vscode.ChatSession,
31
public readonly extension: IExtensionDescription,
32
request: IChatAgentRequest,
33
public readonly proxy: IChatAgentProgressShape,
34
public readonly commandsConverter: CommandsConverter,
35
public readonly sessionDisposables: DisposableStore
36
) {
37
this._stream = new ChatAgentResponseStream(extension, request, proxy, commandsConverter, sessionDisposables);
38
}
39
40
get activeResponseStream() {
41
return this._stream;
42
}
43
44
getActiveRequestStream(request: IChatAgentRequest) {
45
return new ChatAgentResponseStream(this.extension, request, this.proxy, this.commandsConverter, this.sessionDisposables);
46
}
47
}
48
49
export class ExtHostChatSessions extends Disposable implements ExtHostChatSessionsShape {
50
private static _sessionHandlePool = 0;
51
52
private readonly _proxy: Proxied<MainThreadChatSessionsShape>;
53
private readonly _chatSessionItemProviders = new Map<number, {
54
readonly provider: vscode.ChatSessionItemProvider;
55
readonly extension: IExtensionDescription;
56
readonly disposable: DisposableStore;
57
}>();
58
private readonly _chatSessionContentProviders = new Map<number, {
59
readonly provider: vscode.ChatSessionContentProvider;
60
readonly extension: IExtensionDescription;
61
readonly capabilities?: vscode.ChatSessionCapabilities;
62
readonly disposable: DisposableStore;
63
}>();
64
private _nextChatSessionItemProviderHandle = 0;
65
private _nextChatSessionContentProviderHandle = 0;
66
private readonly _sessionMap: Map<string, vscode.ChatSessionItem> = new Map();
67
68
constructor(
69
private readonly commands: ExtHostCommands,
70
private readonly _languageModels: ExtHostLanguageModels,
71
@IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService,
72
@ILogService private readonly _logService: ILogService,
73
) {
74
super();
75
this._proxy = this._extHostRpc.getProxy(MainContext.MainThreadChatSessions);
76
77
commands.registerArgumentProcessor({
78
processArgument: (arg) => {
79
if (arg && arg.$mid === MarshalledId.ChatSessionContext) {
80
const id = arg.session.id;
81
const sessionContent = this._sessionMap.get(id);
82
if (sessionContent) {
83
return sessionContent;
84
} else {
85
this._logService.warn(`No chat session found for ID: ${id}`);
86
return arg;
87
}
88
}
89
90
return arg;
91
}
92
});
93
}
94
95
registerChatSessionItemProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionItemProvider): vscode.Disposable {
96
const handle = this._nextChatSessionItemProviderHandle++;
97
const disposables = new DisposableStore();
98
99
this._chatSessionItemProviders.set(handle, { provider, extension, disposable: disposables });
100
this._proxy.$registerChatSessionItemProvider(handle, chatSessionType);
101
if (provider.onDidChangeChatSessionItems) {
102
disposables.add(provider.onDidChangeChatSessionItems(() => {
103
this._proxy.$onDidChangeChatSessionItems(handle);
104
}));
105
}
106
return {
107
dispose: () => {
108
this._chatSessionItemProviders.delete(handle);
109
disposables.dispose();
110
this._proxy.$unregisterChatSessionItemProvider(handle);
111
}
112
};
113
}
114
115
registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable {
116
const handle = this._nextChatSessionContentProviderHandle++;
117
const disposables = new DisposableStore();
118
119
this._chatSessionContentProviders.set(handle, { provider, extension, capabilities, disposable: disposables });
120
this._proxy.$registerChatSessionContentProvider(handle, chatSessionType);
121
122
return new extHostTypes.Disposable(() => {
123
this._chatSessionContentProviders.delete(handle);
124
disposables.dispose();
125
this._proxy.$unregisterChatSessionContentProvider(handle);
126
});
127
}
128
129
async showChatSession(_extension: IExtensionDescription, chatSessionType: string, sessionId: string, options: vscode.ChatSessionShowOptions | undefined): Promise<void> {
130
await this._proxy.$showChatSession(chatSessionType, sessionId, typeConvert.ViewColumn.from(options?.viewColumn));
131
}
132
133
private convertChatSessionStatus(status: vscode.ChatSessionStatus | undefined): ChatSessionStatus | undefined {
134
if (status === undefined) {
135
return undefined;
136
}
137
switch (status) {
138
case 0: // vscode.ChatSessionStatus.Failed
139
return ChatSessionStatus.Failed;
140
case 1: // vscode.ChatSessionStatus.Completed
141
return ChatSessionStatus.Completed;
142
case 2: // vscode.ChatSessionStatus.InProgress
143
return ChatSessionStatus.InProgress;
144
default:
145
return undefined;
146
}
147
}
148
149
private convertChatSessionItem(sessionContent: vscode.ChatSessionItem): IChatSessionItem {
150
return {
151
id: sessionContent.id,
152
label: sessionContent.label,
153
description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined,
154
status: this.convertChatSessionStatus(sessionContent.status),
155
tooltip: typeConvert.MarkdownString.fromStrict(sessionContent.tooltip),
156
timing: {
157
startTime: sessionContent.timing?.startTime ?? 0,
158
endTime: sessionContent.timing?.endTime
159
},
160
statistics: sessionContent.statistics ? {
161
insertions: sessionContent.statistics?.insertions ?? 0,
162
deletions: sessionContent.statistics?.deletions ?? 0
163
} : undefined
164
};
165
}
166
167
async $provideNewChatSessionItem(handle: number, options: { request: IChatAgentRequest; prompt?: string; history: any[]; metadata?: any }, token: CancellationToken): Promise<IChatSessionItem> {
168
const entry = this._chatSessionItemProviders.get(handle);
169
if (!entry || !entry.provider.provideNewChatSessionItem) {
170
throw new Error(`No provider registered for handle ${handle} or provider does not support creating sessions`);
171
}
172
173
try {
174
const model = await this.getModelForRequest(options.request, entry.extension);
175
const vscodeRequest = typeConvert.ChatAgentRequest.to(
176
revive(options.request),
177
undefined,
178
model,
179
[],
180
new Map(),
181
entry.extension,
182
this._logService);
183
184
const vscodeOptions = {
185
request: vscodeRequest,
186
prompt: options.prompt,
187
history: options.history,
188
metadata: options.metadata
189
};
190
191
const chatSessionItem = await entry.provider.provideNewChatSessionItem(vscodeOptions, token);
192
if (!chatSessionItem || !chatSessionItem.id) {
193
throw new Error('Provider did not create session');
194
}
195
this._sessionMap.set(
196
chatSessionItem.id,
197
chatSessionItem
198
);
199
return this.convertChatSessionItem(chatSessionItem);
200
} catch (error) {
201
this._logService.error(`Error creating chat session: ${error}`);
202
throw error;
203
}
204
}
205
206
async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise<IChatSessionItem[]> {
207
const entry = this._chatSessionItemProviders.get(handle);
208
if (!entry) {
209
this._logService.error(`No provider registered for handle ${handle}`);
210
return [];
211
}
212
213
const sessions = await entry.provider.provideChatSessionItems(token);
214
if (!sessions) {
215
return [];
216
}
217
218
const response: IChatSessionItem[] = [];
219
for (const sessionContent of sessions) {
220
if (sessionContent.id) {
221
this._sessionMap.set(
222
sessionContent.id,
223
sessionContent
224
);
225
response.push(this.convertChatSessionItem(sessionContent));
226
}
227
}
228
return response;
229
}
230
231
private readonly _extHostChatSessions = new Map<string, { readonly sessionObj: ExtHostChatSession; readonly disposeCts: CancellationTokenSource }>();
232
233
async $provideChatSessionContent(handle: number, id: string, token: CancellationToken): Promise<ChatSessionDto> {
234
const provider = this._chatSessionContentProviders.get(handle);
235
if (!provider) {
236
throw new Error(`No provider for handle ${handle}`);
237
}
238
239
const session = await provider.provider.provideChatSessionContent(id, token);
240
241
const sessionDisposables = new DisposableStore();
242
const sessionId = ExtHostChatSessions._sessionHandlePool++;
243
const chatSession = new ExtHostChatSession(session, provider.extension, {
244
sessionId: `${id}.${sessionId}`,
245
requestId: 'ongoing',
246
agentId: id,
247
message: '',
248
variables: { variables: [] },
249
location: ChatAgentLocation.Panel,
250
}, {
251
$handleProgressChunk: (requestId, chunks) => {
252
return this._proxy.$handleProgressChunk(handle, id, requestId, chunks);
253
},
254
$handleAnchorResolve: (requestId, requestHandle, anchor) => {
255
this._proxy.$handleAnchorResolve(handle, id, requestId, requestHandle, anchor);
256
},
257
}, this.commands.converter, sessionDisposables);
258
259
const disposeCts = sessionDisposables.add(new CancellationTokenSource());
260
this._extHostChatSessions.set(`${handle}_${id}`, { sessionObj: chatSession, disposeCts });
261
262
// Call activeResponseCallback immediately for best user experience
263
if (session.activeResponseCallback) {
264
Promise.resolve(session.activeResponseCallback(chatSession.activeResponseStream.apiObject, disposeCts.token)).finally(() => {
265
// complete
266
this._proxy.$handleProgressComplete(handle, id, 'ongoing');
267
});
268
}
269
const { capabilities } = provider;
270
return {
271
id: sessionId + '',
272
hasActiveResponseCallback: !!session.activeResponseCallback,
273
hasRequestHandler: !!session.requestHandler,
274
supportsInterruption: !!capabilities?.supportsInterruptions,
275
history: session.history.map(turn => {
276
if (turn instanceof extHostTypes.ChatRequestTurn) {
277
return { type: 'request' as const, prompt: turn.prompt, participant: turn.participant };
278
} else {
279
const responseTurn = turn as extHostTypes.ChatResponseTurn2;
280
const parts = coalesce(responseTurn.response.map(r => typeConvert.ChatResponsePart.from(r, this.commands.converter, sessionDisposables)));
281
282
return {
283
type: 'response' as const,
284
parts,
285
participant: responseTurn.participant
286
};
287
}
288
})
289
};
290
}
291
292
async $interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise<void> {
293
const key = `${providerHandle}_${sessionId}`;
294
const entry = this._extHostChatSessions.get(key);
295
entry?.disposeCts.cancel();
296
}
297
298
async $disposeChatSessionContent(providerHandle: number, sessionId: string): Promise<void> {
299
const key = `${providerHandle}_${sessionId}`;
300
const entry = this._extHostChatSessions.get(key);
301
if (!entry) {
302
this._logService.warn(`No chat session found for ID: ${key}`);
303
return;
304
}
305
306
entry.disposeCts.cancel();
307
entry.sessionObj.sessionDisposables.dispose();
308
this._extHostChatSessions.delete(key);
309
}
310
311
async $invokeChatSessionRequestHandler(handle: number, id: string, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult> {
312
const entry = this._extHostChatSessions.get(`${handle}_${id}`);
313
if (!entry || !entry.sessionObj.session.requestHandler) {
314
return {};
315
}
316
317
const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, await this.getModelForRequest(request, entry.sessionObj.extension), [], new Map(), entry.sessionObj.extension, this._logService);
318
319
const stream = entry.sessionObj.getActiveRequestStream(request);
320
await entry.sessionObj.session.requestHandler(chatRequest, { history: history }, stream.apiObject, token);
321
322
// TODO: do we need to dispose the stream object?
323
return {};
324
}
325
326
private async getModelForRequest(request: IChatAgentRequest, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {
327
let model: vscode.LanguageModelChat | undefined;
328
if (request.userSelectedModelId) {
329
model = await this._languageModels.getLanguageModelByIdentifier(extension, request.userSelectedModelId);
330
}
331
if (!model) {
332
model = await this._languageModels.getDefaultLanguageModel(extension);
333
if (!model) {
334
throw new Error('Language model unavailable');
335
}
336
}
337
338
return model;
339
}
340
}
341
342