Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/chatServiceImpl.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 { DeferredPromise } from '../../../../base/common/async.js';
7
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8
import { memoize } from '../../../../base/common/decorators.js';
9
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
10
import { ErrorNoTelemetry } from '../../../../base/common/errors.js';
11
import { Emitter, Event } from '../../../../base/common/event.js';
12
import { MarkdownString } from '../../../../base/common/htmlContent.js';
13
import { Iterable } from '../../../../base/common/iterator.js';
14
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
15
import { revive } from '../../../../base/common/marshalling.js';
16
import { autorun, derived, IObservable, ObservableMap } from '../../../../base/common/observable.js';
17
import { StopWatch } from '../../../../base/common/stopwatch.js';
18
import { URI } from '../../../../base/common/uri.js';
19
import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
20
import { localize } from '../../../../nls.js';
21
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
22
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
23
import { ILogService } from '../../../../platform/log/common/log.js';
24
import { Progress } from '../../../../platform/progress/common/progress.js';
25
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
26
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
27
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
28
import { IMcpService } from '../../mcp/common/mcpTypes.js';
29
import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js';
30
import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js';
31
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js';
32
import { ChatRequestParser } from './chatRequestParser.js';
33
import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js';
34
import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';
35
import { IChatSessionsService } from './chatSessionsService.js';
36
import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js';
37
import { IChatSlashCommandService } from './chatSlashCommands.js';
38
import { IChatTransferService } from './chatTransferService.js';
39
import { ChatSessionUri } from './chatUri.js';
40
import { IChatRequestVariableEntry } from './chatVariableEntries.js';
41
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js';
42
import { ChatMessageRole, IChatMessage } from './languageModels.js';
43
import { ILanguageModelToolsService } from './languageModelToolsService.js';
44
45
const serializedChatKey = 'interactive.sessions';
46
47
const globalChatKey = 'chat.workspaceTransfer';
48
49
const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60;
50
51
const maxPersistedSessions = 25;
52
53
class CancellableRequest implements IDisposable {
54
constructor(
55
public readonly cancellationTokenSource: CancellationTokenSource,
56
public requestId: string | undefined,
57
@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService
58
) { }
59
60
dispose() {
61
this.cancellationTokenSource.dispose();
62
}
63
64
cancel() {
65
if (this.requestId) {
66
this.toolsService.cancelToolCallsForRequest(this.requestId);
67
}
68
69
this.cancellationTokenSource.cancel();
70
}
71
}
72
73
export class ChatService extends Disposable implements IChatService {
74
declare _serviceBrand: undefined;
75
76
private readonly _sessionModels = new ObservableMap<string, ChatModel>();
77
private readonly _contentProviderSessionModels = new Map<string, Map<string, { readonly model: IChatModel; readonly disposables: DisposableStore }>>();
78
private readonly _pendingRequests = this._register(new DisposableMap<string, CancellableRequest>());
79
private _persistedSessions: ISerializableChatsData;
80
81
/** Just for empty windows, need to enforce that a chat was deleted, even though other windows still have it */
82
private _deletedChatIds = new Set<string>();
83
84
private _transferredSessionData: IChatTransferredSessionData | undefined;
85
public get transferredSessionData(): IChatTransferredSessionData | undefined {
86
return this._transferredSessionData;
87
}
88
89
private readonly _onDidSubmitRequest = this._register(new Emitter<{ chatSessionId: string }>());
90
public readonly onDidSubmitRequest: Event<{ chatSessionId: string }> = this._onDidSubmitRequest.event;
91
92
private readonly _onDidPerformUserAction = this._register(new Emitter<IChatUserActionEvent>());
93
public readonly onDidPerformUserAction: Event<IChatUserActionEvent> = this._onDidPerformUserAction.event;
94
95
private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; reason: 'cleared' }>());
96
public readonly onDidDisposeSession = this._onDidDisposeSession.event;
97
98
private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap<string, CancellationTokenSource>());
99
private readonly _chatServiceTelemetry: ChatServiceTelemetry;
100
private readonly _chatSessionStore: ChatSessionStore;
101
102
readonly requestInProgressObs: IObservable<boolean>;
103
104
@memoize
105
private get useFileStorage(): boolean {
106
return this.configurationService.getValue(ChatConfiguration.UseFileStorage);
107
}
108
109
public get edits2Enabled(): boolean {
110
return this.configurationService.getValue(ChatConfiguration.Edits2Enabled);
111
}
112
113
private get isEmptyWindow(): boolean {
114
const workspace = this.workspaceContextService.getWorkspace();
115
return !workspace.configuration && workspace.folders.length === 0;
116
}
117
118
constructor(
119
@IStorageService private readonly storageService: IStorageService,
120
@ILogService private readonly logService: ILogService,
121
@IExtensionService private readonly extensionService: IExtensionService,
122
@IInstantiationService private readonly instantiationService: IInstantiationService,
123
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
124
@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,
125
@IChatAgentService private readonly chatAgentService: IChatAgentService,
126
@IConfigurationService private readonly configurationService: IConfigurationService,
127
@IChatTransferService private readonly chatTransferService: IChatTransferService,
128
@IChatSessionsService private readonly chatSessionService: IChatSessionsService,
129
@IMcpService private readonly mcpService: IMcpService,
130
) {
131
super();
132
133
this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry);
134
135
const sessionData = storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, '');
136
if (sessionData) {
137
this._persistedSessions = this.deserializeChats(sessionData);
138
const countsForLog = Object.keys(this._persistedSessions).length;
139
if (countsForLog > 0) {
140
this.trace('constructor', `Restored ${countsForLog} persisted sessions`);
141
}
142
} else {
143
this._persistedSessions = {};
144
}
145
146
const transferredData = this.getTransferredSessionData();
147
const transferredChat = transferredData?.chat;
148
if (transferredChat) {
149
this.trace('constructor', `Transferred session ${transferredChat.sessionId}`);
150
this._persistedSessions[transferredChat.sessionId] = transferredChat;
151
this._transferredSessionData = {
152
sessionId: transferredChat.sessionId,
153
inputValue: transferredData.inputValue,
154
location: transferredData.location,
155
mode: transferredData.mode,
156
};
157
}
158
159
this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore));
160
if (this.useFileStorage) {
161
this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions);
162
163
// When using file storage, populate _persistedSessions with session metadata from the index
164
// This ensures that getPersistedSessionTitle() can find titles for inactive sessions
165
this.initializePersistedSessionsFromFileStorage();
166
}
167
168
this._register(storageService.onWillSaveState(() => this.saveState()));
169
170
this.requestInProgressObs = derived(reader => {
171
const models = this._sessionModels.observable.read(reader).values();
172
return Array.from(models).some(model => model.requestInProgressObs.read(reader));
173
});
174
}
175
176
isEnabled(location: ChatAgentLocation): boolean {
177
return this.chatAgentService.getContributedDefaultAgent(location) !== undefined;
178
}
179
180
private saveState(): void {
181
const liveChats = Array.from(this._sessionModels.values())
182
.filter(session =>
183
this.shouldSaveToHistory(session.sessionId) && (session.initialLocation === ChatAgentLocation.Panel || session.initialLocation === ChatAgentLocation.Editor));
184
185
if (this.useFileStorage) {
186
this._chatSessionStore.storeSessions(liveChats);
187
} else {
188
if (this.isEmptyWindow) {
189
this.syncEmptyWindowChats(liveChats);
190
} else {
191
let allSessions: (ChatModel | ISerializableChatData)[] = liveChats;
192
allSessions = allSessions.concat(
193
Object.values(this._persistedSessions)
194
.filter(session => !this._sessionModels.has(session.sessionId))
195
.filter(session => session.requests.length));
196
allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0));
197
198
allSessions = allSessions.slice(0, maxPersistedSessions);
199
200
if (allSessions.length) {
201
this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`);
202
}
203
204
const serialized = JSON.stringify(allSessions);
205
206
if (allSessions.length) {
207
this.trace('onWillSaveState', `Persisting ${serialized.length} chars`);
208
}
209
210
this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE);
211
}
212
213
}
214
215
this._deletedChatIds.clear();
216
}
217
218
private syncEmptyWindowChats(thisWindowChats: ChatModel[]): void {
219
// Note- an unavoidable race condition exists here. If there are multiple empty windows open, and the user quits the application, then the focused
220
// window may lose active chats, because all windows are reading and writing to storageService at the same time. This can't be fixed without some
221
// kind of locking, but in reality, the focused window will likely have run `saveState` at some point, like on a window focus change, and it will
222
// generally be fine.
223
const sessionData = this.storageService.get(serializedChatKey, StorageScope.APPLICATION, '');
224
225
const originalPersistedSessions = this._persistedSessions;
226
let persistedSessions: ISerializableChatsData;
227
if (sessionData) {
228
persistedSessions = this.deserializeChats(sessionData);
229
const countsForLog = Object.keys(persistedSessions).length;
230
if (countsForLog > 0) {
231
this.trace('constructor', `Restored ${countsForLog} persisted sessions`);
232
}
233
} else {
234
persistedSessions = {};
235
}
236
237
this._deletedChatIds.forEach(id => delete persistedSessions[id]);
238
239
// Has the chat in this window been updated, and then closed? Overwrite the old persisted chats.
240
Object.values(originalPersistedSessions).forEach(session => {
241
const persistedSession = persistedSessions[session.sessionId];
242
if (persistedSession && session.requests.length > persistedSession.requests.length) {
243
// We will add a 'modified date' at some point, but comparing the number of requests is good enough
244
persistedSessions[session.sessionId] = session;
245
} else if (!persistedSession && session.isNew) {
246
// This session was created in this window, and hasn't been persisted yet
247
session.isNew = false;
248
persistedSessions[session.sessionId] = session;
249
}
250
});
251
252
this._persistedSessions = persistedSessions;
253
254
// Add this window's active chat models to the set to persist.
255
// Having the same session open in two empty windows at the same time can lead to data loss, this is acceptable
256
const allSessions: Record<string, ISerializableChatData | ChatModel> = { ...this._persistedSessions };
257
for (const chat of thisWindowChats) {
258
allSessions[chat.sessionId] = chat;
259
}
260
261
let sessionsList = Object.values(allSessions);
262
sessionsList.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0));
263
sessionsList = sessionsList.slice(0, maxPersistedSessions);
264
const data = JSON.stringify(sessionsList);
265
this.storageService.store(serializedChatKey, data, StorageScope.APPLICATION, StorageTarget.MACHINE);
266
}
267
268
notifyUserAction(action: IChatUserActionEvent): void {
269
this._chatServiceTelemetry.notifyUserAction(action);
270
this._onDidPerformUserAction.fire(action);
271
if (action.action.kind === 'chatEditingSessionAction') {
272
const model = this._sessionModels.get(action.sessionId);
273
if (model) {
274
model.notifyEditingAction(action.action);
275
}
276
}
277
}
278
279
async setChatSessionTitle(sessionId: string, title: string): Promise<void> {
280
const model = this._sessionModels.get(sessionId);
281
if (model) {
282
model.setCustomTitle(title);
283
}
284
285
// Also update the persisted session data
286
if (this.useFileStorage) {
287
// Update the title in the file storage
288
await this._chatSessionStore.setSessionTitle(sessionId, title);
289
// Trigger immediate save to ensure consistency
290
await this.saveState();
291
} else {
292
// Update the in-memory storage
293
if (this._persistedSessions[sessionId]) {
294
this._persistedSessions[sessionId].customTitle = title;
295
} else {
296
// Create a minimal placeholder entry with the title
297
// The full session data will be merged later when the session is activated or saved
298
this._persistedSessions[sessionId] = {
299
version: 3,
300
sessionId: sessionId,
301
customTitle: title,
302
creationDate: Date.now(),
303
lastMessageDate: Date.now(),
304
isImported: false,
305
initialLocation: undefined,
306
requests: [],
307
requesterUsername: '',
308
responderUsername: '',
309
requesterAvatarIconUri: undefined,
310
responderAvatarIconUri: undefined,
311
};
312
}
313
314
// Trigger immediate save to ensure the title is persisted
315
await this.saveState();
316
}
317
}
318
319
private trace(method: string, message?: string): void {
320
if (message) {
321
this.logService.trace(`ChatService#${method}: ${message}`);
322
} else {
323
this.logService.trace(`ChatService#${method}`);
324
}
325
}
326
327
private error(method: string, message: string): void {
328
this.logService.error(`ChatService#${method} ${message}`);
329
}
330
331
private deserializeChats(sessionData: string): ISerializableChatsData {
332
try {
333
const arrayOfSessions: ISerializableChatDataIn[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data
334
if (!Array.isArray(arrayOfSessions)) {
335
throw new Error('Expected array');
336
}
337
338
const sessions = arrayOfSessions.reduce<ISerializableChatsData>((acc, session) => {
339
// Revive serialized markdown strings in response data
340
for (const request of session.requests) {
341
if (Array.isArray(request.response)) {
342
request.response = request.response.map((response) => {
343
if (typeof response === 'string') {
344
return new MarkdownString(response);
345
}
346
return response;
347
});
348
} else if (typeof request.response === 'string') {
349
request.response = [new MarkdownString(request.response)];
350
}
351
}
352
353
acc[session.sessionId] = normalizeSerializableChatData(session);
354
return acc;
355
}, {});
356
return sessions;
357
} catch (err) {
358
this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`);
359
return {};
360
}
361
}
362
363
private getTransferredSessionData(): IChatTransfer2 | undefined {
364
const data: IChatTransfer2[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []);
365
const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri;
366
if (!workspaceUri) {
367
return;
368
}
369
370
const thisWorkspace = workspaceUri.toString();
371
const currentTime = Date.now();
372
// Only use transferred data if it was created recently
373
const transferred = data.find(item => URI.revive(item.toWorkspace).toString() === thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS));
374
// Keep data that isn't for the current workspace and that hasn't expired yet
375
const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS));
376
this.storageService.store(globalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE);
377
return transferred;
378
}
379
380
private async initializePersistedSessionsFromFileStorage(): Promise<void> {
381
382
const index = await this._chatSessionStore.getIndex();
383
const sessionIds = Object.keys(index);
384
385
for (const sessionId of sessionIds) {
386
const metadata = index[sessionId];
387
if (metadata && !this._persistedSessions[sessionId]) {
388
// Create a minimal session entry with the title information
389
// This allows getPersistedSessionTitle() to find the title without loading the full session
390
const minimalSession: ISerializableChatData = {
391
version: 3,
392
sessionId: sessionId,
393
customTitle: metadata.title,
394
creationDate: Date.now(), // Use current time as fallback
395
lastMessageDate: metadata.lastMessageDate,
396
isImported: metadata.isImported || false,
397
initialLocation: metadata.initialLocation,
398
requests: [], // Empty requests array - this is just for title lookup
399
requesterUsername: '',
400
responderUsername: '',
401
requesterAvatarIconUri: undefined,
402
responderAvatarIconUri: undefined,
403
};
404
405
this._persistedSessions[sessionId] = minimalSession;
406
}
407
}
408
}
409
410
/**
411
* Returns an array of chat details for all persisted chat sessions that have at least one request.
412
* Chat sessions that have already been loaded into the chat view are excluded from the result.
413
* Imported chat sessions are also excluded from the result.
414
*/
415
async getHistory(): Promise<IChatDetail[]> {
416
if (this.useFileStorage) {
417
const liveSessionItems = Array.from(this._sessionModels.values())
418
.filter(session => !session.isImported)
419
.map(session => {
420
const title = session.title || localize('newChat', "New Chat");
421
return {
422
sessionId: session.sessionId,
423
title,
424
lastMessageDate: session.lastMessageDate,
425
isActive: true,
426
} satisfies IChatDetail;
427
});
428
429
const index = await this._chatSessionStore.getIndex();
430
const entries = Object.values(index)
431
.filter(entry => !this._sessionModels.has(entry.sessionId) && !entry.isImported && !entry.isEmpty)
432
.map((entry): IChatDetail => ({
433
...entry,
434
isActive: this._sessionModels.has(entry.sessionId),
435
}));
436
return [...liveSessionItems, ...entries];
437
}
438
439
const persistedSessions = Object.values(this._persistedSessions)
440
.filter(session => session.requests.length > 0)
441
.filter(session => !this._sessionModels.has(session.sessionId));
442
443
const persistedSessionItems = persistedSessions
444
.filter(session => !session.isImported)
445
.map(session => {
446
const title = session.customTitle ?? ChatModel.getDefaultTitle(session.requests);
447
return {
448
sessionId: session.sessionId,
449
title,
450
lastMessageDate: session.lastMessageDate,
451
isActive: false,
452
} satisfies IChatDetail;
453
});
454
const liveSessionItems = Array.from(this._sessionModels.values())
455
.filter(session => !session.isImported)
456
.map(session => {
457
const title = session.title || localize('newChat', "New Chat");
458
return {
459
sessionId: session.sessionId,
460
title,
461
lastMessageDate: session.lastMessageDate,
462
isActive: true,
463
} satisfies IChatDetail;
464
});
465
return [...liveSessionItems, ...persistedSessionItems];
466
}
467
468
async removeHistoryEntry(sessionId: string): Promise<void> {
469
if (this.useFileStorage) {
470
await this._chatSessionStore.deleteSession(sessionId);
471
return;
472
}
473
474
if (this._persistedSessions[sessionId]) {
475
this._deletedChatIds.add(sessionId);
476
delete this._persistedSessions[sessionId];
477
this.saveState();
478
}
479
}
480
481
async clearAllHistoryEntries(): Promise<void> {
482
if (this.useFileStorage) {
483
await this._chatSessionStore.clearAllSessions();
484
return;
485
}
486
487
Object.values(this._persistedSessions).forEach(session => this._deletedChatIds.add(session.sessionId));
488
this._persistedSessions = {};
489
this.saveState();
490
}
491
492
startSession(location: ChatAgentLocation, token: CancellationToken, isGlobalEditingSession: boolean = true): ChatModel {
493
this.trace('startSession');
494
return this._startSession(undefined, location, isGlobalEditingSession, token);
495
}
496
497
private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: ChatAgentLocation, isGlobalEditingSession: boolean, token: CancellationToken): ChatModel {
498
const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, location);
499
if (location === ChatAgentLocation.Panel) {
500
model.startEditingSession(isGlobalEditingSession);
501
}
502
503
this._sessionModels.set(model.sessionId, model);
504
this.initializeSession(model, token);
505
return model;
506
}
507
508
private initializeSession(model: ChatModel, token: CancellationToken): void {
509
this.trace('initializeSession', `Initialize session ${model.sessionId}`);
510
511
// Activate the default extension provided agent but do not wait
512
// for it to be ready so that the session can be used immediately
513
// without having to wait for the agent to be ready.
514
this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e));
515
}
516
517
async activateDefaultAgent(location: ChatAgentLocation): Promise<void> {
518
await this.extensionService.whenInstalledExtensionsRegistered();
519
520
const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel);
521
if (!defaultAgentData) {
522
throw new ErrorNoTelemetry('No default agent contributed');
523
}
524
525
// Await activation of the extension provided agent
526
// Using `activateById` as workaround for the issue
527
// https://github.com/microsoft/vscode/issues/250590
528
if (!defaultAgentData.isCore) {
529
await this.extensionService.activateById(defaultAgentData.extensionId, {
530
activationEvent: `onChatParticipant:${defaultAgentData.id}`,
531
extensionId: defaultAgentData.extensionId,
532
startup: false
533
});
534
}
535
536
const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id);
537
if (!defaultAgent) {
538
throw new ErrorNoTelemetry('No default agent registered');
539
}
540
}
541
542
getSession(sessionId: string): IChatModel | undefined {
543
return this._sessionModels.get(sessionId);
544
}
545
546
async getOrRestoreSession(sessionId: string): Promise<ChatModel | undefined> {
547
this.trace('getOrRestoreSession', `sessionId: ${sessionId}`);
548
const model = this._sessionModels.get(sessionId);
549
if (model) {
550
return model;
551
}
552
553
let sessionData: ISerializableChatData | undefined;
554
if (!this.useFileStorage || this.transferredSessionData?.sessionId === sessionId) {
555
sessionData = revive(this._persistedSessions[sessionId]);
556
} else {
557
sessionData = revive(await this._chatSessionStore.readSession(sessionId));
558
}
559
560
if (!sessionData) {
561
return undefined;
562
}
563
564
const session = this._startSession(sessionData, sessionData.initialLocation ?? ChatAgentLocation.Panel, true, CancellationToken.None);
565
566
const isTransferred = this.transferredSessionData?.sessionId === sessionId;
567
if (isTransferred) {
568
this._transferredSessionData = undefined;
569
}
570
571
return session;
572
}
573
574
/**
575
* This is really just for migrating data from the edit session location to the panel.
576
*/
577
isPersistedSessionEmpty(sessionId: string): boolean {
578
const session = this._persistedSessions[sessionId];
579
if (session) {
580
return session.requests.length === 0;
581
}
582
583
return this._chatSessionStore.isSessionEmpty(sessionId);
584
}
585
586
getPersistedSessionTitle(sessionId: string): string | undefined {
587
// First check the memory cache (_persistedSessions)
588
const session = this._persistedSessions[sessionId];
589
if (session) {
590
const title = session.customTitle || ChatModel.getDefaultTitle(session.requests);
591
return title;
592
}
593
594
// If using file storage and not found in memory, try to read directly from file storage index
595
// This handles the case where getName() is called before initialization completes
596
if (this.useFileStorage) {
597
// Access the internal synchronous index method via reflection
598
// This is a workaround for the timing issue where initialization hasn't completed
599
const internalGetIndex = (this._chatSessionStore as any).internalGetIndex;
600
if (typeof internalGetIndex === 'function') {
601
const indexData = internalGetIndex.call(this._chatSessionStore);
602
const metadata = indexData.entries[sessionId];
603
if (metadata && metadata.title) {
604
return metadata.title;
605
}
606
}
607
}
608
609
return undefined;
610
}
611
612
loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined {
613
return this._startSession(data, data.initialLocation ?? ChatAgentLocation.Panel, true, CancellationToken.None);
614
}
615
616
async loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModel | undefined> {
617
// TODO: Move this into a new ChatModelService
618
const parsed = ChatSessionUri.parse(resource);
619
if (!parsed) {
620
throw new Error('Invalid chat session URI');
621
}
622
623
const existing = this._contentProviderSessionModels.get(parsed.chatSessionType)?.get(parsed.sessionId);
624
if (existing) {
625
return existing.model;
626
}
627
628
if (parsed.chatSessionType === 'local') {
629
return this.getOrRestoreSession(parsed.sessionId);
630
}
631
632
const chatSessionType = parsed.chatSessionType;
633
const content = await this.chatSessionService.provideChatSessionContent(chatSessionType, parsed.sessionId, CancellationToken.None);
634
635
const model = this._startSession(undefined, location, true, CancellationToken.None);
636
if (!this._contentProviderSessionModels.has(chatSessionType)) {
637
this._contentProviderSessionModels.set(chatSessionType, new Map());
638
}
639
const disposables = new DisposableStore();
640
this._contentProviderSessionModels.get(chatSessionType)!.set(parsed.sessionId, { model, disposables });
641
642
disposables.add(model.onDidDispose(() => {
643
this._contentProviderSessionModels?.get(chatSessionType)?.delete(parsed.sessionId);
644
content.dispose();
645
}));
646
647
let lastRequest: ChatRequestModel | undefined;
648
for (const message of content.history) {
649
if (message.type === 'request') {
650
if (lastRequest) {
651
model.completeResponse(lastRequest);
652
}
653
654
const requestText = message.prompt;
655
656
const parsedRequest: IParsedChatRequest = {
657
text: requestText,
658
parts: [new ChatRequestTextPart(
659
new OffsetRange(0, requestText.length),
660
{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: requestText.length + 1 },
661
requestText
662
)]
663
};
664
const agent =
665
message.participant
666
? this.chatAgentService.getAgent(message.participant) // TODO(jospicer): Remove and always hardcode?
667
: this.chatAgentService.getAgent(chatSessionType);
668
lastRequest = model.addRequest(parsedRequest,
669
{ variables: [] }, // variableData
670
0, // attempt
671
undefined,
672
agent,
673
undefined, // slashCommand
674
undefined, // confirmation
675
undefined, // locationData
676
undefined, // attachments
677
true // isCompleteAddedRequest - this indicates it's a complete request, not user input
678
);
679
} else {
680
// response
681
if (lastRequest) {
682
for (const part of message.parts) {
683
model.acceptResponseProgress(lastRequest, part);
684
}
685
}
686
}
687
}
688
689
if (content.progressObs && lastRequest && content.interruptActiveResponseCallback) {
690
const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);
691
this._pendingRequests.set(model.sessionId, initialCancellationRequest);
692
const cancellationListener = new MutableDisposable();
693
694
const createCancellationListener = (token: CancellationToken) => {
695
return token.onCancellationRequested(() => {
696
content.interruptActiveResponseCallback?.().then(userConfirmedInterruption => {
697
if (!userConfirmedInterruption) {
698
// User cancelled the interruption
699
const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);
700
this._pendingRequests.set(model.sessionId, newCancellationRequest);
701
cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token);
702
}
703
});
704
});
705
};
706
707
cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token);
708
disposables.add(cancellationListener);
709
710
let lastProgressLength = 0;
711
disposables.add(autorun(reader => {
712
const progressArray = content.progressObs?.read(reader) ?? [];
713
const isComplete = content.isCompleteObs?.read(reader) ?? false;
714
715
// Process only new progress items
716
if (progressArray.length > lastProgressLength) {
717
const newProgress = progressArray.slice(lastProgressLength);
718
for (const progress of newProgress) {
719
model?.acceptResponseProgress(lastRequest, progress);
720
}
721
lastProgressLength = progressArray.length;
722
}
723
724
// Handle completion
725
if (isComplete) {
726
model?.completeResponse(lastRequest);
727
cancellationListener.clear();
728
}
729
}));
730
} else {
731
if (lastRequest) {
732
model.completeResponse(lastRequest);
733
}
734
}
735
736
return model;
737
}
738
739
async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise<void> {
740
const model = this._sessionModels.get(request.session.sessionId);
741
if (!model && model !== request.session) {
742
throw new Error(`Unknown session: ${request.session.sessionId}`);
743
}
744
745
const cts = this._pendingRequests.get(request.session.sessionId);
746
if (cts) {
747
this.trace('resendRequest', `Session ${request.session.sessionId} already has a pending request, cancelling...`);
748
cts.cancel();
749
}
750
751
const location = options?.location ?? model.initialLocation;
752
const attempt = options?.attempt ?? 0;
753
const enableCommandDetection = !options?.noCommandDetection;
754
const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;
755
756
model.removeRequest(request.id, ChatRequestRemovalReason.Resend);
757
758
const resendOptions: IChatSendRequestOptions = {
759
...options,
760
locationData: request.locationData,
761
attachedContext: request.attachedContext,
762
};
763
await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise;
764
}
765
766
async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise<IChatSendRequestData | undefined> {
767
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);
768
769
770
if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.agentIdSilent) {
771
this.trace('sendRequest', 'Rejected empty message');
772
return;
773
}
774
775
const model = this._sessionModels.get(sessionId);
776
if (!model) {
777
throw new Error(`Unknown session: ${sessionId}`);
778
}
779
780
if (this._pendingRequests.has(sessionId)) {
781
this.trace('sendRequest', `Session ${sessionId} already has a pending request`);
782
return;
783
}
784
785
const requests = model.getRequests();
786
for (let i = requests.length - 1; i >= 0; i -= 1) {
787
const request = requests[i];
788
if (request.shouldBeRemovedOnSend) {
789
if (request.shouldBeRemovedOnSend.afterUndoStop) {
790
request.response?.finalizeUndoState();
791
} else {
792
await this.removeRequest(sessionId, request.id);
793
}
794
}
795
}
796
797
const location = options?.location ?? model.initialLocation;
798
const attempt = options?.attempt ?? 0;
799
const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;
800
801
const parsedRequest = this.parseChatRequest(sessionId, request, location, options);
802
const silentAgent = options?.agentIdSilent ? this.chatAgentService.getAgent(options.agentIdSilent) : undefined;
803
const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;
804
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
805
806
// This method is only returning whether the request was accepted - don't block on the actual request
807
return {
808
...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, silentAgent ?? defaultAgent, location, options),
809
agent,
810
slashCommand: agentSlashCommandPart?.command,
811
};
812
}
813
814
private parseChatRequest(sessionId: string, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest {
815
let parserContext = options?.parserContext;
816
if (options?.agentId) {
817
const agent = this.chatAgentService.getAgent(options.agentId);
818
if (!agent) {
819
throw new Error(`Unknown agent: ${options.agentId}`);
820
}
821
parserContext = { selectedAgent: agent, mode: options.modeInfo?.kind };
822
const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : '';
823
request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`;
824
}
825
826
const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext);
827
return parsedRequest;
828
}
829
830
private refreshFollowupsCancellationToken(sessionId: string): CancellationToken {
831
this._sessionFollowupCancelTokens.get(sessionId)?.cancel();
832
const newTokenSource = new CancellationTokenSource();
833
this._sessionFollowupCancelTokens.set(sessionId, newTokenSource);
834
835
return newTokenSource.token;
836
}
837
838
private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState {
839
const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId);
840
let request: ChatRequestModel;
841
const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
842
const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);
843
const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);
844
const requests = [...model.getRequests()];
845
const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, {
846
agentPart,
847
agentSlashCommandPart,
848
commandPart,
849
sessionId: model.sessionId,
850
location: model.initialLocation,
851
options,
852
enableCommandDetection
853
});
854
855
let gotProgress = false;
856
const requestType = commandPart ? 'slashCommand' : 'string';
857
858
const responseCreated = new DeferredPromise<IChatResponseModel>();
859
let responseCreatedComplete = false;
860
function completeResponseCreated(): void {
861
if (!responseCreatedComplete && request?.response) {
862
responseCreated.complete(request.response);
863
responseCreatedComplete = true;
864
}
865
}
866
867
const store = new DisposableStore();
868
const source = store.add(new CancellationTokenSource());
869
const token = source.token;
870
const sendRequestInternal = async () => {
871
const progressCallback = (progress: IChatProgress[]) => {
872
if (token.isCancellationRequested) {
873
return;
874
}
875
876
gotProgress = true;
877
878
for (let i = 0; i < progress.length; i++) {
879
const isLast = i === progress.length - 1;
880
const progressItem = progress[i];
881
882
if (progressItem.kind === 'markdownContent') {
883
this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progressItem.content.value.length} chars`);
884
} else {
885
this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progressItem)}`);
886
}
887
888
model.acceptResponseProgress(request, progressItem, !isLast);
889
}
890
completeResponseCreated();
891
};
892
893
let detectedAgent: IChatAgentData | undefined;
894
let detectedCommand: IChatAgentCommand | undefined;
895
896
const stopWatch = new StopWatch(false);
897
store.add(token.onCancellationRequested(() => {
898
this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);
899
if (!request) {
900
return;
901
}
902
903
requestTelemetry.complete({
904
timeToFirstProgress: undefined,
905
result: 'cancelled',
906
// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling
907
totalTime: stopWatch.elapsed(),
908
requestType,
909
detectedAgent,
910
request,
911
});
912
913
model.cancelRequest(request);
914
}));
915
916
try {
917
let rawResult: IChatAgentResult | null | undefined;
918
let agentOrCommandFollowups: Promise<IChatFollowup[] | undefined> | undefined = undefined;
919
let chatTitlePromise: Promise<string | undefined> | undefined;
920
921
if (agentPart || (defaultAgent && !commandPart)) {
922
const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => {
923
const initVariableData: IChatRequestVariableData = { variables: [] };
924
request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, options?.modeInfo, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId);
925
926
let variableData: IChatRequestVariableData;
927
let message: string;
928
if (chatRequest) {
929
variableData = chatRequest.variableData;
930
message = getPromptText(request.message).message;
931
} else {
932
variableData = { variables: this.prepareContext(request.attachedContext) };
933
model.updateRequest(request, variableData);
934
935
const promptTextResult = getPromptText(request.message);
936
variableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack
937
message = promptTextResult.message;
938
}
939
940
let isInitialTools = true;
941
942
store.add(autorun(reader => {
943
const tools = options?.userSelectedTools?.read(reader);
944
if (isInitialTools) {
945
isInitialTools = false;
946
return;
947
}
948
949
if (tools) {
950
this.chatAgentService.setRequestTools(agent.id, request.id, tools);
951
}
952
}));
953
954
return {
955
sessionId,
956
requestId: request.id,
957
agentId: agent.id,
958
message,
959
command: command?.name,
960
variables: variableData,
961
enableCommandDetection,
962
isParticipantDetected,
963
attempt,
964
location,
965
locationData: request.locationData,
966
acceptedConfirmationData: options?.acceptedConfirmationData,
967
rejectedConfirmationData: options?.rejectedConfirmationData,
968
userSelectedModelId: options?.userSelectedModelId,
969
userSelectedTools: options?.userSelectedTools?.get(),
970
modeInstructions: options?.modeInfo?.instructions,
971
editedFileEvents: request.editedFileEvents
972
} satisfies IChatAgentRequest;
973
};
974
975
if (
976
this.configurationService.getValue('chat.detectParticipant.enabled') !== false &&
977
this.chatAgentService.hasChatParticipantDetectionProviders() &&
978
!agentPart &&
979
!commandPart &&
980
!agentSlashCommandPart &&
981
enableCommandDetection &&
982
options?.modeInfo?.kind !== ChatModeKind.Agent &&
983
options?.modeInfo?.kind !== ChatModeKind.Edit &&
984
!options?.agentIdSilent
985
) {
986
// We have no agent or command to scope history with, pass the full history to the participant detection provider
987
const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id);
988
989
// Prepare the request object that we will send to the participant detection provider
990
const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false);
991
992
const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token);
993
if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) {
994
// Update the response in the ChatModel to reflect the detected agent and command
995
request.response?.setAgent(result.agent, result.command);
996
detectedAgent = result.agent;
997
detectedCommand = result.command;
998
}
999
}
1000
1001
const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!;
1002
const command = detectedCommand ?? agentSlashCommandPart?.command;
1003
await Promise.all([
1004
this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`),
1005
this.mcpService.autostart(token),
1006
]);
1007
1008
// Recompute history in case the agent or command changed
1009
const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id);
1010
const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent);
1011
const pendingRequest = this._pendingRequests.get(sessionId);
1012
if (pendingRequest && !pendingRequest.requestId) {
1013
pendingRequest.requestId = requestProps.requestId;
1014
}
1015
completeResponseCreated();
1016
const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);
1017
rawResult = agentResult;
1018
agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken);
1019
chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id), CancellationToken.None) : undefined;
1020
} else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) {
1021
if (commandPart.slashCommand.silent !== true) {
1022
request = model.addRequest(parsedRequest, { variables: [] }, attempt, options?.modeInfo);
1023
completeResponseCreated();
1024
}
1025
// contributed slash commands
1026
// TODO: spell this out in the UI
1027
const history: IChatMessage[] = [];
1028
for (const modelRequest of model.getRequests()) {
1029
if (!modelRequest.response) {
1030
continue;
1031
}
1032
history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] });
1033
history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] });
1034
}
1035
const message = parsedRequest.text;
1036
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatProgress>(p => {
1037
progressCallback([p]);
1038
}), history, location, token);
1039
agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);
1040
rawResult = {};
1041
1042
} else {
1043
throw new Error(`Cannot handle request`);
1044
}
1045
1046
if (token.isCancellationRequested && !rawResult) {
1047
return;
1048
} else {
1049
if (!rawResult) {
1050
this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);
1051
rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };
1052
}
1053
1054
const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' :
1055
rawResult.errorDetails && gotProgress ? 'errorWithOutput' :
1056
rawResult.errorDetails ? 'error' :
1057
'success';
1058
1059
requestTelemetry.complete({
1060
timeToFirstProgress: rawResult.timings?.firstProgress,
1061
totalTime: rawResult.timings?.totalElapsed,
1062
result,
1063
requestType,
1064
detectedAgent,
1065
request,
1066
});
1067
1068
model.setResponse(request, rawResult);
1069
completeResponseCreated();
1070
this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);
1071
1072
model.completeResponse(request);
1073
if (agentOrCommandFollowups) {
1074
agentOrCommandFollowups.then(followups => {
1075
model.setFollowups(request, followups);
1076
const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command;
1077
this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0);
1078
});
1079
}
1080
chatTitlePromise?.then(title => {
1081
if (title) {
1082
model.setCustomTitle(title);
1083
}
1084
});
1085
}
1086
} catch (err) {
1087
this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`);
1088
requestTelemetry.complete({
1089
timeToFirstProgress: undefined,
1090
totalTime: undefined,
1091
result: 'error',
1092
requestType,
1093
detectedAgent,
1094
request,
1095
});
1096
if (request) {
1097
const rawResult: IChatAgentResult = { errorDetails: { message: err.message } };
1098
model.setResponse(request, rawResult);
1099
completeResponseCreated();
1100
model.completeResponse(request);
1101
}
1102
} finally {
1103
store.dispose();
1104
}
1105
};
1106
const rawResponsePromise = sendRequestInternal();
1107
// Note- requestId is not known at this point, assigned later
1108
this._pendingRequests.set(model.sessionId, this.instantiationService.createInstance(CancellableRequest, source, undefined));
1109
rawResponsePromise.finally(() => {
1110
this._pendingRequests.deleteAndDispose(model.sessionId);
1111
});
1112
this._onDidSubmitRequest.fire({ chatSessionId: model.sessionId });
1113
return {
1114
responseCreatedPromise: responseCreated.p,
1115
responseCompletePromise: rawResponsePromise,
1116
};
1117
}
1118
1119
private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] {
1120
attachedContextVariables ??= [];
1121
1122
// "reverse", high index first so that replacement is simple
1123
attachedContextVariables.sort((a, b) => {
1124
// If either range is undefined, sort it to the back
1125
if (!a.range && !b.range) {
1126
return 0; // Keep relative order if both ranges are undefined
1127
}
1128
if (!a.range) {
1129
return 1; // a goes after b
1130
}
1131
if (!b.range) {
1132
return -1; // a goes before b
1133
}
1134
return b.range.start - a.range.start;
1135
});
1136
1137
return attachedContextVariables;
1138
}
1139
1140
private getHistoryEntriesFromModel(requests: IChatRequestModel[], sessionId: string, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] {
1141
const history: IChatAgentHistoryEntry[] = [];
1142
const agent = this.chatAgentService.getAgent(forAgentId);
1143
for (const request of requests) {
1144
if (!request.response) {
1145
continue;
1146
}
1147
1148
if (forAgentId !== request.response.agent?.id && !agent?.isDefault) {
1149
// An agent only gets to see requests that were sent to this agent.
1150
// The default agent (the undefined case) gets to see all of them.
1151
continue;
1152
}
1153
1154
const promptTextResult = getPromptText(request.message);
1155
const historyRequest: IChatAgentRequest = {
1156
sessionId: sessionId,
1157
requestId: request.id,
1158
agentId: request.response.agent?.id ?? '',
1159
message: promptTextResult.message,
1160
command: request.response.slashCommand?.name,
1161
variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack
1162
location: ChatAgentLocation.Panel,
1163
editedFileEvents: request.editedFileEvents,
1164
};
1165
history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} });
1166
}
1167
1168
return history;
1169
}
1170
1171
async removeRequest(sessionId: string, requestId: string): Promise<void> {
1172
const model = this._sessionModels.get(sessionId);
1173
if (!model) {
1174
throw new Error(`Unknown session: ${sessionId}`);
1175
}
1176
1177
const pendingRequest = this._pendingRequests.get(sessionId);
1178
if (pendingRequest?.requestId === requestId) {
1179
pendingRequest.cancel();
1180
this._pendingRequests.deleteAndDispose(sessionId);
1181
}
1182
1183
model.removeRequest(requestId);
1184
}
1185
1186
async adoptRequest(sessionId: string, request: IChatRequestModel) {
1187
if (!(request instanceof ChatRequestModel)) {
1188
throw new TypeError('Can only adopt requests of type ChatRequestModel');
1189
}
1190
const target = this._sessionModels.get(sessionId);
1191
if (!target) {
1192
throw new Error(`Unknown session: ${sessionId}`);
1193
}
1194
1195
const oldOwner = request.session;
1196
target.adoptRequest(request);
1197
1198
if (request.response && !request.response.isComplete) {
1199
const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId);
1200
if (cts) {
1201
cts.requestId = request.id;
1202
this._pendingRequests.set(target.sessionId, cts);
1203
}
1204
}
1205
}
1206
1207
async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise<void> {
1208
this.trace('addCompleteRequest', `message: ${message}`);
1209
1210
const model = this._sessionModels.get(sessionId);
1211
if (!model) {
1212
throw new Error(`Unknown session: ${sessionId}`);
1213
}
1214
1215
const parsedRequest = typeof message === 'string' ?
1216
this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) :
1217
message;
1218
const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0, undefined, undefined, undefined, undefined, undefined, undefined, true);
1219
if (typeof response.message === 'string') {
1220
// TODO is this possible?
1221
model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' });
1222
} else {
1223
for (const part of response.message) {
1224
model.acceptResponseProgress(request, part, true);
1225
}
1226
}
1227
model.setResponse(request, response.result || {});
1228
if (response.followups !== undefined) {
1229
model.setFollowups(request, response.followups);
1230
}
1231
model.completeResponse(request);
1232
}
1233
1234
cancelCurrentRequestForSession(sessionId: string): void {
1235
this.trace('cancelCurrentRequestForSession', `sessionId: ${sessionId}`);
1236
this._pendingRequests.get(sessionId)?.cancel();
1237
this._pendingRequests.deleteAndDispose(sessionId);
1238
}
1239
1240
async clearSession(sessionId: string): Promise<void> {
1241
const shouldSaveToHistory = this.shouldSaveToHistory(sessionId);
1242
this.trace('clearSession', `sessionId: ${sessionId}, save to history: ${shouldSaveToHistory}`);
1243
const model = this._sessionModels.get(sessionId);
1244
if (!model) {
1245
throw new Error(`Unknown session: ${sessionId}`);
1246
}
1247
1248
if (shouldSaveToHistory && (model.initialLocation === ChatAgentLocation.Panel || model.initialLocation === ChatAgentLocation.Editor)) {
1249
if (this.useFileStorage) {
1250
// Always preserve sessions that have custom titles, even if empty
1251
if (model.getRequests().length === 0 && !model.customTitle) {
1252
await this._chatSessionStore.deleteSession(sessionId);
1253
} else {
1254
await this._chatSessionStore.storeSessions([model]);
1255
}
1256
} else {
1257
// Always preserve sessions that have custom titles, even if empty
1258
if (model.getRequests().length === 0 && !model.customTitle) {
1259
delete this._persistedSessions[sessionId];
1260
} else {
1261
// Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to
1262
// assign values to properties that are getters- microsoft/vscode-copilot-release#1233
1263
const sessionData: ISerializableChatData = JSON.parse(JSON.stringify(model));
1264
sessionData.isNew = true;
1265
this._persistedSessions[sessionId] = sessionData;
1266
}
1267
}
1268
}
1269
1270
this._sessionModels.delete(sessionId);
1271
model.dispose();
1272
this._pendingRequests.get(sessionId)?.cancel();
1273
this._pendingRequests.deleteAndDispose(sessionId);
1274
this._onDidDisposeSession.fire({ sessionId, reason: 'cleared' });
1275
}
1276
1277
public hasSessions(): boolean {
1278
if (this.useFileStorage) {
1279
return this._chatSessionStore.hasSessions();
1280
} else {
1281
return Object.values(this._persistedSessions).length > 0;
1282
}
1283
}
1284
1285
transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void {
1286
const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId);
1287
if (!model) {
1288
throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`);
1289
}
1290
1291
const existingRaw: IChatTransfer2[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []);
1292
existingRaw.push({
1293
chat: model.toJSON(),
1294
timestampInMilliseconds: Date.now(),
1295
toWorkspace: toWorkspace,
1296
inputValue: transferredSessionData.inputValue,
1297
location: transferredSessionData.location,
1298
mode: transferredSessionData.mode,
1299
});
1300
1301
this.storageService.store(globalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE);
1302
this.chatTransferService.addWorkspaceToTransferred(toWorkspace);
1303
this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`);
1304
}
1305
1306
getChatStorageFolder(): URI {
1307
return this._chatSessionStore.getChatStorageFolder();
1308
}
1309
1310
logChatIndex(): void {
1311
this._chatSessionStore.logIndex();
1312
}
1313
1314
private shouldSaveToHistory(sessionId: string): boolean {
1315
// We shouldn't save contributed sessions from content providers
1316
for (const [_, sessions] of this._contentProviderSessionModels) {
1317
let session: { readonly model: IChatModel; readonly disposables: DisposableStore } | undefined;
1318
for (const entry of sessions.values()) {
1319
if (entry.model.sessionId === sessionId) {
1320
session = entry;
1321
break;
1322
}
1323
}
1324
if (session) {
1325
return false;
1326
}
1327
}
1328
1329
return true;
1330
}
1331
}
1332
1333