Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.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 { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../base/common/codicons.js';
8
import { Emitter, Event } from '../../../../base/common/event.js';
9
import { MarkdownString } from '../../../../base/common/htmlContent.js';
10
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
11
import { truncate } from '../../../../base/common/strings.js';
12
import { localize, localize2 } from '../../../../nls.js';
13
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
14
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
15
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
16
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
17
import { ILogService } from '../../../../platform/log/common/log.js';
18
import { IEditableData } from '../../../common/views.js';
19
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
20
import { IEditorService } from '../../../services/editor/common/editorService.js';
21
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
22
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
23
import { IChatWidgetService } from '../browser/chat.js';
24
import { ChatEditorInput } from '../browser/chatEditorInput.js';
25
import { IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js';
26
import { ChatContextKeys } from '../common/chatContextKeys.js';
27
import { IChatProgress, IChatService } from '../common/chatService.js';
28
import { ChatSession, ChatSessionStatus, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService } from '../common/chatSessionsService.js';
29
import { ChatSessionUri } from '../common/chatUri.js';
30
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
31
import { CHAT_CATEGORY } from './actions/chatActions.js';
32
import { IChatEditorOptions } from './chatEditor.js';
33
import { VIEWLET_ID } from './chatSessions.js';
34
35
const CODING_AGENT_DOCS = 'https://code.visualstudio.com/docs/copilot/copilot-coding-agent';
36
37
const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsExtensionPoint[]>({
38
extensionPoint: 'chatSessions',
39
jsonSchema: {
40
description: localize('chatSessionsExtPoint', 'Contributes chat session integrations to the chat widget.'),
41
type: 'array',
42
items: {
43
type: 'object',
44
properties: {
45
type: {
46
description: localize('chatSessionsExtPoint.chatSessionType', 'Unique identifier for the type of chat session.'),
47
type: 'string',
48
},
49
name: {
50
description: localize('chatSessionsExtPoint.name', 'Name shown in the chat widget. (eg: @agent)'),
51
type: 'string',
52
},
53
displayName: {
54
description: localize('chatSessionsExtPoint.displayName', 'A longer name for this item which is used for display in menus.'),
55
type: 'string',
56
},
57
description: {
58
description: localize('chatSessionsExtPoint.description', 'Description of the chat session for use in menus and tooltips.'),
59
type: 'string'
60
},
61
when: {
62
description: localize('chatSessionsExtPoint.when', 'Condition which must be true to show this item.'),
63
type: 'string'
64
},
65
capabilities: {
66
description: localize('chatSessionsExtPoint.capabilities', 'Optional capabilities for this chat session.'),
67
type: 'object',
68
properties: {
69
supportsFileAttachments: {
70
description: localize('chatSessionsExtPoint.supportsFileAttachments', 'Whether this chat session supports attaching files or file references.'),
71
type: 'boolean'
72
},
73
supportsToolAttachments: {
74
description: localize('chatSessionsExtPoint.supportsToolAttachments', 'Whether this chat session supports attaching tools or tool references.'),
75
type: 'boolean'
76
}
77
}
78
}
79
},
80
required: ['type', 'name', 'displayName', 'description'],
81
}
82
},
83
activationEventsGenerator: (contribs, results) => {
84
for (const contrib of contribs) {
85
results.push(`onChatSession:${contrib.type}`);
86
}
87
}
88
});
89
90
class ContributedChatSessionData implements IDisposable {
91
private readonly _disposableStore: DisposableStore;
92
93
constructor(
94
readonly session: ChatSession,
95
readonly chatSessionType: string,
96
readonly id: string,
97
private readonly onWillDispose: (session: ChatSession, chatSessionType: string, id: string) => void
98
) {
99
this._disposableStore = new DisposableStore();
100
this._disposableStore.add(this.session.onWillDispose(() => {
101
this.onWillDispose(this.session, this.chatSessionType, this.id);
102
}));
103
}
104
105
dispose(): void {
106
this._disposableStore.dispose();
107
}
108
}
109
110
111
export class ChatSessionsService extends Disposable implements IChatSessionsService {
112
readonly _serviceBrand: undefined;
113
private readonly _itemsProviders: Map<string, IChatSessionItemProvider> = new Map();
114
115
private readonly _onDidChangeItemsProviders = this._register(new Emitter<IChatSessionItemProvider>());
116
readonly onDidChangeItemsProviders: Event<IChatSessionItemProvider> = this._onDidChangeItemsProviders.event;
117
private readonly _contentProviders: Map<string, IChatSessionContentProvider> = new Map();
118
private readonly _contributions: Map<string, IChatSessionsExtensionPoint> = new Map();
119
private readonly _disposableStores: Map<string, DisposableStore> = new Map();
120
private readonly _contextKeys = new Set<string>();
121
private readonly _onDidChangeSessionItems = this._register(new Emitter<string>());
122
readonly onDidChangeSessionItems: Event<string> = this._onDidChangeSessionItems.event;
123
private readonly _onDidChangeAvailability = this._register(new Emitter<void>());
124
readonly onDidChangeAvailability: Event<void> = this._onDidChangeAvailability.event;
125
private readonly _onDidChangeInProgress = this._register(new Emitter<void>());
126
public get onDidChangeInProgress() { return this._onDidChangeInProgress.event; }
127
private readonly inProgressMap: Map<string, number> = new Map();
128
129
constructor(
130
@ILogService private readonly _logService: ILogService,
131
@IInstantiationService private readonly _instantiationService: IInstantiationService,
132
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
133
@IExtensionService private readonly _extensionService: IExtensionService,
134
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
135
) {
136
super();
137
this._register(extensionPoint.setHandler(extensions => {
138
for (const ext of extensions) {
139
if (!isProposedApiEnabled(ext.description, 'chatSessionsProvider')) {
140
continue;
141
}
142
if (!Array.isArray(ext.value)) {
143
continue;
144
}
145
for (const contribution of ext.value) {
146
const c: IChatSessionsExtensionPoint = {
147
type: contribution.type,
148
name: contribution.name,
149
displayName: contribution.displayName,
150
description: contribution.description,
151
when: contribution.when,
152
capabilities: contribution.capabilities,
153
extensionDescription: ext.description,
154
};
155
this._register(this.registerContribution(c));
156
}
157
}
158
}));
159
160
// Listen for context changes and re-evaluate contributions
161
this._register(Event.filter(this._contextKeyService.onDidChangeContext, e => e.affectsSome(this._contextKeys))(() => {
162
this._evaluateAvailability();
163
}));
164
165
this._register(this.onDidChangeSessionItems(chatSessionType => {
166
this.updateInProgressStatus(chatSessionType).catch(error => {
167
this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error);
168
});
169
}));
170
}
171
172
public reportInProgress(chatSessionType: string, count: number): void {
173
let displayName: string | undefined;
174
175
if (chatSessionType === 'local') {
176
displayName = 'Local Chat Sessions';
177
} else {
178
displayName = this._contributions.get(chatSessionType)?.displayName;
179
}
180
181
if (displayName) {
182
this.inProgressMap.set(displayName, count);
183
}
184
this._onDidChangeInProgress.fire();
185
}
186
187
public getInProgress(): { displayName: string; count: number }[] {
188
return Array.from(this.inProgressMap.entries()).map(([displayName, count]) => ({ displayName, count }));
189
}
190
191
private async updateInProgressStatus(chatSessionType: string): Promise<void> {
192
try {
193
const items = await this.provideChatSessionItems(chatSessionType, CancellationToken.None);
194
const inProgress = items.filter(item => item.status === ChatSessionStatus.InProgress);
195
this.reportInProgress(chatSessionType, inProgress.length);
196
} catch (error) {
197
this._logService.warn(`Failed to update in-progress status for chat session type '${chatSessionType}':`, error);
198
}
199
}
200
201
private registerContribution(contribution: IChatSessionsExtensionPoint): IDisposable {
202
if (this._contributions.has(contribution.type)) {
203
this._logService.warn(`Chat session contribution with id '${contribution.type}' is already registered.`);
204
return { dispose: () => { } };
205
}
206
207
// Track context keys from the when condition
208
if (contribution.when) {
209
const whenExpr = ContextKeyExpr.deserialize(contribution.when);
210
if (whenExpr) {
211
for (const key of whenExpr.keys()) {
212
this._contextKeys.add(key);
213
}
214
}
215
}
216
217
this._contributions.set(contribution.type, contribution);
218
this._evaluateAvailability();
219
220
return {
221
dispose: () => {
222
this._contributions.delete(contribution.type);
223
const store = this._disposableStores.get(contribution.type);
224
if (store) {
225
store.dispose();
226
this._disposableStores.delete(contribution.type);
227
}
228
}
229
};
230
}
231
232
private _isContributionAvailable(contribution: IChatSessionsExtensionPoint): boolean {
233
if (!contribution.when) {
234
return true;
235
}
236
const whenExpr = ContextKeyExpr.deserialize(contribution.when);
237
return !whenExpr || this._contextKeyService.contextMatchesRules(whenExpr);
238
}
239
240
private _registerMenuItems(contribution: IChatSessionsExtensionPoint): IDisposable {
241
return MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
242
command: {
243
id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`,
244
title: localize('interactiveSession.openNewSessionEditor', "New {0} Chat Editor", contribution.displayName),
245
icon: Codicon.plus,
246
},
247
group: 'navigation',
248
order: 1,
249
when: ContextKeyExpr.and(
250
ContextKeyExpr.equals('view', `${VIEWLET_ID}.${contribution.type}`)
251
),
252
});
253
}
254
255
private _registerCommands(contribution: IChatSessionsExtensionPoint): IDisposable {
256
return registerAction2(class OpenNewChatSessionEditorAction extends Action2 {
257
constructor() {
258
super({
259
id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`,
260
title: localize2('interactiveSession.openNewSessionEditor', "New {0} Chat Editor", contribution.displayName),
261
category: CHAT_CATEGORY,
262
icon: Codicon.plus,
263
f1: true, // Show in command palette
264
precondition: ChatContextKeys.enabled
265
});
266
}
267
268
async run(accessor: ServicesAccessor) {
269
const editorService = accessor.get(IEditorService);
270
const logService = accessor.get(ILogService);
271
272
const { type } = contribution;
273
274
try {
275
const options: IChatEditorOptions = {
276
override: ChatEditorInput.EditorID,
277
pinned: true,
278
};
279
await editorService.openEditor({
280
resource: ChatEditorInput.getNewEditorUri().with({ query: `chatSessionType=${type}` }),
281
options,
282
});
283
} catch (e) {
284
logService.error(`Failed to open new '${type}' chat session editor`, e);
285
}
286
}
287
});
288
}
289
290
private _evaluateAvailability(): void {
291
let hasChanges = false;
292
for (const contribution of this._contributions.values()) {
293
const isCurrentlyRegistered = this._disposableStores.has(contribution.type);
294
const shouldBeRegistered = this._isContributionAvailable(contribution);
295
if (isCurrentlyRegistered && !shouldBeRegistered) {
296
// Disable the contribution by disposing its disposable store
297
const store = this._disposableStores.get(contribution.type);
298
if (store) {
299
store.dispose();
300
this._disposableStores.delete(contribution.type);
301
}
302
// Also dispose any cached sessions for this contribution
303
this._disposeSessionsForContribution(contribution.type);
304
hasChanges = true;
305
} else if (!isCurrentlyRegistered && shouldBeRegistered) {
306
// Enable the contribution by registering it
307
this._enableContribution(contribution);
308
hasChanges = true;
309
}
310
}
311
if (hasChanges) {
312
this._onDidChangeAvailability.fire();
313
for (const provider of this._itemsProviders.values()) {
314
this._onDidChangeItemsProviders.fire(provider);
315
}
316
for (const contribution of this._contributions.values()) {
317
this._onDidChangeSessionItems.fire(contribution.type);
318
}
319
}
320
}
321
322
private _enableContribution(contribution: IChatSessionsExtensionPoint): void {
323
const disposableStore = new DisposableStore();
324
this._disposableStores.set(contribution.type, disposableStore);
325
326
disposableStore.add(this._registerDynamicAgent(contribution));
327
disposableStore.add(this._registerCommands(contribution));
328
disposableStore.add(this._registerMenuItems(contribution));
329
}
330
331
private _disposeSessionsForContribution(contributionId: string): void {
332
// Find and dispose all sessions that belong to this contribution
333
const sessionsToDispose: string[] = [];
334
for (const [sessionKey, sessionData] of this._sessions) {
335
if (sessionData.chatSessionType === contributionId) {
336
sessionsToDispose.push(sessionKey);
337
}
338
}
339
340
if (sessionsToDispose.length > 0) {
341
this._logService.info(`Disposing ${sessionsToDispose.length} cached sessions for contribution '${contributionId}' due to when clause change`);
342
}
343
344
for (const sessionKey of sessionsToDispose) {
345
const sessionData = this._sessions.get(sessionKey);
346
if (sessionData) {
347
sessionData.dispose(); // This will call _onWillDisposeSession and clean up
348
}
349
}
350
}
351
352
private _registerDynamicAgent(contribution: IChatSessionsExtensionPoint): IDisposable {
353
const { type: id, name, displayName, description, extensionDescription } = contribution;
354
const { identifier: extensionId, name: extensionName, displayName: extensionDisplayName, publisher: extensionPublisherId } = extensionDescription;
355
const agentData: IChatAgentData = {
356
id,
357
name,
358
fullName: displayName,
359
description: description,
360
isDefault: false,
361
isCore: false,
362
isDynamic: true,
363
slashCommands: [],
364
locations: [ChatAgentLocation.Panel],
365
modes: [ChatModeKind.Agent, ChatModeKind.Ask], // TODO: These are no longer respected
366
disambiguation: [],
367
metadata: {
368
themeIcon: Codicon.sendToRemoteAgent,
369
isSticky: false,
370
},
371
capabilities: contribution.capabilities,
372
extensionId,
373
extensionVersion: extensionDescription.version,
374
extensionDisplayName: extensionDisplayName || extensionName,
375
extensionPublisherId,
376
};
377
378
const agentImpl = this._instantiationService.createInstance(CodingAgentChatImplementation, contribution);
379
const disposable = this._chatAgentService.registerDynamicAgent(agentData, agentImpl);
380
return disposable;
381
}
382
383
getAllChatSessionContributions(): IChatSessionsExtensionPoint[] {
384
return Array.from(this._contributions.values()).filter(contribution =>
385
this._isContributionAvailable(contribution)
386
);
387
}
388
389
getAllChatSessionItemProviders(): IChatSessionItemProvider[] {
390
return [...this._itemsProviders.values()].filter(provider => {
391
// Check if the provider's corresponding contribution is available
392
const contribution = this._contributions.get(provider.chatSessionType);
393
return !contribution || this._isContributionAvailable(contribution);
394
});
395
}
396
397
async canResolveItemProvider(chatViewType: string) {
398
await this._extensionService.whenInstalledExtensionsRegistered();
399
const contribution = this._contributions.get(chatViewType);
400
if (contribution && !this._isContributionAvailable(contribution)) {
401
return false;
402
}
403
404
if (this._itemsProviders.has(chatViewType)) {
405
return true;
406
}
407
408
await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`);
409
410
return this._itemsProviders.has(chatViewType);
411
}
412
413
async canResolveContentProvider(chatViewType: string) {
414
await this._extensionService.whenInstalledExtensionsRegistered();
415
const contribution = this._contributions.get(chatViewType);
416
if (contribution && !this._isContributionAvailable(contribution)) {
417
return false;
418
}
419
420
if (this._contentProviders.has(chatViewType)) {
421
return true;
422
}
423
424
await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`);
425
426
return this._contentProviders.has(chatViewType);
427
}
428
429
public async provideChatSessionItems(chatSessionType: string, token: CancellationToken): Promise<IChatSessionItem[]> {
430
if (!(await this.canResolveItemProvider(chatSessionType))) {
431
throw Error(`Can not find provider for ${chatSessionType}`);
432
}
433
434
const provider = this._itemsProviders.get(chatSessionType);
435
436
if (provider?.provideChatSessionItems) {
437
const sessions = await provider.provideChatSessionItems(token);
438
return sessions;
439
}
440
441
return [];
442
}
443
444
public registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable {
445
const chatSessionType = provider.chatSessionType;
446
this._itemsProviders.set(chatSessionType, provider);
447
this._onDidChangeItemsProviders.fire(provider);
448
449
const disposables = new DisposableStore();
450
disposables.add(provider.onDidChangeChatSessionItems(() => {
451
this._onDidChangeSessionItems.fire(chatSessionType);
452
}));
453
454
this.updateInProgressStatus(chatSessionType).catch(error => {
455
this._logService.warn(`Failed to update initial progress status for '${chatSessionType}':`, error);
456
});
457
458
return {
459
dispose: () => {
460
disposables.dispose();
461
462
const provider = this._itemsProviders.get(chatSessionType);
463
if (provider) {
464
this._itemsProviders.delete(chatSessionType);
465
this._onDidChangeItemsProviders.fire(provider);
466
}
467
}
468
};
469
}
470
471
registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable {
472
this._contentProviders.set(chatSessionType, provider);
473
return {
474
dispose: () => {
475
this._contentProviders.delete(chatSessionType);
476
477
// Remove all sessions that were created by this provider
478
for (const [key, session] of this._sessions) {
479
if (session.chatSessionType === chatSessionType) {
480
session.dispose();
481
this._sessions.delete(key);
482
}
483
}
484
}
485
};
486
}
487
488
private readonly _sessions = new Map<string, ContributedChatSessionData>();
489
490
// Editable session support
491
private readonly _editableSessions = new Map<string, IEditableData>();
492
493
/**
494
* Creates a new chat session by delegating to the appropriate provider
495
* @param chatSessionType The type of chat session provider to use
496
* @param options Options for the new session including the request
497
* @param token A cancellation token
498
* @returns A session ID for the newly created session
499
*/
500
public async provideNewChatSessionItem(chatSessionType: string, options: {
501
request: IChatAgentRequest;
502
prompt?: string;
503
history?: any[];
504
metadata?: any;
505
}, token: CancellationToken): Promise<IChatSessionItem> {
506
if (!(await this.canResolveItemProvider(chatSessionType))) {
507
throw Error(`Cannot find provider for ${chatSessionType}`);
508
}
509
510
const provider = this._itemsProviders.get(chatSessionType);
511
if (!provider?.provideNewChatSessionItem) {
512
throw Error(`Provider for ${chatSessionType} does not support creating sessions`);
513
}
514
const chatSessionItem = await provider.provideNewChatSessionItem(options, token);
515
this._onDidChangeSessionItems.fire(chatSessionType);
516
return chatSessionItem;
517
}
518
519
public async provideChatSessionContent(chatSessionType: string, id: string, token: CancellationToken): Promise<ChatSession> {
520
if (!(await this.canResolveContentProvider(chatSessionType))) {
521
throw Error(`Can not find provider for ${chatSessionType}`);
522
}
523
524
const provider = this._contentProviders.get(chatSessionType);
525
if (!provider) {
526
throw Error(`Can not find provider for ${chatSessionType}`);
527
}
528
529
const sessionKey = `${chatSessionType}_${id}`;
530
const existingSessionData = this._sessions.get(sessionKey);
531
if (existingSessionData) {
532
return existingSessionData.session;
533
}
534
535
const session = await provider.provideChatSessionContent(id, token);
536
const sessionData = new ContributedChatSessionData(session, chatSessionType, id, this._onWillDisposeSession.bind(this));
537
538
this._sessions.set(sessionKey, sessionData);
539
540
return session;
541
}
542
543
private _onWillDisposeSession(session: ChatSession, chatSessionType: string, id: string): void {
544
const sessionKey = `${chatSessionType}_${id}`;
545
this._sessions.delete(sessionKey);
546
}
547
548
// Implementation of editable session methods
549
public async setEditableSession(sessionId: string, data: IEditableData | null): Promise<void> {
550
if (!data) {
551
this._editableSessions.delete(sessionId);
552
} else {
553
this._editableSessions.set(sessionId, data);
554
}
555
// Trigger refresh of the session views that might need to update their rendering
556
this._onDidChangeSessionItems.fire('local');
557
}
558
559
public getEditableData(sessionId: string): IEditableData | undefined {
560
return this._editableSessions.get(sessionId);
561
}
562
563
public isEditable(sessionId: string): boolean {
564
return this._editableSessions.has(sessionId);
565
}
566
567
public notifySessionItemsChanged(chatSessionType: string): void {
568
this._onDidChangeSessionItems.fire(chatSessionType);
569
}
570
}
571
572
registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);
573
574
/**
575
* Implementation for individual remote coding agent chat functionality
576
*/
577
class CodingAgentChatImplementation extends Disposable implements IChatAgentImplementation {
578
579
constructor(
580
private readonly chatSession: IChatSessionsExtensionPoint,
581
@IChatService private readonly chatService: IChatService,
582
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
583
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
584
@IChatSessionsService private readonly chatSessionService: IChatSessionsService,
585
@IEditorService private readonly editorService: IEditorService,
586
@ILogService private readonly logService: ILogService,
587
) {
588
super();
589
}
590
591
async invoke(request: IChatAgentRequest, progress: (progress: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> {
592
const widget = this.chatWidgetService.getWidgetBySessionId(request.sessionId);
593
594
if (!widget) {
595
return {};
596
}
597
598
let chatSession: ChatSession | undefined;
599
600
// Find the first editor that matches the chat session
601
for (const group of this.editorGroupService.groups) {
602
if (chatSession) {
603
break;
604
}
605
606
for (const editor of group.editors) {
607
if (editor instanceof ChatEditorInput) {
608
try {
609
const chatModel = await this.chatService.loadSessionForResource(editor.resource, request.location, CancellationToken.None);
610
if (chatModel?.sessionId === request.sessionId) {
611
// this is the model
612
const identifier = ChatSessionUri.parse(editor.resource);
613
614
if (identifier) {
615
chatSession = await this.chatSessionService.provideChatSessionContent(this.chatSession.type, identifier.sessionId, token);
616
}
617
break;
618
}
619
} catch (error) {
620
// might not be us
621
}
622
}
623
}
624
}
625
626
if (chatSession?.requestHandler) {
627
await chatSession.requestHandler(request, progress, history, token); // TODO: Revisit this function's signature in relation to its extension API (eg: 'history' is not strongly typed here)
628
} else {
629
try {
630
const chatSessionItem = await this.chatSessionService.provideNewChatSessionItem(
631
this.chatSession.type,
632
{
633
request,
634
prompt: request.message,
635
history,
636
},
637
token,
638
);
639
const options: IChatEditorOptions = {
640
pinned: true,
641
preferredTitle: truncate(chatSessionItem.label, 30),
642
};
643
644
// Prefetch the chat session content to make the subsequent editor swap quick
645
await this.chatSessionService.provideChatSessionContent(
646
this.chatSession.type,
647
chatSessionItem.id,
648
token,
649
);
650
651
const activeGroup = this.editorGroupService.activeGroup;
652
const currentEditor = activeGroup?.activeEditor;
653
if (currentEditor instanceof ChatEditorInput) {
654
await this.editorService.replaceEditors([{
655
editor: currentEditor,
656
replacement: {
657
resource: ChatSessionUri.forSession(this.chatSession.type, chatSessionItem.id),
658
options,
659
}
660
}], activeGroup);
661
} else {
662
// Fallback: open in new editor if we couldn't find the current one
663
await this.editorService.openEditor({
664
resource: ChatSessionUri.forSession(this.chatSession.type, chatSessionItem.id),
665
options,
666
});
667
progress([{
668
kind: 'markdownContent',
669
content: new MarkdownString(localize('continueInNewChat', 'Continue **{0}** in a new chat editor', truncate(chatSessionItem.label, 30))),
670
}]);
671
}
672
} catch (error) {
673
// NOTE: May end up here if extension does not support 'provideNewChatSessionItem' or that API usage throws
674
this.logService.error(`Failed to create new chat session for type '${this.chatSession.type}'`, error);
675
const content =
676
this.chatSession.type === 'copilot-swe-agent' // TODO: Use contributed error messages
677
? new MarkdownString(localize('chatSessionNotFoundCopilot', "Failed to create chat session. Use `#copilotCodingAgent` to begin a new [coding agent session]({0}).", CODING_AGENT_DOCS))
678
: new MarkdownString(localize('chatSessionNotFoundGeneric', "Failed to create chat session. Please try again later."));
679
progress([{
680
kind: 'markdownContent',
681
content,
682
}]);
683
}
684
}
685
686
return {};
687
}
688
}
689
690