Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostChatSessions.ts
5239 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
/* eslint-disable local/code-no-native-private */
6
7
import type * as vscode from 'vscode';
8
import { coalesce } from '../../../base/common/arrays.js';
9
import { DeferredPromise } from '../../../base/common/async.js';
10
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
11
import { CancellationError } from '../../../base/common/errors.js';
12
import { Emitter, Event } from '../../../base/common/event.js';
13
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
14
import { ResourceMap } from '../../../base/common/map.js';
15
import { MarshalledId } from '../../../base/common/marshallingIds.js';
16
import { basename } from '../../../base/common/resources.js';
17
import { URI, UriComponents } from '../../../base/common/uri.js';
18
import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js';
19
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
20
import { ILogService } from '../../../platform/log/common/log.js';
21
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, IPromptFileVariableEntry, ISymbolVariableEntry, PromptFileVariableKind } from '../../contrib/chat/common/attachments/chatVariableEntries.js';
22
import { ChatSessionStatus, IChatSessionItem, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js';
23
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
24
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js';
25
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
26
import { ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape } from './extHost.protocol.js';
27
import { ChatAgentResponseStream } from './extHostChatAgents2.js';
28
import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';
29
import { ExtHostLanguageModels } from './extHostLanguageModels.js';
30
import { IExtHostRpcService } from './extHostRpcService.js';
31
import * as typeConvert from './extHostTypeConverters.js';
32
import { Diagnostic } from './extHostTypeConverters.js';
33
import * as extHostTypes from './extHostTypes.js';
34
import * as objects from '../../../base/common/objects.js';
35
36
type ChatSessionTiming = vscode.ChatSessionItem['timing'];
37
38
// #region Chat Session Item Controller
39
40
class ChatSessionItemImpl implements vscode.ChatSessionItem {
41
#label: string;
42
#iconPath?: vscode.IconPath;
43
#description?: string | vscode.MarkdownString;
44
#badge?: string | vscode.MarkdownString;
45
#status?: vscode.ChatSessionStatus;
46
#archived?: boolean;
47
#tooltip?: string | vscode.MarkdownString;
48
#timing?: ChatSessionTiming;
49
#changes?: readonly vscode.ChatSessionChangedFile[];
50
#metadata?: { readonly [key: string]: unknown };
51
#onChanged: () => void;
52
53
readonly resource: vscode.Uri;
54
55
constructor(resource: vscode.Uri, label: string, onChanged: () => void) {
56
this.resource = resource;
57
this.#label = label;
58
this.#onChanged = onChanged;
59
}
60
61
get label(): string {
62
return this.#label;
63
}
64
65
set label(value: string) {
66
if (this.#label !== value) {
67
this.#label = value;
68
this.#onChanged();
69
}
70
}
71
72
get iconPath(): vscode.IconPath | undefined {
73
return this.#iconPath;
74
}
75
76
set iconPath(value: vscode.IconPath | undefined) {
77
if (this.#iconPath !== value) {
78
this.#iconPath = value;
79
this.#onChanged();
80
}
81
}
82
83
get description(): string | vscode.MarkdownString | undefined {
84
return this.#description;
85
}
86
87
set description(value: string | vscode.MarkdownString | undefined) {
88
if (this.#description !== value) {
89
this.#description = value;
90
this.#onChanged();
91
}
92
}
93
94
get badge(): string | vscode.MarkdownString | undefined {
95
return this.#badge;
96
}
97
98
set badge(value: string | vscode.MarkdownString | undefined) {
99
if (this.#badge !== value) {
100
this.#badge = value;
101
this.#onChanged();
102
}
103
}
104
105
get status(): vscode.ChatSessionStatus | undefined {
106
return this.#status;
107
}
108
109
set status(value: vscode.ChatSessionStatus | undefined) {
110
if (this.#status !== value) {
111
this.#status = value;
112
this.#onChanged();
113
}
114
}
115
116
get archived(): boolean | undefined {
117
return this.#archived;
118
}
119
120
set archived(value: boolean | undefined) {
121
if (this.#archived !== value) {
122
this.#archived = value;
123
this.#onChanged();
124
}
125
}
126
127
get tooltip(): string | vscode.MarkdownString | undefined {
128
return this.#tooltip;
129
}
130
131
set tooltip(value: string | vscode.MarkdownString | undefined) {
132
if (this.#tooltip !== value) {
133
this.#tooltip = value;
134
this.#onChanged();
135
}
136
}
137
138
get timing(): ChatSessionTiming | undefined {
139
return this.#timing;
140
}
141
142
set timing(value: ChatSessionTiming | undefined) {
143
if (this.#timing !== value) {
144
this.#timing = value;
145
this.#onChanged();
146
}
147
}
148
149
get changes(): readonly vscode.ChatSessionChangedFile[] | undefined {
150
return this.#changes;
151
}
152
153
set changes(value: readonly vscode.ChatSessionChangedFile[] | undefined) {
154
if (this.#changes !== value) {
155
this.#changes = value;
156
this.#onChanged();
157
}
158
}
159
160
get metadata(): { readonly [key: string]: unknown } | undefined {
161
return this.#metadata;
162
}
163
164
set metadata(value: { readonly [key: string]: unknown } | undefined) {
165
if (value !== undefined) {
166
try {
167
JSON.stringify(value);
168
} catch {
169
throw new Error('metadata must be JSON-serializable');
170
}
171
}
172
if (!objects.equals(this.#metadata, value)) {
173
this.#metadata = value;
174
this.#onChanged();
175
}
176
}
177
}
178
179
class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection {
180
readonly #items = new ResourceMap<vscode.ChatSessionItem>();
181
#onItemsChanged: () => void;
182
183
constructor(onItemsChanged: () => void) {
184
this.#onItemsChanged = onItemsChanged;
185
}
186
187
get size(): number {
188
return this.#items.size;
189
}
190
191
replace(items: readonly vscode.ChatSessionItem[]): void {
192
if (items.length === 0 && this.#items.size === 0) {
193
return;
194
}
195
196
this.#items.clear();
197
for (const item of items) {
198
this.#items.set(item.resource, item);
199
}
200
this.#onItemsChanged();
201
}
202
203
forEach(callback: (item: vscode.ChatSessionItem, collection: vscode.ChatSessionItemCollection) => unknown, thisArg?: any): void {
204
for (const [_, item] of this.#items) {
205
callback.call(thisArg, item, this);
206
}
207
}
208
209
add(item: vscode.ChatSessionItem): void {
210
this.#items.set(item.resource, item);
211
this.#onItemsChanged();
212
}
213
214
delete(resource: vscode.Uri): void {
215
this.#items.delete(resource);
216
this.#onItemsChanged();
217
}
218
219
get(resource: vscode.Uri): vscode.ChatSessionItem | undefined {
220
return this.#items.get(resource);
221
}
222
223
[Symbol.iterator](): Iterator<readonly [id: URI, chatSessionItem: vscode.ChatSessionItem]> {
224
return this.#items.entries();
225
}
226
}
227
228
// #endregion
229
230
class ExtHostChatSession {
231
private _stream: ChatAgentResponseStream;
232
// Empty map since question carousel is designed for chat agents, not chat sessions
233
private readonly _pendingCarouselResolvers = new Map<string, Map<string, DeferredPromise<Record<string, unknown> | undefined>>>();
234
235
constructor(
236
public readonly session: vscode.ChatSession,
237
public readonly extension: IExtensionDescription,
238
request: IChatAgentRequest,
239
public readonly proxy: IChatAgentProgressShape,
240
public readonly commandsConverter: CommandsConverter,
241
public readonly sessionDisposables: DisposableStore
242
) {
243
this._stream = new ChatAgentResponseStream(extension, request, proxy, commandsConverter, sessionDisposables, this._pendingCarouselResolvers, CancellationToken.None);
244
}
245
246
get activeResponseStream() {
247
return this._stream;
248
}
249
250
getActiveRequestStream(request: IChatAgentRequest) {
251
return new ChatAgentResponseStream(this.extension, request, this.proxy, this.commandsConverter, this.sessionDisposables, this._pendingCarouselResolvers, CancellationToken.None);
252
}
253
}
254
255
export class ExtHostChatSessions extends Disposable implements ExtHostChatSessionsShape {
256
private static _sessionHandlePool = 0;
257
258
private readonly _proxy: Proxied<MainThreadChatSessionsShape>;
259
260
private _itemProviderHandlePool = 0;
261
private readonly _chatSessionItemProviders = new Map</* handle */ number, {
262
readonly sessionType: string;
263
readonly provider: vscode.ChatSessionItemProvider;
264
readonly extension: IExtensionDescription;
265
readonly disposable: DisposableStore;
266
}>();
267
268
private _itemControllerHandlePool = 0;
269
private readonly _chatSessionItemControllers = new Map</* handle */ number, {
270
readonly sessionType: string;
271
readonly controller: vscode.ChatSessionItemController;
272
readonly extension: IExtensionDescription;
273
readonly disposable: DisposableStore;
274
readonly onDidChangeChatSessionItemStateEmitter: Emitter<vscode.ChatSessionItem>;
275
}>();
276
277
private _contentProviderHandlePool = 0;
278
private readonly _chatSessionContentProviders = new Map</* handle */ number, {
279
readonly provider: vscode.ChatSessionContentProvider;
280
readonly extension: IExtensionDescription;
281
readonly capabilities?: vscode.ChatSessionCapabilities;
282
readonly disposable: DisposableStore;
283
}>();
284
285
/**
286
* Map of uri -> chat session items
287
*
288
* TODO: this isn't cleared/updated properly
289
*/
290
private readonly _sessionItems = new ResourceMap<vscode.ChatSessionItem>();
291
292
/**
293
* Map of uri -> chat sessions infos
294
*/
295
private readonly _extHostChatSessions = new ResourceMap<{ readonly sessionObj: ExtHostChatSession; readonly disposeCts: CancellationTokenSource }>();
296
/**
297
* Store option groups with onSearch callbacks per provider handle
298
*/
299
private readonly _providerOptionGroups = new Map<number, vscode.ChatSessionProviderOptionGroup[]>();
300
301
constructor(
302
private readonly commands: ExtHostCommands,
303
private readonly _languageModels: ExtHostLanguageModels,
304
@IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService,
305
@ILogService private readonly _logService: ILogService,
306
) {
307
super();
308
this._proxy = this._extHostRpc.getProxy(MainContext.MainThreadChatSessions);
309
310
commands.registerArgumentProcessor({
311
processArgument: (arg) => {
312
if (arg && arg.$mid === MarshalledId.AgentSessionContext) {
313
const id = arg.session.resource || arg.sessionId;
314
const sessionContent = this._sessionItems.get(id);
315
if (sessionContent) {
316
return sessionContent;
317
} else {
318
this._logService.warn(`No chat session found for ID: ${id}`);
319
return arg;
320
}
321
}
322
323
return arg;
324
}
325
});
326
}
327
328
registerChatSessionItemProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionItemProvider): vscode.Disposable {
329
const handle = this._itemProviderHandlePool++;
330
const disposables = new DisposableStore();
331
332
this._chatSessionItemProviders.set(handle, { provider, extension, disposable: disposables, sessionType: chatSessionType });
333
this._proxy.$registerChatSessionItemProvider(handle, chatSessionType);
334
if (provider.onDidChangeChatSessionItems) {
335
disposables.add(provider.onDidChangeChatSessionItems(() => {
336
this._logService.trace(`ExtHostChatSessions. Firing $onDidChangeChatSessionItems for ${chatSessionType}`);
337
this._proxy.$onDidChangeChatSessionItems(handle);
338
}));
339
}
340
341
if (provider.onDidCommitChatSessionItem) {
342
disposables.add(provider.onDidCommitChatSessionItem((e) => {
343
const { original, modified } = e;
344
this._proxy.$onDidCommitChatSessionItem(handle, original.resource, modified.resource);
345
}));
346
}
347
348
return {
349
dispose: () => {
350
this._chatSessionItemProviders.delete(handle);
351
disposables.dispose();
352
this._proxy.$unregisterChatSessionItemProvider(handle);
353
}
354
};
355
}
356
357
358
createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: (token: vscode.CancellationToken) => Thenable<void>): vscode.ChatSessionItemController {
359
const controllerHandle = this._itemControllerHandlePool++;
360
const disposables = new DisposableStore();
361
362
let isDisposed = false;
363
let refreshIdPool = 0;
364
let activeRefreshId: number | undefined = undefined;
365
366
const onDidChangeItemsEmitter = disposables.add(new Emitter<void>());
367
const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter<vscode.ChatSessionItem>());
368
369
const notifyItemsChanged = () => {
370
// Suppress updates when a refresh is already happening
371
if (typeof activeRefreshId === 'undefined') {
372
onDidChangeItemsEmitter.fire();
373
}
374
};
375
376
const collection = new ChatSessionItemCollectionImpl(() => {
377
notifyItemsChanged();
378
});
379
380
const controller = Object.freeze<vscode.ChatSessionItemController>({
381
id,
382
refreshHandler: async (refreshToken: CancellationToken) => {
383
if (isDisposed) {
384
throw new Error('ChatSessionItemController has been disposed');
385
}
386
387
const opId = ++refreshIdPool;
388
activeRefreshId = opId;
389
390
try {
391
this._logService.trace(`ExtHostChatSessions. Controller(${id}).refresh()`);
392
await refreshHandler(refreshToken);
393
} finally {
394
if (activeRefreshId === opId) {
395
activeRefreshId = undefined;
396
}
397
}
398
},
399
items: collection,
400
onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event,
401
createChatSessionItem: (resource: vscode.Uri, label: string) => {
402
if (isDisposed) {
403
throw new Error('ChatSessionItemController has been disposed');
404
}
405
406
return new ChatSessionItemImpl(resource, label, () => {
407
// TODO: Optimize to only update the specific item
408
notifyItemsChanged();
409
});
410
},
411
dispose: () => {
412
isDisposed = true;
413
disposables.dispose();
414
},
415
});
416
417
this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id, onDidChangeChatSessionItemStateEmitter });
418
419
// Controllers are implemented using providers on the ext host side for now
420
disposables.add(this.registerChatSessionItemProvider(extension, id, {
421
onDidChangeChatSessionItems: onDidChangeItemsEmitter.event,
422
onDidCommitChatSessionItem: Event.None,
423
provideChatSessionItems: async (token: CancellationToken): Promise<vscode.ChatSessionItem[]> => {
424
await controller.refreshHandler(token);
425
return Array.from(controller.items, x => x[1]);
426
},
427
}));
428
429
disposables.add(toDisposable(() => {
430
this._chatSessionItemControllers.delete(controllerHandle);
431
this._proxy.$unregisterChatSessionItemProvider(controllerHandle);
432
}));
433
434
return controller;
435
}
436
437
registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionScheme: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable {
438
const handle = this._contentProviderHandlePool++;
439
const disposables = new DisposableStore();
440
441
this._chatSessionContentProviders.set(handle, { provider, extension, capabilities, disposable: disposables });
442
this._proxy.$registerChatSessionContentProvider(handle, chatSessionScheme);
443
444
if (provider.onDidChangeChatSessionOptions) {
445
disposables.add(provider.onDidChangeChatSessionOptions(evt => {
446
this._proxy.$onDidChangeChatSessionOptions(handle, evt.resource, evt.updates);
447
}));
448
}
449
450
if (provider.onDidChangeChatSessionProviderOptions) {
451
disposables.add(provider.onDidChangeChatSessionProviderOptions(() => {
452
this._proxy.$onDidChangeChatSessionProviderOptions(handle);
453
}));
454
}
455
456
return new extHostTypes.Disposable(() => {
457
this._chatSessionContentProviders.delete(handle);
458
disposables.dispose();
459
this._proxy.$unregisterChatSessionContentProvider(handle);
460
});
461
}
462
463
private convertChatSessionStatus(status: vscode.ChatSessionStatus | undefined): ChatSessionStatus | undefined {
464
if (status === undefined) {
465
return undefined;
466
}
467
468
switch (status) {
469
case 0: // vscode.ChatSessionStatus.Failed
470
return ChatSessionStatus.Failed;
471
case 1: // vscode.ChatSessionStatus.Completed
472
return ChatSessionStatus.Completed;
473
case 2: // vscode.ChatSessionStatus.InProgress
474
return ChatSessionStatus.InProgress;
475
// Need to support NeedsInput status if we ever export it to the extension API
476
default:
477
return undefined;
478
}
479
}
480
481
private convertChatSessionItem(sessionContent: vscode.ChatSessionItem): IChatSessionItem {
482
// Support both new (created, lastRequestStarted, lastRequestEnded) and old (startTime, endTime) timing properties
483
const timing = sessionContent.timing;
484
const created = timing?.created ?? timing?.startTime ?? 0;
485
const lastRequestStarted = timing?.lastRequestStarted ?? timing?.startTime;
486
const lastRequestEnded = timing?.lastRequestEnded ?? timing?.endTime;
487
488
return {
489
resource: sessionContent.resource,
490
label: sessionContent.label,
491
description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined,
492
badge: sessionContent.badge ? typeConvert.MarkdownString.from(sessionContent.badge) : undefined,
493
status: this.convertChatSessionStatus(sessionContent.status),
494
archived: sessionContent.archived,
495
tooltip: typeConvert.MarkdownString.fromStrict(sessionContent.tooltip),
496
timing: {
497
created,
498
lastRequestStarted,
499
lastRequestEnded,
500
},
501
changes: sessionContent.changes instanceof Array ? sessionContent.changes : undefined,
502
metadata: sessionContent.metadata,
503
};
504
}
505
506
async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise<IChatSessionItem[]> {
507
const itemProvider = this._chatSessionItemProviders.get(handle);
508
if (!itemProvider) {
509
this._logService.error(`No provider registered for handle ${handle}`);
510
return [];
511
}
512
513
this._logService.trace(`ExtHostChatSessions:$provideChatSessionItems(${itemProvider.sessionType})`);
514
const items = await itemProvider.provider.provideChatSessionItems(token) ?? [];
515
if (token.isCancellationRequested) {
516
return [];
517
}
518
519
const response: IChatSessionItem[] = [];
520
for (const sessionContent of items) {
521
this._sessionItems.set(sessionContent.resource, sessionContent);
522
response.push(this.convertChatSessionItem(sessionContent));
523
}
524
return response;
525
}
526
527
async $provideChatSessionContent(handle: number, sessionResourceComponents: UriComponents, token: CancellationToken): Promise<ChatSessionDto> {
528
const provider = this._chatSessionContentProviders.get(handle);
529
if (!provider) {
530
throw new Error(`No provider for handle ${handle}`);
531
}
532
533
const sessionResource = URI.revive(sessionResourceComponents);
534
535
const session = await provider.provider.provideChatSessionContent(sessionResource, token);
536
if (token.isCancellationRequested) {
537
throw new CancellationError();
538
}
539
540
const sessionDisposables = new DisposableStore();
541
const sessionId = ExtHostChatSessions._sessionHandlePool++;
542
const id = sessionResource.toString();
543
const chatSession = new ExtHostChatSession(session, provider.extension, {
544
sessionResource,
545
requestId: 'ongoing',
546
agentId: id,
547
message: '',
548
variables: { variables: [] },
549
location: ChatAgentLocation.Chat,
550
}, {
551
$handleProgressChunk: (requestId, chunks) => {
552
return this._proxy.$handleProgressChunk(handle, sessionResource, requestId, chunks);
553
},
554
$handleAnchorResolve: (requestId, requestHandle, anchor) => {
555
this._proxy.$handleAnchorResolve(handle, sessionResource, requestId, requestHandle, anchor);
556
},
557
}, this.commands.converter, sessionDisposables);
558
559
const disposeCts = sessionDisposables.add(new CancellationTokenSource());
560
this._extHostChatSessions.set(sessionResource, { sessionObj: chatSession, disposeCts });
561
562
// Call activeResponseCallback immediately for best user experience
563
if (session.activeResponseCallback) {
564
Promise.resolve(session.activeResponseCallback(chatSession.activeResponseStream.apiObject, disposeCts.token)).finally(() => {
565
// complete
566
this._proxy.$handleProgressComplete(handle, sessionResource, 'ongoing');
567
});
568
}
569
const { capabilities } = provider;
570
return {
571
id: sessionId + '',
572
resource: URI.revive(sessionResource),
573
hasActiveResponseCallback: !!session.activeResponseCallback,
574
hasRequestHandler: !!session.requestHandler,
575
supportsInterruption: !!capabilities?.supportsInterruptions,
576
options: session.options,
577
history: session.history.map(turn => {
578
if (turn instanceof extHostTypes.ChatRequestTurn) {
579
return this.convertRequestTurn(turn);
580
} else {
581
return this.convertResponseTurn(turn as extHostTypes.ChatResponseTurn2, sessionDisposables);
582
}
583
})
584
};
585
}
586
587
async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>, token: CancellationToken): Promise<void> {
588
const sessionResource = URI.revive(sessionResourceComponents);
589
const provider = this._chatSessionContentProviders.get(handle);
590
if (!provider) {
591
this._logService.warn(`No provider for handle ${handle}`);
592
return;
593
}
594
595
if (!provider.provider.provideHandleOptionsChange) {
596
this._logService.debug(`Provider for handle ${handle} does not implement provideHandleOptionsChange`);
597
return;
598
}
599
600
try {
601
const updatesToSend = updates.map(update => ({
602
optionId: update.optionId,
603
value: update.value === undefined ? undefined : (typeof update.value === 'string' ? update.value : update.value.id)
604
}));
605
await provider.provider.provideHandleOptionsChange(sessionResource, updatesToSend, token);
606
} catch (error) {
607
this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionResource ${sessionResource}:`, error);
608
}
609
}
610
611
async $provideChatSessionProviderOptions(handle: number, token: CancellationToken): Promise<IChatSessionProviderOptions | undefined> {
612
const entry = this._chatSessionContentProviders.get(handle);
613
if (!entry) {
614
this._logService.warn(`No provider for handle ${handle} when requesting chat session options`);
615
return;
616
}
617
618
const provider = entry.provider;
619
if (!provider.provideChatSessionProviderOptions) {
620
return;
621
}
622
623
try {
624
const { optionGroups } = await provider.provideChatSessionProviderOptions(token);
625
if (!optionGroups) {
626
return;
627
}
628
this._providerOptionGroups.set(handle, optionGroups);
629
return {
630
optionGroups,
631
};
632
} catch (error) {
633
this._logService.error(`Error calling provideChatSessionProviderOptions for handle ${handle}:`, error);
634
return;
635
}
636
}
637
638
async $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise<void> {
639
const entry = this._extHostChatSessions.get(URI.revive(sessionResource));
640
entry?.disposeCts.cancel();
641
}
642
643
async $disposeChatSessionContent(providerHandle: number, sessionResource: UriComponents): Promise<void> {
644
const entry = this._extHostChatSessions.get(URI.revive(sessionResource));
645
if (!entry) {
646
this._logService.warn(`No chat session found for resource: ${sessionResource}`);
647
return;
648
}
649
650
entry.disposeCts.cancel();
651
entry.sessionObj.sessionDisposables.dispose();
652
this._extHostChatSessions.delete(URI.revive(sessionResource));
653
}
654
655
async $invokeChatSessionRequestHandler(handle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult> {
656
const entry = this._extHostChatSessions.get(URI.revive(sessionResource));
657
if (!entry || !entry.sessionObj.session.requestHandler) {
658
return {};
659
}
660
661
const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, await this.getModelForRequest(request, entry.sessionObj.extension), [], new Map(), entry.sessionObj.extension, this._logService);
662
663
const stream = entry.sessionObj.getActiveRequestStream(request);
664
await entry.sessionObj.session.requestHandler(chatRequest, { history, yieldRequested: false }, stream.apiObject, token);
665
666
// TODO: do we need to dispose the stream object?
667
return {};
668
}
669
670
private async getModelForRequest(request: IChatAgentRequest, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {
671
let model: vscode.LanguageModelChat | undefined;
672
if (request.userSelectedModelId) {
673
model = await this._languageModels.getLanguageModelByIdentifier(extension, request.userSelectedModelId);
674
}
675
if (!model) {
676
model = await this._languageModels.getDefaultLanguageModel(extension);
677
if (!model) {
678
throw new Error('Language model unavailable');
679
}
680
}
681
682
return model;
683
}
684
685
private convertRequestTurn(turn: extHostTypes.ChatRequestTurn) {
686
const variables = turn.references.map(ref => this.convertReferenceToVariable(ref));
687
return {
688
type: 'request' as const,
689
id: turn.id,
690
prompt: turn.prompt,
691
participant: turn.participant,
692
command: turn.command,
693
variableData: variables.length > 0 ? { variables } : undefined
694
};
695
}
696
697
private convertReferenceToVariable(ref: vscode.ChatPromptReference): IChatRequestVariableEntry {
698
const value = ref.value && typeof ref.value === 'object' && 'uri' in ref.value && 'range' in ref.value
699
? typeConvert.Location.from(ref.value as vscode.Location)
700
: ref.value;
701
const range = ref.range ? { start: ref.range[0], endExclusive: ref.range[1] } : undefined;
702
703
if (value && value instanceof extHostTypes.ChatReferenceDiagnostic && Array.isArray(value.diagnostics) && value.diagnostics.length && value.diagnostics[0][1].length) {
704
const marker = Diagnostic.from(value.diagnostics[0][1][0]);
705
const refValue: IDiagnosticVariableEntryFilterData = {
706
filterRange: { startLineNumber: marker.startLineNumber, startColumn: marker.startColumn, endLineNumber: marker.endLineNumber, endColumn: marker.endColumn },
707
filterSeverity: marker.severity,
708
filterUri: value.diagnostics[0][0],
709
problemMessage: value.diagnostics[0][1][0].message
710
};
711
return IDiagnosticVariableEntryFilterData.toEntry(refValue);
712
}
713
714
if (extHostTypes.Location.isLocation(ref.value) && ref.name.startsWith(`sym:`)) {
715
const loc = typeConvert.Location.from(ref.value);
716
return {
717
id: ref.id,
718
name: ref.name,
719
fullName: ref.name.substring(4),
720
value: { uri: ref.value.uri, range: loc.range },
721
// We never send this information to extensions, so default to Property
722
symbolKind: SymbolKind.Property,
723
// We never send this information to extensions, so default to Property
724
icon: SymbolKinds.toIcon(SymbolKind.Property),
725
kind: 'symbol',
726
range,
727
} satisfies ISymbolVariableEntry;
728
}
729
730
if (URI.isUri(value) && ref.name.startsWith(`prompt:`) &&
731
ref.id.startsWith(PromptFileVariableKind.PromptFile) &&
732
ref.id.endsWith(value.toString())) {
733
return {
734
id: ref.id,
735
name: `prompt:${basename(value)}`,
736
value,
737
kind: 'promptFile',
738
modelDescription: 'Prompt instructions file',
739
isRoot: true,
740
automaticallyAdded: false,
741
range,
742
} satisfies IPromptFileVariableEntry;
743
}
744
745
const isFile = URI.isUri(value) || (value && typeof value === 'object' && 'uri' in value);
746
const isFolder = isFile && URI.isUri(value) && value.path.endsWith('/');
747
return {
748
id: ref.id,
749
name: ref.name,
750
value,
751
modelDescription: ref.modelDescription,
752
range,
753
kind: isFolder ? 'directory' as const : isFile ? 'file' as const : 'generic' as const
754
};
755
}
756
757
private convertResponseTurn(turn: extHostTypes.ChatResponseTurn2, sessionDisposables: DisposableStore) {
758
const parts = coalesce(turn.response.map(r => typeConvert.ChatResponsePart.from(r, this.commands.converter, sessionDisposables)));
759
return {
760
type: 'response' as const,
761
parts,
762
participant: turn.participant
763
};
764
}
765
766
async $invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise<IChatSessionProviderOptionItem[]> {
767
const optionGroups = this._providerOptionGroups.get(providerHandle);
768
if (!optionGroups) {
769
this._logService.warn(`No option groups found for provider handle ${providerHandle}`);
770
return [];
771
}
772
773
const group = optionGroups.find((g: vscode.ChatSessionProviderOptionGroup) => g.id === optionGroupId);
774
if (!group || !group.onSearch) {
775
this._logService.warn(`No onSearch callback found for option group ${optionGroupId}`);
776
return [];
777
}
778
779
try {
780
const results = await group.onSearch(query, token);
781
return results ?? [];
782
} catch (error) {
783
this._logService.error(`Error calling onSearch for option group ${optionGroupId}:`, error);
784
return [];
785
}
786
}
787
788
$onDidChangeChatSessionItemState(controllerHandle: number, sessionResourceComponents: UriComponents, archived: boolean): void {
789
const controllerData = this._chatSessionItemControllers.get(controllerHandle);
790
if (!controllerData) {
791
this._logService.warn(`No controller found for handle ${controllerHandle}`);
792
return;
793
}
794
795
const sessionResource = URI.revive(sessionResourceComponents);
796
const item = controllerData.controller.items.get(sessionResource);
797
if (!item) {
798
this._logService.warn(`No item found for session resource ${sessionResource.toString()}`);
799
return;
800
}
801
802
item.archived = archived;
803
controllerData.onDidChangeChatSessionItemStateEmitter.fire(item);
804
}
805
}
806
807