Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadChatSessions.ts
5226 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 { raceCancellationError } from '../../../base/common/async.js';
7
import { CancellationToken } from '../../../base/common/cancellation.js';
8
import { Emitter, Event } from '../../../base/common/event.js';
9
import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';
10
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
11
import { ResourceMap } from '../../../base/common/map.js';
12
import { revive } from '../../../base/common/marshalling.js';
13
import { autorun, IObservable, observableValue } from '../../../base/common/observable.js';
14
import { isEqual } from '../../../base/common/resources.js';
15
import { URI, UriComponents } from '../../../base/common/uri.js';
16
import { localize } from '../../../nls.js';
17
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
18
import { ILogService } from '../../../platform/log/common/log.js';
19
import { hasValidDiff, IAgentSession } from '../../contrib/chat/browser/agentSessions/agentSessionsModel.js';
20
import { IAgentSessionsService } from '../../contrib/chat/browser/agentSessions/agentSessionsService.js';
21
import { IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js';
22
import { IChatEditorOptions } from '../../contrib/chat/browser/widgetHosts/editor/chatEditor.js';
23
import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/chatEditorInput.js';
24
import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js';
25
import { awaitStatsForSession } from '../../contrib/chat/common/chat.js';
26
import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js';
27
import { ChatSessionStatus, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js';
28
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
29
import { IChatModel } from '../../contrib/chat/common/model/chatModel.js';
30
import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js';
31
import { IChatTodoListService } from '../../contrib/chat/common/tools/chatTodoListService.js';
32
import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';
33
import { IEditorService } from '../../services/editor/common/editorService.js';
34
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
35
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
36
import { ExtHostChatSessionsShape, ExtHostContext, IChatProgressDto, IChatSessionHistoryItemDto, MainContext, MainThreadChatSessionsShape } from '../common/extHost.protocol.js';
37
38
export class ObservableChatSession extends Disposable implements IChatSession {
39
40
readonly sessionResource: URI;
41
readonly providerHandle: number;
42
readonly history: Array<IChatSessionHistoryItem>;
43
private _options?: Record<string, string | IChatSessionProviderOptionItem>;
44
public get options(): Record<string, string | IChatSessionProviderOptionItem> | undefined {
45
return this._options;
46
}
47
private readonly _progressObservable = observableValue<IChatProgress[]>(this, []);
48
private readonly _isCompleteObservable = observableValue<boolean>(this, false);
49
50
private readonly _onWillDispose = new Emitter<void>();
51
readonly onWillDispose = this._onWillDispose.event;
52
53
private readonly _pendingProgressChunks = new Map<string, IChatProgress[]>();
54
private _isInitialized = false;
55
private _interruptionWasCanceled = false;
56
private _disposalPending = false;
57
58
private _initializationPromise?: Promise<void>;
59
60
interruptActiveResponseCallback?: () => Promise<boolean>;
61
requestHandler?: (
62
request: IChatAgentRequest,
63
progress: (progress: IChatProgress[]) => void,
64
history: any[],
65
token: CancellationToken
66
) => Promise<void>;
67
68
private readonly _proxy: ExtHostChatSessionsShape;
69
private readonly _providerHandle: number;
70
private readonly _logService: ILogService;
71
private readonly _dialogService: IDialogService;
72
73
get progressObs(): IObservable<IChatProgress[]> {
74
return this._progressObservable;
75
}
76
77
get isCompleteObs(): IObservable<boolean> {
78
return this._isCompleteObservable;
79
}
80
81
constructor(
82
resource: URI,
83
providerHandle: number,
84
proxy: ExtHostChatSessionsShape,
85
logService: ILogService,
86
dialogService: IDialogService
87
) {
88
super();
89
90
this.sessionResource = resource;
91
this.providerHandle = providerHandle;
92
this.history = [];
93
this._proxy = proxy;
94
this._providerHandle = providerHandle;
95
this._logService = logService;
96
this._dialogService = dialogService;
97
}
98
99
initialize(token: CancellationToken): Promise<void> {
100
if (!this._initializationPromise) {
101
this._initializationPromise = this._doInitializeContent(token);
102
}
103
return this._initializationPromise;
104
}
105
106
private async _doInitializeContent(token: CancellationToken): Promise<void> {
107
try {
108
const sessionContent = await raceCancellationError(
109
this._proxy.$provideChatSessionContent(this._providerHandle, this.sessionResource, token),
110
token
111
);
112
113
this._options = sessionContent.options;
114
this.history.length = 0;
115
this.history.push(...sessionContent.history.map((turn: IChatSessionHistoryItemDto) => {
116
if (turn.type === 'request') {
117
const variables = turn.variableData?.variables.map(v => {
118
const entry = {
119
...v,
120
value: revive(v.value)
121
};
122
return entry as IChatRequestVariableEntry;
123
});
124
125
return {
126
type: 'request' as const,
127
prompt: turn.prompt,
128
participant: turn.participant,
129
command: turn.command,
130
variableData: variables ? { variables } : undefined,
131
id: turn.id
132
};
133
}
134
135
return {
136
type: 'response' as const,
137
parts: turn.parts.map((part: IChatProgressDto) => revive(part) as IChatProgress),
138
participant: turn.participant
139
};
140
}));
141
142
if (sessionContent.hasActiveResponseCallback && !this.interruptActiveResponseCallback) {
143
this.interruptActiveResponseCallback = async () => {
144
const confirmInterrupt = () => {
145
if (this._disposalPending) {
146
this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionResource);
147
this._disposalPending = false;
148
}
149
this._proxy.$interruptChatSessionActiveResponse(this._providerHandle, this.sessionResource, 'ongoing');
150
return true;
151
};
152
153
if (sessionContent.supportsInterruption) {
154
// If the session supports hot reload, interrupt without confirmation
155
return confirmInterrupt();
156
}
157
158
// Prompt the user to confirm interruption
159
return this._dialogService.confirm({
160
message: localize('interruptActiveResponse', 'Are you sure you want to interrupt the active session?')
161
}).then(confirmed => {
162
if (confirmed.confirmed) {
163
// User confirmed interruption - dispose the session content on extension host
164
return confirmInterrupt();
165
} else {
166
// When user cancels the interruption, fire an empty progress message to keep the session alive
167
// This matches the behavior of the old implementation
168
this._addProgress([{
169
kind: 'progressMessage',
170
content: { value: '', isTrusted: false }
171
}]);
172
// Set flag to prevent completion when extension host calls handleProgressComplete
173
this._interruptionWasCanceled = true;
174
// User canceled interruption - cancel the deferred disposal
175
if (this._disposalPending) {
176
this._logService.info(`Canceling deferred disposal for session ${this.sessionResource} (user canceled interruption)`);
177
this._disposalPending = false;
178
}
179
return false;
180
}
181
});
182
};
183
}
184
185
if (sessionContent.hasRequestHandler && !this.requestHandler) {
186
this.requestHandler = async (
187
request: IChatAgentRequest,
188
progress: (progress: IChatProgress[]) => void,
189
history: any[],
190
token: CancellationToken
191
) => {
192
// Clear previous progress and mark as active
193
this._progressObservable.set([], undefined);
194
this._isCompleteObservable.set(false, undefined);
195
196
// Set up reactive progress observation before starting the request
197
let lastProgressLength = 0;
198
const progressDisposable = autorun(reader => {
199
const progressArray = this._progressObservable.read(reader);
200
const isComplete = this._isCompleteObservable.read(reader);
201
202
if (progressArray.length > lastProgressLength) {
203
const newProgress = progressArray.slice(lastProgressLength);
204
progress(newProgress);
205
lastProgressLength = progressArray.length;
206
}
207
208
if (isComplete) {
209
progressDisposable.dispose();
210
}
211
});
212
213
try {
214
await this._proxy.$invokeChatSessionRequestHandler(this._providerHandle, this.sessionResource, request, history, token);
215
216
// Only mark as complete if there's no active response callback
217
// Sessions with active response callbacks should only complete when explicitly told to via handleProgressComplete
218
if (!this._isCompleteObservable.get() && !this.interruptActiveResponseCallback) {
219
this._markComplete();
220
}
221
} catch (error) {
222
const errorProgress: IChatProgress = {
223
kind: 'progressMessage',
224
content: { value: `Error: ${error instanceof Error ? error.message : String(error)}`, isTrusted: false }
225
};
226
227
this._addProgress([errorProgress]);
228
this._markComplete();
229
throw error;
230
} finally {
231
// Ensure progress observation is cleaned up
232
progressDisposable.dispose();
233
}
234
};
235
}
236
237
this._isInitialized = true;
238
239
// Process any pending progress chunks
240
const hasActiveResponse = sessionContent.hasActiveResponseCallback;
241
const hasRequestHandler = sessionContent.hasRequestHandler;
242
const hasAnyCapability = hasActiveResponse || hasRequestHandler;
243
244
for (const [requestId, chunks] of this._pendingProgressChunks) {
245
this._logService.debug(`Processing ${chunks.length} pending progress chunks for session ${this.sessionResource}, requestId ${requestId}`);
246
this._addProgress(chunks);
247
}
248
this._pendingProgressChunks.clear();
249
250
// If session has no active response callback and no request handler, mark it as complete
251
if (!hasAnyCapability) {
252
this._isCompleteObservable.set(true, undefined);
253
}
254
255
} catch (error) {
256
this._logService.error(`Failed to initialize chat session ${this.sessionResource}:`, error);
257
throw error;
258
}
259
}
260
261
/**
262
* Handle progress chunks coming from the extension host.
263
* If the session is not initialized yet, the chunks will be queued.
264
*/
265
handleProgressChunk(requestId: string, progress: IChatProgress[]): void {
266
if (!this._isInitialized) {
267
const existing = this._pendingProgressChunks.get(requestId) || [];
268
this._pendingProgressChunks.set(requestId, [...existing, ...progress]);
269
this._logService.debug(`Queuing ${progress.length} progress chunks for session ${this.sessionResource}, requestId ${requestId} (session not initialized)`);
270
return;
271
}
272
273
this._addProgress(progress);
274
}
275
276
/**
277
* Handle progress completion from the extension host.
278
*/
279
handleProgressComplete(requestId: string): void {
280
// Clean up any pending chunks for this request
281
this._pendingProgressChunks.delete(requestId);
282
283
if (this._isInitialized) {
284
// Don't mark as complete if user canceled the interruption
285
if (!this._interruptionWasCanceled) {
286
this._markComplete();
287
} else {
288
// Reset the flag and don't mark as complete
289
this._interruptionWasCanceled = false;
290
}
291
}
292
}
293
294
private _addProgress(progress: IChatProgress[]): void {
295
const currentProgress = this._progressObservable.get();
296
this._progressObservable.set([...currentProgress, ...progress], undefined);
297
}
298
299
private _markComplete(): void {
300
if (!this._isCompleteObservable.get()) {
301
this._isCompleteObservable.set(true, undefined);
302
}
303
}
304
305
override dispose(): void {
306
this._onWillDispose.fire();
307
this._onWillDispose.dispose();
308
this._pendingProgressChunks.clear();
309
310
// If this session has an active response callback and disposal is happening,
311
// defer the actual session content disposal until we know the user's choice
312
if (this.interruptActiveResponseCallback && !this._interruptionWasCanceled) {
313
this._disposalPending = true;
314
// The actual disposal will happen in the interruption callback based on user's choice
315
} else {
316
// No active response callback or user already canceled interruption - dispose immediately
317
this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionResource);
318
}
319
super.dispose();
320
}
321
}
322
323
@extHostNamedCustomer(MainContext.MainThreadChatSessions)
324
export class MainThreadChatSessions extends Disposable implements MainThreadChatSessionsShape {
325
private readonly _itemProvidersRegistrations = this._register(new DisposableMap<number, IDisposable & {
326
readonly provider: IChatSessionItemProvider;
327
readonly onDidChangeItems: Emitter<void>;
328
}>());
329
private readonly _contentProvidersRegistrations = this._register(new DisposableMap<number>());
330
private readonly _sessionTypeToHandle = new Map<string, number>();
331
332
private readonly _activeSessions = new ResourceMap<ObservableChatSession>();
333
private readonly _sessionDisposables = new ResourceMap<IDisposable>();
334
335
private readonly _proxy: ExtHostChatSessionsShape;
336
337
constructor(
338
private readonly _extHostContext: IExtHostContext,
339
@IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService,
340
@IChatSessionsService private readonly _chatSessionsService: IChatSessionsService,
341
@IChatService private readonly _chatService: IChatService,
342
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
343
@IChatTodoListService private readonly _chatTodoListService: IChatTodoListService,
344
@IDialogService private readonly _dialogService: IDialogService,
345
@IEditorService private readonly _editorService: IEditorService,
346
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
347
@ILogService private readonly _logService: ILogService,
348
) {
349
super();
350
351
this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions);
352
353
this._register(this._chatSessionsService.onRequestNotifyExtension(({ sessionResource, updates, waitUntil }) => {
354
const handle = this._getHandleForSessionType(sessionResource.scheme);
355
this._logService.trace(`[MainThreadChatSessions] onRequestNotifyExtension received: scheme '${sessionResource.scheme}', handle ${handle}, ${updates.length} update(s)`);
356
if (handle !== undefined) {
357
waitUntil(this.notifyOptionsChange(handle, sessionResource, updates));
358
} else {
359
this._logService.warn(`[MainThreadChatSessions] Cannot notify option change for scheme '${sessionResource.scheme}': no provider registered. Registered schemes: [${Array.from(this._sessionTypeToHandle.keys()).join(', ')}]`);
360
}
361
}));
362
363
this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => {
364
for (const [handle, { provider }] of this._itemProvidersRegistrations) {
365
if (provider.chatSessionType === session.providerType) {
366
this._proxy.$onDidChangeChatSessionItemState(handle, session.resource, session.isArchived());
367
}
368
}
369
}));
370
}
371
372
private _getHandleForSessionType(chatSessionType: string): number | undefined {
373
return this._sessionTypeToHandle.get(chatSessionType);
374
}
375
376
$registerChatSessionItemProvider(handle: number, chatSessionType: string): void {
377
// Register the provider handle - this tracks that a provider exists
378
const disposables = new DisposableStore();
379
const changeEmitter = disposables.add(new Emitter<void>());
380
const provider: IChatSessionItemProvider = {
381
chatSessionType,
382
onDidChangeChatSessionItems: Event.debounce(changeEmitter.event, (_, e) => e, 200),
383
provideChatSessionItems: (token) => this._provideChatSessionItems(handle, token),
384
};
385
disposables.add(this._chatSessionsService.registerChatSessionItemProvider(provider));
386
387
this._itemProvidersRegistrations.set(handle, {
388
dispose: () => disposables.dispose(),
389
provider,
390
onDidChangeItems: changeEmitter,
391
});
392
393
disposables.add(this._chatSessionsService.registerChatModelChangeListeners(
394
this._chatService,
395
chatSessionType,
396
() => changeEmitter.fire()
397
));
398
}
399
400
$onDidChangeChatSessionItems(handle: number): void {
401
this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire();
402
}
403
404
$onDidChangeChatSessionOptions(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string }>): void {
405
const sessionResource = URI.revive(sessionResourceComponents);
406
407
this._chatSessionsService.notifySessionOptionsChange(sessionResource, updates);
408
}
409
410
async $onDidCommitChatSessionItem(handle: number, originalComponents: UriComponents, modifiedCompoennts: UriComponents): Promise<void> {
411
const originalResource = URI.revive(originalComponents);
412
const modifiedResource = URI.revive(modifiedCompoennts);
413
414
this._logService.trace(`$onDidCommitChatSessionItem: handle(${handle}), original(${originalResource}), modified(${modifiedResource})`);
415
const chatSessionType = this._itemProvidersRegistrations.get(handle)?.provider.chatSessionType;
416
if (!chatSessionType) {
417
this._logService.error(`No chat session type found for provider handle ${handle}`);
418
return;
419
}
420
421
const originalEditor = this._editorService.editors.find(editor => editor.resource?.toString() === originalResource.toString());
422
const originalModel = this._chatService.getActiveSessionReference(originalResource);
423
const contribution = this._chatSessionsService.getAllChatSessionContributions().find(c => c.type === chatSessionType);
424
425
try {
426
427
// Migrate todos from old session to new session
428
this._chatTodoListService.migrateTodos(originalResource, modifiedResource);
429
430
// Find the group containing the original editor
431
const originalGroup =
432
this.editorGroupService.groups.find(group => group.editors.some(editor => isEqual(editor.resource, originalResource)))
433
?? this.editorGroupService.activeGroup;
434
435
const options: IChatEditorOptions = {
436
title: {
437
preferred: originalEditor?.getName() || undefined,
438
fallback: localize('chatEditorContributionName', "{0}", contribution?.displayName),
439
}
440
};
441
442
// Prefetch the chat session content to make the subsequent editor swap quick
443
const newSession = await this._chatSessionsService.getOrCreateChatSession(
444
URI.revive(modifiedResource),
445
CancellationToken.None,
446
);
447
448
if (originalEditor) {
449
newSession.transferredState = originalEditor instanceof ChatEditorInput
450
? { editingSession: originalEditor.transferOutEditingSession(), inputState: originalModel?.object?.inputModel.toJSON() }
451
: undefined;
452
453
await this._editorService.replaceEditors([{
454
editor: originalEditor,
455
replacement: {
456
resource: modifiedResource,
457
options,
458
},
459
}], originalGroup);
460
return;
461
}
462
463
// If chat editor is in the side panel, then those are not listed as editors.
464
// In that case we need to transfer editing session using the original model.
465
if (originalModel) {
466
newSession.transferredState = {
467
editingSession: originalModel.object.editingSession,
468
inputState: originalModel.object.inputModel.toJSON()
469
};
470
}
471
472
const chatViewWidget = this._chatWidgetService.getWidgetBySessionResource(originalResource);
473
if (chatViewWidget && isIChatViewViewContext(chatViewWidget.viewContext)) {
474
await this._chatWidgetService.openSession(modifiedResource, undefined, { preserveFocus: true });
475
} else {
476
// Loading the session to ensure the session is created and editing session is transferred.
477
const ref = await this._chatService.loadSessionForResource(modifiedResource, ChatAgentLocation.Chat, CancellationToken.None);
478
ref?.dispose();
479
}
480
} finally {
481
originalModel?.dispose();
482
}
483
}
484
485
private async _provideChatSessionItems(handle: number, token: CancellationToken): Promise<IChatSessionItem[]> {
486
try {
487
// Get all results as an array from the RPC call
488
const sessions = await this._proxy.$provideChatSessionItems(handle, token);
489
return Promise.all(sessions.map(async session => {
490
const uri = URI.revive(session.resource);
491
const model = this._chatService.getSession(uri);
492
if (model) {
493
session = await this.handleSessionModelOverrides(model, session);
494
}
495
496
// We can still get stats if there is no model or if fetching from model failed
497
if (!session.changes || !model) {
498
const stats = (await this._chatService.getMetadataForSession(uri))?.stats;
499
// TODO: we shouldn't be converting this, the types should match
500
const diffs: IAgentSession['changes'] = {
501
files: stats?.fileCount || 0,
502
insertions: stats?.added || 0,
503
deletions: stats?.removed || 0
504
};
505
if (hasValidDiff(diffs)) {
506
session.changes = diffs;
507
}
508
}
509
510
return {
511
...session,
512
changes: revive(session.changes),
513
resource: uri,
514
iconPath: session.iconPath,
515
tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined,
516
archived: session.archived,
517
} satisfies IChatSessionItem;
518
}));
519
} catch (error) {
520
this._logService.error('Error providing chat sessions:', error);
521
}
522
return [];
523
}
524
525
private async handleSessionModelOverrides(model: IChatModel, session: Dto<IChatSessionItem>): Promise<Dto<IChatSessionItem>> {
526
// Override desciription if there's an in-progress count
527
const inProgress = model.getRequests().filter(r => r.response && !r.response.isComplete);
528
if (inProgress.length) {
529
session.description = this._chatSessionsService.getInProgressSessionDescription(model);
530
}
531
532
// Override changes
533
// TODO: @osortega we don't really use statistics anymore, we need to clarify that in the API
534
if (!(session.changes instanceof Array)) {
535
const modelStats = await awaitStatsForSession(model);
536
if (modelStats) {
537
session.changes = {
538
files: modelStats.fileCount,
539
insertions: modelStats.added,
540
deletions: modelStats.removed
541
};
542
}
543
}
544
545
// Override status if the models needs input
546
if (model.lastRequest?.response?.state === ResponseModelState.NeedsInput) {
547
session.status = ChatSessionStatus.NeedsInput;
548
}
549
550
return session;
551
}
552
553
private async _provideChatSessionContent(providerHandle: number, sessionResource: URI, token: CancellationToken): Promise<IChatSession> {
554
let session = this._activeSessions.get(sessionResource);
555
556
if (!session) {
557
session = new ObservableChatSession(
558
sessionResource,
559
providerHandle,
560
this._proxy,
561
this._logService,
562
this._dialogService
563
);
564
this._activeSessions.set(sessionResource, session);
565
const disposable = session.onWillDispose(() => {
566
this._activeSessions.delete(sessionResource);
567
this._sessionDisposables.get(sessionResource)?.dispose();
568
this._sessionDisposables.delete(sessionResource);
569
});
570
this._sessionDisposables.set(sessionResource, disposable);
571
}
572
573
try {
574
await session.initialize(token);
575
if (session.options) {
576
for (const [_, handle] of this._sessionTypeToHandle) {
577
if (handle === providerHandle) {
578
for (const [optionId, value] of Object.entries(session.options)) {
579
this._chatSessionsService.setSessionOption(sessionResource, optionId, value);
580
}
581
break;
582
}
583
}
584
}
585
return session;
586
} catch (error) {
587
session.dispose();
588
this._logService.error(`Error providing chat session content for handle ${providerHandle} and resource ${sessionResource.toString()}:`, error);
589
throw error;
590
}
591
}
592
593
$unregisterChatSessionItemProvider(handle: number): void {
594
this._itemProvidersRegistrations.deleteAndDispose(handle);
595
}
596
597
$registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void {
598
const provider: IChatSessionContentProvider = {
599
provideChatSessionContent: (resource, token) => this._provideChatSessionContent(handle, resource, token)
600
};
601
602
this._sessionTypeToHandle.set(chatSessionScheme, handle);
603
this._contentProvidersRegistrations.set(handle, this._chatSessionsService.registerChatSessionContentProvider(chatSessionScheme, provider));
604
this._refreshProviderOptions(handle, chatSessionScheme);
605
}
606
607
$unregisterChatSessionContentProvider(handle: number): void {
608
this._contentProvidersRegistrations.deleteAndDispose(handle);
609
for (const [sessionType, h] of this._sessionTypeToHandle) {
610
if (h === handle) {
611
this._sessionTypeToHandle.delete(sessionType);
612
break;
613
}
614
}
615
616
// dispose all sessions from this provider and clean up its disposables
617
for (const [key, session] of this._activeSessions) {
618
if (session.providerHandle === handle) {
619
session.dispose();
620
this._activeSessions.delete(key);
621
}
622
}
623
}
624
625
async $handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise<void> {
626
const resource = URI.revive(sessionResource);
627
const observableSession = this._activeSessions.get(resource);
628
if (!observableSession) {
629
this._logService.warn(`No session found for progress chunks: handle ${handle}, sessionResource ${resource}, requestId ${requestId}`);
630
return;
631
}
632
633
const chatProgressParts: IChatProgress[] = chunks.map(chunk => {
634
const [progress] = Array.isArray(chunk) ? chunk : [chunk];
635
return revive(progress) as IChatProgress;
636
});
637
638
observableSession.handleProgressChunk(requestId, chatProgressParts);
639
}
640
641
$handleProgressComplete(handle: number, sessionResource: UriComponents, requestId: string) {
642
const resource = URI.revive(sessionResource);
643
const observableSession = this._activeSessions.get(resource);
644
if (!observableSession) {
645
this._logService.warn(`No session found for progress completion: handle ${handle}, sessionResource ${resource}, requestId ${requestId}`);
646
return;
647
}
648
649
observableSession.handleProgressComplete(requestId);
650
}
651
652
$handleAnchorResolve(handle: number, sesssionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto<IChatContentInlineReference>): void {
653
// throw new Error('Method not implemented.');
654
}
655
656
$onDidChangeChatSessionProviderOptions(handle: number): void {
657
let sessionType: string | undefined;
658
for (const [type, h] of this._sessionTypeToHandle) {
659
if (h === handle) {
660
sessionType = type;
661
break;
662
}
663
}
664
665
if (!sessionType) {
666
this._logService.warn(`No session type found for chat session content provider handle ${handle} when refreshing provider options`);
667
return;
668
}
669
670
this._refreshProviderOptions(handle, sessionType);
671
}
672
673
private _refreshProviderOptions(handle: number, chatSessionScheme: string): void {
674
this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => {
675
if (options?.optionGroups && options.optionGroups.length) {
676
const groupsWithCallbacks = options.optionGroups.map(group => ({
677
...group,
678
onSearch: group.searchable ? async (query: string, token: CancellationToken) => {
679
return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token);
680
} : undefined,
681
}));
682
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, groupsWithCallbacks);
683
}
684
}).catch(err => this._logService.error('Error fetching chat session options', err));
685
}
686
687
override dispose(): void {
688
for (const session of this._activeSessions.values()) {
689
session.dispose();
690
}
691
this._activeSessions.clear();
692
693
for (const disposable of this._sessionDisposables.values()) {
694
disposable.dispose();
695
}
696
this._sessionDisposables.clear();
697
698
super.dispose();
699
}
700
701
private _reviveTooltip(tooltip: string | IMarkdownString | undefined): string | MarkdownString | undefined {
702
if (!tooltip) {
703
return undefined;
704
}
705
706
// If it's already a string, return as-is
707
if (typeof tooltip === 'string') {
708
return tooltip;
709
}
710
711
// If it's a serialized IMarkdownString, revive it to MarkdownString
712
if (typeof tooltip === 'object' && 'value' in tooltip) {
713
return MarkdownString.lift(tooltip);
714
}
715
716
return undefined;
717
}
718
719
/**
720
* Notify the extension about option changes for a session
721
*/
722
async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>): Promise<void> {
723
this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: starting proxy call for handle ${handle}, sessionResource ${sessionResource}`);
724
try {
725
await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None);
726
this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: proxy call completed for handle ${handle}, sessionResource ${sessionResource}`);
727
} catch (error) {
728
this._logService.error(`[MainThreadChatSessions] notifyOptionsChange: error for handle ${handle}, sessionResource ${sessionResource}:`, error);
729
}
730
}
731
}
732
733