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