Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.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 { sep } from '../../../../../base/common/path.js';
7
import { raceCancellationError } from '../../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { Emitter, Event } from '../../../../../base/common/event.js';
11
import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
12
import { ResourceMap } from '../../../../../base/common/map.js';
13
import { Schemas } from '../../../../../base/common/network.js';
14
import * as resources from '../../../../../base/common/resources.js';
15
import { ThemeIcon } from '../../../../../base/common/themables.js';
16
import { URI, UriComponents } from '../../../../../base/common/uri.js';
17
import { generateUuid } from '../../../../../base/common/uuid.js';
18
import { localize, localize2 } from '../../../../../nls.js';
19
import { Action2, IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
20
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
21
import { IRelaxedExtensionDescription } from '../../../../../platform/extensions/common/extensions.js';
22
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
23
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
24
import { ILabelService } from '../../../../../platform/label/common/label.js';
25
import { ILogService } from '../../../../../platform/log/common/log.js';
26
import { isDark } from '../../../../../platform/theme/common/theme.js';
27
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
28
import { IEditorService } from '../../../../services/editor/common/editorService.js';
29
import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';
30
import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js';
31
import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
32
import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';
33
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
34
import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js';
35
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
36
import { CHAT_CATEGORY } from '../actions/chatActions.js';
37
import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';
38
import { IChatModel } from '../../common/model/chatModel.js';
39
import { IChatService, IChatToolInvocation } from '../../common/chatService/chatService.js';
40
import { autorun, autorunIterableDelta, observableSignalFromEvent } from '../../../../../base/common/observable.js';
41
import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js';
42
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
43
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
44
import { IViewsService } from '../../../../services/views/common/viewsService.js';
45
import { ChatViewId } from '../chat.js';
46
import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';
47
import { AgentSessionProviders } from '../agentSessions/agentSessions.js';
48
49
const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsExtensionPoint[]>({
50
extensionPoint: 'chatSessions',
51
jsonSchema: {
52
description: localize('chatSessionsExtPoint', 'Contributes chat session integrations to the chat widget.'),
53
type: 'array',
54
items: {
55
type: 'object',
56
additionalProperties: false,
57
properties: {
58
type: {
59
description: localize('chatSessionsExtPoint.chatSessionType', 'Unique identifier for the type of chat session.'),
60
type: 'string',
61
},
62
name: {
63
description: localize('chatSessionsExtPoint.name', 'Name of the dynamically registered chat participant (eg: @agent). Must not contain whitespace.'),
64
type: 'string',
65
pattern: '^[\\w-]+$'
66
},
67
displayName: {
68
description: localize('chatSessionsExtPoint.displayName', 'A longer name for this item which is used for display in menus.'),
69
type: 'string',
70
},
71
description: {
72
description: localize('chatSessionsExtPoint.description', 'Description of the chat session for use in menus and tooltips.'),
73
type: 'string'
74
},
75
when: {
76
description: localize('chatSessionsExtPoint.when', 'Condition which must be true to show this item.'),
77
type: 'string'
78
},
79
icon: {
80
description: localize('chatSessionsExtPoint.icon', 'Icon identifier (codicon ID) for the chat session editor tab. For example, "$(github)" or "$(cloud)".'),
81
anyOf: [{
82
type: 'string'
83
},
84
{
85
type: 'object',
86
properties: {
87
light: {
88
description: localize('icon.light', 'Icon path when a light theme is used'),
89
type: 'string'
90
},
91
dark: {
92
description: localize('icon.dark', 'Icon path when a dark theme is used'),
93
type: 'string'
94
}
95
}
96
}]
97
},
98
order: {
99
description: localize('chatSessionsExtPoint.order', 'Order in which this item should be displayed.'),
100
type: 'integer'
101
},
102
alternativeIds: {
103
description: localize('chatSessionsExtPoint.alternativeIds', 'Alternative identifiers for backward compatibility.'),
104
type: 'array',
105
items: {
106
type: 'string'
107
}
108
},
109
welcomeTitle: {
110
description: localize('chatSessionsExtPoint.welcomeTitle', 'Title text to display in the chat welcome view for this session type.'),
111
type: 'string'
112
},
113
welcomeMessage: {
114
description: localize('chatSessionsExtPoint.welcomeMessage', 'Message text (supports markdown) to display in the chat welcome view for this session type.'),
115
type: 'string'
116
},
117
welcomeTips: {
118
description: localize('chatSessionsExtPoint.welcomeTips', 'Tips text (supports markdown and theme icons) to display in the chat welcome view for this session type.'),
119
type: 'string'
120
},
121
inputPlaceholder: {
122
description: localize('chatSessionsExtPoint.inputPlaceholder', 'Placeholder text to display in the chat input box for this session type.'),
123
type: 'string'
124
},
125
capabilities: {
126
description: localize('chatSessionsExtPoint.capabilities', 'Optional capabilities for this chat session.'),
127
type: 'object',
128
additionalProperties: false,
129
properties: {
130
supportsFileAttachments: {
131
description: localize('chatSessionsExtPoint.supportsFileAttachments', 'Whether this chat session supports attaching files or file references.'),
132
type: 'boolean'
133
},
134
supportsToolAttachments: {
135
description: localize('chatSessionsExtPoint.supportsToolAttachments', 'Whether this chat session supports attaching tools or tool references.'),
136
type: 'boolean'
137
},
138
supportsMCPAttachments: {
139
description: localize('chatSessionsExtPoint.supportsMCPAttachments', 'Whether this chat session supports attaching MCP resources.'),
140
type: 'boolean'
141
},
142
supportsImageAttachments: {
143
description: localize('chatSessionsExtPoint.supportsImageAttachments', 'Whether this chat session supports attaching images.'),
144
type: 'boolean'
145
},
146
supportsSearchResultAttachments: {
147
description: localize('chatSessionsExtPoint.supportsSearchResultAttachments', 'Whether this chat session supports attaching search results.'),
148
type: 'boolean'
149
},
150
supportsInstructionAttachments: {
151
description: localize('chatSessionsExtPoint.supportsInstructionAttachments', 'Whether this chat session supports attaching instructions.'),
152
type: 'boolean'
153
},
154
supportsSourceControlAttachments: {
155
description: localize('chatSessionsExtPoint.supportsSourceControlAttachments', 'Whether this chat session supports attaching source control changes.'),
156
type: 'boolean'
157
},
158
supportsProblemAttachments: {
159
description: localize('chatSessionsExtPoint.supportsProblemAttachments', 'Whether this chat session supports attaching problems.'),
160
type: 'boolean'
161
},
162
supportsSymbolAttachments: {
163
description: localize('chatSessionsExtPoint.supportsSymbolAttachments', 'Whether this chat session supports attaching symbols.'),
164
type: 'boolean'
165
}
166
}
167
},
168
commands: {
169
markdownDescription: localize('chatCommandsDescription', "Commands available for this chat session, which the user can invoke with a `/`."),
170
type: 'array',
171
items: {
172
additionalProperties: false,
173
type: 'object',
174
defaultSnippets: [{ body: { name: '', description: '' } }],
175
required: ['name'],
176
properties: {
177
name: {
178
description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."),
179
type: 'string'
180
},
181
description: {
182
description: localize('chatCommandDescription', "A description of this command."),
183
type: 'string'
184
},
185
when: {
186
description: localize('chatCommandWhen', "A condition which must be true to enable this command."),
187
type: 'string'
188
},
189
}
190
}
191
},
192
canDelegate: {
193
description: localize('chatSessionsExtPoint.canDelegate', 'Whether delegation is supported. Default is false. Note that enabling this is experimental and may not be respected at all times.'),
194
type: 'boolean',
195
default: false
196
}
197
},
198
required: ['type', 'name', 'displayName', 'description'],
199
}
200
},
201
activationEventsGenerator: function* (contribs) {
202
for (const contrib of contribs) {
203
yield `onChatSession:${contrib.type}`;
204
}
205
}
206
});
207
208
class ContributedChatSessionData extends Disposable {
209
210
private readonly _optionsCache: Map<string /* 'models' */, string | IChatSessionProviderOptionItem>;
211
public getOption(optionId: string): string | IChatSessionProviderOptionItem | undefined {
212
return this._optionsCache.get(optionId);
213
}
214
public setOption(optionId: string, value: string | IChatSessionProviderOptionItem): void {
215
this._optionsCache.set(optionId, value);
216
}
217
218
constructor(
219
readonly session: IChatSession,
220
readonly chatSessionType: string,
221
readonly resource: URI,
222
readonly options: Record<string, string | IChatSessionProviderOptionItem> | undefined,
223
private readonly onWillDispose: (resource: URI) => void
224
) {
225
super();
226
227
this._optionsCache = new Map<string, string | IChatSessionProviderOptionItem>();
228
if (options) {
229
for (const [key, value] of Object.entries(options)) {
230
this._optionsCache.set(key, value);
231
}
232
}
233
234
this._register(this.session.onWillDispose(() => {
235
this.onWillDispose(this.resource);
236
}));
237
}
238
}
239
240
241
export class ChatSessionsService extends Disposable implements IChatSessionsService {
242
readonly _serviceBrand: undefined;
243
244
private readonly _itemsProviders: Map</* type */ string, IChatSessionItemProvider> = new Map();
245
246
private readonly _contributions: Map</* type */ string, { readonly contribution: IChatSessionsExtensionPoint; readonly extension: IRelaxedExtensionDescription }> = new Map();
247
private readonly _contributionDisposables = this._register(new DisposableMap</* type */ string>());
248
249
private readonly _contentProviders: Map</* scheme */ string, IChatSessionContentProvider> = new Map();
250
private readonly _alternativeIdMap: Map</* alternativeId */ string, /* primaryType */ string> = new Map();
251
private readonly _contextKeys = new Set<string>();
252
253
private readonly _onDidChangeItemsProviders = this._register(new Emitter<IChatSessionItemProvider>());
254
readonly onDidChangeItemsProviders: Event<IChatSessionItemProvider> = this._onDidChangeItemsProviders.event;
255
256
private readonly _onDidChangeSessionItems = this._register(new Emitter<string>());
257
readonly onDidChangeSessionItems: Event<string> = this._onDidChangeSessionItems.event;
258
259
private readonly _onDidChangeAvailability = this._register(new Emitter<void>());
260
readonly onDidChangeAvailability: Event<void> = this._onDidChangeAvailability.event;
261
262
private readonly _onDidChangeInProgress = this._register(new Emitter<void>());
263
public get onDidChangeInProgress() { return this._onDidChangeInProgress.event; }
264
265
private readonly _onDidChangeContentProviderSchemes = this._register(new Emitter<{ readonly added: string[]; readonly removed: string[] }>());
266
public get onDidChangeContentProviderSchemes() { return this._onDidChangeContentProviderSchemes.event; }
267
private readonly _onDidChangeSessionOptions = this._register(new Emitter<URI>());
268
public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; }
269
private readonly _onDidChangeOptionGroups = this._register(new Emitter<string>());
270
public get onDidChangeOptionGroups() { return this._onDidChangeOptionGroups.event; }
271
272
private readonly inProgressMap: Map<string, number> = new Map();
273
private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();
274
private readonly _sessionTypeIcons: Map<string, ThemeIcon | { light: URI; dark: URI }> = new Map();
275
private readonly _sessionTypeWelcomeTitles: Map<string, string> = new Map();
276
private readonly _sessionTypeWelcomeMessages: Map<string, string> = new Map();
277
private readonly _sessionTypeWelcomeTips: Map<string, string> = new Map();
278
private readonly _sessionTypeInputPlaceholders: Map<string, string> = new Map();
279
280
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
281
282
private readonly _hasCanDelegateProvidersKey: IContextKey<boolean>;
283
284
constructor(
285
@ILogService private readonly _logService: ILogService,
286
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
287
@IExtensionService private readonly _extensionService: IExtensionService,
288
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
289
@IMenuService private readonly _menuService: IMenuService,
290
@IThemeService private readonly _themeService: IThemeService,
291
@ILabelService private readonly _labelService: ILabelService
292
) {
293
super();
294
295
this._hasCanDelegateProvidersKey = ChatContextKeys.hasCanDelegateProviders.bindTo(this._contextKeyService);
296
297
this._register(extensionPoint.setHandler(extensions => {
298
for (const ext of extensions) {
299
if (!isProposedApiEnabled(ext.description, 'chatSessionsProvider')) {
300
continue;
301
}
302
if (!Array.isArray(ext.value)) {
303
continue;
304
}
305
for (const contribution of ext.value) {
306
this._register(this.registerContribution(contribution, ext.description));
307
}
308
}
309
}));
310
311
// Listen for context changes and re-evaluate contributions
312
this._register(Event.filter(this._contextKeyService.onDidChangeContext, e => e.affectsSome(this._contextKeys))(() => {
313
this._evaluateAvailability();
314
}));
315
316
this._register(this.onDidChangeSessionItems(chatSessionType => {
317
this.updateInProgressStatus(chatSessionType).catch(error => {
318
this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error);
319
});
320
}));
321
322
this._register(this._labelService.registerFormatter({
323
scheme: Schemas.copilotPr,
324
formatting: {
325
label: '${authority}${path}',
326
separator: sep,
327
stripPathStartingSeparator: true,
328
}
329
}));
330
}
331
332
public reportInProgress(chatSessionType: string, count: number): void {
333
let displayName: string | undefined;
334
335
if (chatSessionType === AgentSessionProviders.Local) {
336
displayName = localize('chat.session.inProgress.local', "Local Agent");
337
} else if (chatSessionType === AgentSessionProviders.Background) {
338
displayName = localize('chat.session.inProgress.background', "Background Agent");
339
} else if (chatSessionType === AgentSessionProviders.Cloud) {
340
displayName = localize('chat.session.inProgress.cloud', "Cloud Agent");
341
} else {
342
displayName = this._contributions.get(chatSessionType)?.contribution.displayName;
343
}
344
345
if (displayName) {
346
this.inProgressMap.set(displayName, count);
347
}
348
this._onDidChangeInProgress.fire();
349
}
350
351
public getInProgress(): { displayName: string; count: number }[] {
352
return Array.from(this.inProgressMap.entries()).map(([displayName, count]) => ({ displayName, count }));
353
}
354
355
private async updateInProgressStatus(chatSessionType: string): Promise<void> {
356
try {
357
const items = await this.getChatSessionItems(chatSessionType, CancellationToken.None);
358
const inProgress = items.filter(item => item.status && isSessionInProgressStatus(item.status));
359
this.reportInProgress(chatSessionType, inProgress.length);
360
} catch (error) {
361
this._logService.warn(`Failed to update in-progress status for chat session type '${chatSessionType}':`, error);
362
}
363
}
364
365
private registerContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {
366
if (this._contributions.has(contribution.type)) {
367
return { dispose: () => { } };
368
}
369
370
// Track context keys from the when condition
371
if (contribution.when) {
372
const whenExpr = ContextKeyExpr.deserialize(contribution.when);
373
if (whenExpr) {
374
for (const key of whenExpr.keys()) {
375
this._contextKeys.add(key);
376
}
377
}
378
}
379
380
this._contributions.set(contribution.type, { contribution, extension: ext });
381
382
// Register alternative IDs if provided
383
if (contribution.alternativeIds) {
384
for (const altId of contribution.alternativeIds) {
385
if (this._alternativeIdMap.has(altId)) {
386
this._logService.warn(`Alternative ID '${altId}' is already mapped to '${this._alternativeIdMap.get(altId)}'. Remapping to '${contribution.type}'.`);
387
}
388
this._alternativeIdMap.set(altId, contribution.type);
389
}
390
}
391
392
// Store icon mapping if provided
393
let icon: ThemeIcon | { dark: URI; light: URI } | undefined;
394
395
if (contribution.icon) {
396
// Parse icon string - support ThemeIcon format or file path from extension
397
if (typeof contribution.icon === 'string') {
398
icon = contribution.icon.startsWith('$(') && contribution.icon.endsWith(')')
399
? ThemeIcon.fromString(contribution.icon)
400
: ThemeIcon.fromId(contribution.icon);
401
} else {
402
icon = {
403
dark: resources.joinPath(ext.extensionLocation, contribution.icon.dark),
404
light: resources.joinPath(ext.extensionLocation, contribution.icon.light)
405
};
406
}
407
}
408
409
if (icon) {
410
this._sessionTypeIcons.set(contribution.type, icon);
411
}
412
413
// Store welcome title, message, tips, and input placeholder if provided
414
if (contribution.welcomeTitle) {
415
this._sessionTypeWelcomeTitles.set(contribution.type, contribution.welcomeTitle);
416
}
417
if (contribution.welcomeMessage) {
418
this._sessionTypeWelcomeMessages.set(contribution.type, contribution.welcomeMessage);
419
}
420
if (contribution.welcomeTips) {
421
this._sessionTypeWelcomeTips.set(contribution.type, contribution.welcomeTips);
422
}
423
if (contribution.inputPlaceholder) {
424
this._sessionTypeInputPlaceholders.set(contribution.type, contribution.inputPlaceholder);
425
}
426
427
this._evaluateAvailability();
428
429
return {
430
dispose: () => {
431
this._contributions.delete(contribution.type);
432
// Remove alternative ID mappings
433
if (contribution.alternativeIds) {
434
for (const altId of contribution.alternativeIds) {
435
if (this._alternativeIdMap.get(altId) === contribution.type) {
436
this._alternativeIdMap.delete(altId);
437
}
438
}
439
}
440
this._sessionTypeIcons.delete(contribution.type);
441
this._sessionTypeWelcomeTitles.delete(contribution.type);
442
this._sessionTypeWelcomeMessages.delete(contribution.type);
443
this._sessionTypeWelcomeTips.delete(contribution.type);
444
this._sessionTypeInputPlaceholders.delete(contribution.type);
445
this._contributionDisposables.deleteAndDispose(contribution.type);
446
this._updateHasCanDelegateProvidersContextKey();
447
}
448
};
449
}
450
451
private _isContributionAvailable(contribution: IChatSessionsExtensionPoint): boolean {
452
if (!contribution.when) {
453
return true;
454
}
455
const whenExpr = ContextKeyExpr.deserialize(contribution.when);
456
return !whenExpr || this._contextKeyService.contextMatchesRules(whenExpr);
457
}
458
459
/**
460
* Resolves a session type to its primary type, checking for alternative IDs.
461
* @param sessionType The session type or alternative ID to resolve
462
* @returns The primary session type, or undefined if not found or not available
463
*/
464
private _resolveToPrimaryType(sessionType: string): string | undefined {
465
// Try to find the primary type first
466
const contribution = this._contributions.get(sessionType)?.contribution;
467
if (contribution) {
468
// If the contribution is available, use it
469
if (this._isContributionAvailable(contribution)) {
470
return sessionType;
471
}
472
// If not available, fall through to check for alternatives
473
}
474
475
// Check if this is an alternative ID, or if the primary type is not available
476
const primaryType = this._alternativeIdMap.get(sessionType);
477
if (primaryType) {
478
const altContribution = this._contributions.get(primaryType)?.contribution;
479
if (altContribution && this._isContributionAvailable(altContribution)) {
480
return primaryType;
481
}
482
}
483
484
return undefined;
485
}
486
487
private _registerMenuItems(contribution: IChatSessionsExtensionPoint, extensionDescription: IRelaxedExtensionDescription): IDisposable {
488
// If provider registers anything for the create submenu, let it fully control the creation
489
const contextKeyService = this._contextKeyService.createOverlay([
490
['chatSessionType', contribution.type]
491
]);
492
493
const rawMenuActions = this._menuService.getMenuActions(MenuId.AgentSessionsCreateSubMenu, contextKeyService);
494
const menuActions = rawMenuActions.map(value => value[1]).flat();
495
496
const disposables = new DisposableStore();
497
498
// Mirror all create submenu actions into the global Chat New menu
499
for (const action of menuActions) {
500
if (action instanceof MenuItemAction) {
501
disposables.add(MenuRegistry.appendMenuItem(MenuId.ChatNewMenu, {
502
command: action.item,
503
group: '4_externally_contributed',
504
}));
505
}
506
}
507
return {
508
dispose: () => disposables.dispose()
509
};
510
}
511
512
private _registerCommands(contribution: IChatSessionsExtensionPoint): IDisposable {
513
return combinedDisposable(
514
registerAction2(class OpenChatSessionAction extends Action2 {
515
constructor() {
516
super({
517
id: `workbench.action.chat.openSessionWithPrompt.${contribution.type}`,
518
title: localize2('interactiveSession.openSessionWithPrompt', "New {0} with Prompt", contribution.displayName),
519
category: CHAT_CATEGORY,
520
icon: Codicon.plus,
521
f1: false,
522
precondition: ChatContextKeys.enabled
523
});
524
}
525
526
async run(accessor: ServicesAccessor, chatOptions?: { resource: UriComponents; prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {
527
const chatService = accessor.get(IChatService);
528
const { type } = contribution;
529
530
if (chatOptions) {
531
const resource = URI.revive(chatOptions.resource);
532
const ref = await chatService.loadSessionForResource(resource, ChatAgentLocation.Chat, CancellationToken.None);
533
await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });
534
ref?.dispose();
535
}
536
}
537
}),
538
// Creates a chat editor
539
registerAction2(class OpenNewChatSessionEditorAction extends Action2 {
540
constructor() {
541
super({
542
id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`,
543
title: localize2('interactiveSession.openNewSessionEditor', "New {0}", contribution.displayName),
544
category: CHAT_CATEGORY,
545
icon: Codicon.plus,
546
f1: true,
547
precondition: ChatContextKeys.enabled,
548
});
549
}
550
551
async run(accessor: ServicesAccessor, chatOptions?: { prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {
552
const editorService = accessor.get(IEditorService);
553
const logService = accessor.get(ILogService);
554
const chatService = accessor.get(IChatService);
555
const { type } = contribution;
556
557
try {
558
const options: IChatEditorOptions = {
559
override: ChatEditorInput.EditorID,
560
pinned: true,
561
title: {
562
fallback: localize('chatEditorContributionName', "{0}", contribution.displayName),
563
}
564
};
565
const resource = URI.from({
566
scheme: type,
567
path: `/untitled-${generateUuid()}`,
568
});
569
await editorService.openEditor({ resource, options });
570
if (chatOptions?.prompt) {
571
await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });
572
}
573
} catch (e) {
574
logService.error(`Failed to open new '${type}' chat session editor`, e);
575
}
576
}
577
}),
578
// New chat in sidebar chat (+ button)
579
registerAction2(class OpenNewChatSessionSidebarAction extends Action2 {
580
constructor() {
581
super({
582
id: `workbench.action.chat.openNewSessionSidebar.${contribution.type}`,
583
title: localize2('interactiveSession.openNewSessionSidebar', "New {0}", contribution.displayName),
584
category: CHAT_CATEGORY,
585
icon: Codicon.plus,
586
f1: false, // Hide from Command Palette
587
precondition: ChatContextKeys.enabled,
588
menu: {
589
id: MenuId.ChatNewMenu,
590
group: '3_new_special',
591
}
592
});
593
}
594
595
async run(accessor: ServicesAccessor, chatOptions?: { prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {
596
const viewsService = accessor.get(IViewsService);
597
const logService = accessor.get(ILogService);
598
const chatService = accessor.get(IChatService);
599
const { type } = contribution;
600
601
try {
602
const resource = URI.from({
603
scheme: type,
604
path: `/untitled-${generateUuid()}`,
605
});
606
607
const view = await viewsService.openView(ChatViewId) as ChatViewPane;
608
await view.loadSession(resource);
609
if (chatOptions?.prompt) {
610
await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });
611
}
612
view.focus();
613
} catch (e) {
614
logService.error(`Failed to open new '${type}' chat session in sidebar`, e);
615
}
616
}
617
})
618
);
619
}
620
621
private _evaluateAvailability(): void {
622
let hasChanges = false;
623
for (const { contribution, extension } of this._contributions.values()) {
624
const isCurrentlyRegistered = this._contributionDisposables.has(contribution.type);
625
const shouldBeRegistered = this._isContributionAvailable(contribution);
626
if (isCurrentlyRegistered && !shouldBeRegistered) {
627
// Disable the contribution by disposing its disposable store
628
this._contributionDisposables.deleteAndDispose(contribution.type);
629
630
// Also dispose any cached sessions for this contribution
631
this._disposeSessionsForContribution(contribution.type);
632
hasChanges = true;
633
} else if (!isCurrentlyRegistered && shouldBeRegistered) {
634
// Enable the contribution by registering it
635
this._enableContribution(contribution, extension);
636
hasChanges = true;
637
}
638
}
639
if (hasChanges) {
640
this._onDidChangeAvailability.fire();
641
for (const provider of this._itemsProviders.values()) {
642
this._onDidChangeItemsProviders.fire(provider);
643
}
644
for (const { contribution } of this._contributions.values()) {
645
this._onDidChangeSessionItems.fire(contribution.type);
646
}
647
}
648
this._updateHasCanDelegateProvidersContextKey();
649
}
650
651
private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void {
652
const disposableStore = new DisposableStore();
653
this._contributionDisposables.set(contribution.type, disposableStore);
654
if (contribution.canDelegate) {
655
disposableStore.add(this._registerAgent(contribution, ext));
656
disposableStore.add(this._registerCommands(contribution));
657
}
658
disposableStore.add(this._registerMenuItems(contribution, ext));
659
}
660
661
private _disposeSessionsForContribution(contributionId: string): void {
662
// Find and dispose all sessions that belong to this contribution
663
const sessionsToDispose: URI[] = [];
664
for (const [sessionResource, sessionData] of this._sessions) {
665
if (sessionData.chatSessionType === contributionId) {
666
sessionsToDispose.push(sessionResource);
667
}
668
}
669
670
if (sessionsToDispose.length > 0) {
671
this._logService.info(`Disposing ${sessionsToDispose.length} cached sessions for contribution '${contributionId}' due to when clause change`);
672
}
673
674
for (const sessionKey of sessionsToDispose) {
675
const sessionData = this._sessions.get(sessionKey);
676
if (sessionData) {
677
sessionData.dispose(); // This will call _onWillDisposeSession and clean up
678
}
679
}
680
}
681
682
private _registerAgent(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {
683
const { type: id, name, displayName, description } = contribution;
684
const storedIcon = this._sessionTypeIcons.get(id);
685
const icons = ThemeIcon.isThemeIcon(storedIcon)
686
? { themeIcon: storedIcon, icon: undefined, iconDark: undefined }
687
: storedIcon
688
? { icon: storedIcon.light, iconDark: storedIcon.dark }
689
: { themeIcon: Codicon.sendToRemoteAgent };
690
691
const agentData: IChatAgentData = {
692
id,
693
name,
694
fullName: displayName,
695
description: description,
696
isDefault: false,
697
isCore: false,
698
isDynamic: true,
699
slashCommands: contribution.commands ?? [],
700
locations: [ChatAgentLocation.Chat],
701
modes: [ChatModeKind.Agent, ChatModeKind.Ask],
702
disambiguation: [],
703
metadata: {
704
...icons,
705
},
706
capabilities: contribution.capabilities,
707
canAccessPreviousChatHistory: true,
708
extensionId: ext.identifier,
709
extensionVersion: ext.version,
710
extensionDisplayName: ext.displayName || ext.name,
711
extensionPublisherId: ext.publisher,
712
};
713
714
return this._chatAgentService.registerAgent(id, agentData);
715
}
716
717
getAllChatSessionContributions(): IChatSessionsExtensionPoint[] {
718
return Array.from(this._contributions.values(), x => x.contribution)
719
.filter(contribution => this._isContributionAvailable(contribution));
720
}
721
722
private _updateHasCanDelegateProvidersContextKey(): void {
723
const hasCanDelegate = this.getAllChatSessionContributions().filter(c => c.canDelegate);
724
const canDelegateEnabled = hasCanDelegate.length > 0;
725
this._logService.trace(`[ChatSessionsService] hasCanDelegateProvidersAvailable=${canDelegateEnabled} (${hasCanDelegate.map(c => c.type).join(', ')})`);
726
this._hasCanDelegateProvidersKey.set(canDelegateEnabled);
727
}
728
729
getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {
730
const contribution = this._contributions.get(chatSessionType)?.contribution;
731
if (!contribution) {
732
return undefined;
733
}
734
735
return this._isContributionAvailable(contribution) ? contribution : undefined;
736
}
737
738
getAllChatSessionItemProviders(): IChatSessionItemProvider[] {
739
return [...this._itemsProviders.values()].filter(provider => {
740
// Check if the provider's corresponding contribution is available
741
const contribution = this._contributions.get(provider.chatSessionType)?.contribution;
742
return !contribution || this._isContributionAvailable(contribution);
743
});
744
}
745
746
async activateChatSessionItemProvider(chatViewType: string): Promise<IChatSessionItemProvider | undefined> {
747
await this._extensionService.whenInstalledExtensionsRegistered();
748
const resolvedType = this._resolveToPrimaryType(chatViewType);
749
if (resolvedType) {
750
chatViewType = resolvedType;
751
}
752
753
const contribution = this._contributions.get(chatViewType)?.contribution;
754
if (contribution && !this._isContributionAvailable(contribution)) {
755
return undefined;
756
}
757
758
if (this._itemsProviders.has(chatViewType)) {
759
return this._itemsProviders.get(chatViewType);
760
}
761
762
await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`);
763
764
return this._itemsProviders.get(chatViewType);
765
}
766
767
async canResolveChatSession(chatSessionResource: URI) {
768
await this._extensionService.whenInstalledExtensionsRegistered();
769
const resolvedType = this._resolveToPrimaryType(chatSessionResource.scheme) || chatSessionResource.scheme;
770
const contribution = this._contributions.get(resolvedType)?.contribution;
771
if (contribution && !this._isContributionAvailable(contribution)) {
772
return false;
773
}
774
775
if (this._contentProviders.has(chatSessionResource.scheme)) {
776
return true;
777
}
778
779
await this._extensionService.activateByEvent(`onChatSession:${chatSessionResource.scheme}`);
780
return this._contentProviders.has(chatSessionResource.scheme);
781
}
782
783
async getAllChatSessionItems(token: CancellationToken): Promise<Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }>> {
784
return Promise.all(Array.from(this.getAllChatSessionContributions(), async contrib => {
785
return {
786
chatSessionType: contrib.type,
787
items: await this.getChatSessionItems(contrib.type, token)
788
};
789
}));
790
}
791
792
private async getChatSessionItems(chatSessionType: string, token: CancellationToken): Promise<IChatSessionItem[]> {
793
if (!(await this.activateChatSessionItemProvider(chatSessionType))) {
794
return [];
795
}
796
797
const resolvedType = this._resolveToPrimaryType(chatSessionType);
798
if (resolvedType) {
799
chatSessionType = resolvedType;
800
}
801
802
const provider = this._itemsProviders.get(chatSessionType);
803
if (provider?.provideChatSessionItems) {
804
const sessions = await provider.provideChatSessionItems(token);
805
return sessions;
806
}
807
808
return [];
809
}
810
811
public registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable {
812
const chatSessionType = provider.chatSessionType;
813
this._itemsProviders.set(chatSessionType, provider);
814
this._onDidChangeItemsProviders.fire(provider);
815
816
const disposables = new DisposableStore();
817
disposables.add(provider.onDidChangeChatSessionItems(() => {
818
this._onDidChangeSessionItems.fire(chatSessionType);
819
}));
820
821
this.updateInProgressStatus(chatSessionType).catch(error => {
822
this._logService.warn(`Failed to update initial progress status for '${chatSessionType}':`, error);
823
});
824
825
return {
826
dispose: () => {
827
disposables.dispose();
828
829
const provider = this._itemsProviders.get(chatSessionType);
830
if (provider) {
831
this._itemsProviders.delete(chatSessionType);
832
this._onDidChangeItemsProviders.fire(provider);
833
}
834
}
835
};
836
}
837
838
registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable {
839
if (this._contentProviders.has(chatSessionType)) {
840
throw new Error(`Content provider for ${chatSessionType} is already registered.`);
841
}
842
843
this._contentProviders.set(chatSessionType, provider);
844
this._onDidChangeContentProviderSchemes.fire({ added: [chatSessionType], removed: [] });
845
846
return {
847
dispose: () => {
848
this._contentProviders.delete(chatSessionType);
849
850
this._onDidChangeContentProviderSchemes.fire({ added: [], removed: [chatSessionType] });
851
852
// Remove all sessions that were created by this provider
853
for (const [key, session] of this._sessions) {
854
if (session.chatSessionType === chatSessionType) {
855
session.dispose();
856
this._sessions.delete(key);
857
}
858
}
859
}
860
};
861
}
862
863
public registerChatModelChangeListeners(
864
chatService: IChatService,
865
chatSessionType: string,
866
onChange: () => void
867
): IDisposable {
868
const disposableStore = new DisposableStore();
869
const chatModelsICareAbout = chatService.chatModels.map(models =>
870
Array.from(models).filter((model: IChatModel) => model.sessionResource.scheme === chatSessionType)
871
);
872
873
const listeners = new ResourceMap<IDisposable>();
874
const autoRunDisposable = autorunIterableDelta(
875
reader => chatModelsICareAbout.read(reader),
876
({ addedValues, removedValues }) => {
877
removedValues.forEach((removed) => {
878
const listener = listeners.get(removed.sessionResource);
879
if (listener) {
880
listeners.delete(removed.sessionResource);
881
listener.dispose();
882
}
883
});
884
addedValues.forEach((added) => {
885
const requestChangeListener = added.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange));
886
const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', added.onDidChange);
887
listeners.set(added.sessionResource, autorun(reader => {
888
requestChangeListener.read(reader)?.read(reader);
889
modelChangeListener.read(reader);
890
onChange();
891
}));
892
});
893
}
894
);
895
disposableStore.add(toDisposable(() => {
896
for (const listener of listeners.values()) { listener.dispose(); }
897
}));
898
disposableStore.add(autoRunDisposable);
899
return disposableStore;
900
}
901
902
903
public getInProgressSessionDescription(chatModel: IChatModel): string | undefined {
904
const requests = chatModel.getRequests();
905
if (requests.length === 0) {
906
return undefined;
907
}
908
909
// Get the last request to check its response status
910
const lastRequest = requests.at(-1);
911
const response = lastRequest?.response;
912
if (!response) {
913
return undefined;
914
}
915
916
// If the response is complete, show Finished
917
if (response.isComplete) {
918
return undefined;
919
}
920
921
// Get the response parts to find tool invocations and progress messages
922
const responseParts = response.response.value;
923
let description: string | IMarkdownString | undefined = '';
924
925
for (let i = responseParts.length - 1; i >= 0; i--) {
926
const part = responseParts[i];
927
if (description) {
928
break;
929
}
930
931
if (part.kind === 'confirmation' && typeof part.message === 'string') {
932
description = part.message;
933
} else if (part.kind === 'toolInvocation') {
934
const toolInvocation = part as IChatToolInvocation;
935
const state = toolInvocation.state.get();
936
description = toolInvocation.generatedTitle || toolInvocation.pastTenseMessage || toolInvocation.invocationMessage;
937
if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
938
const confirmationTitle = toolInvocation.confirmationMessages?.title;
939
const titleMessage = confirmationTitle && (typeof confirmationTitle === 'string'
940
? confirmationTitle
941
: confirmationTitle.value);
942
const descriptionValue = typeof description === 'string' ? description : description.value;
943
description = titleMessage ?? localize('chat.sessions.description.waitingForConfirmation', "Waiting for confirmation: {0}", descriptionValue);
944
}
945
} else if (part.kind === 'toolInvocationSerialized') {
946
description = part.invocationMessage;
947
} else if (part.kind === 'progressMessage') {
948
description = part.content;
949
} else if (part.kind === 'thinking') {
950
description = localize('chat.sessions.description.thinking', 'Thinking...');
951
}
952
}
953
954
return renderAsPlaintext(description, { useLinkFormatter: true });
955
}
956
957
public async getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise<IChatSession> {
958
const existingSessionData = this._sessions.get(sessionResource);
959
if (existingSessionData) {
960
return existingSessionData.session;
961
}
962
963
if (!(await raceCancellationError(this.canResolveChatSession(sessionResource), token))) {
964
throw Error(`Can not find provider for ${sessionResource}`);
965
}
966
967
const resolvedType = this._resolveToPrimaryType(sessionResource.scheme) || sessionResource.scheme;
968
const provider = this._contentProviders.get(resolvedType);
969
if (!provider) {
970
throw Error(`Can not find provider for ${sessionResource}`);
971
}
972
973
const session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token);
974
const sessionData = new ContributedChatSessionData(session, sessionResource.scheme, sessionResource, session.options, resource => {
975
sessionData.dispose();
976
this._sessions.delete(resource);
977
});
978
979
this._sessions.set(sessionResource, sessionData);
980
981
return session;
982
}
983
984
public hasAnySessionOptions(sessionResource: URI): boolean {
985
const session = this._sessions.get(sessionResource);
986
return !!session && !!session.options && Object.keys(session.options).length > 0;
987
}
988
989
public getSessionOption(sessionResource: URI, optionId: string): string | IChatSessionProviderOptionItem | undefined {
990
const session = this._sessions.get(sessionResource);
991
return session?.getOption(optionId);
992
}
993
994
public setSessionOption(sessionResource: URI, optionId: string, value: string | IChatSessionProviderOptionItem): boolean {
995
const session = this._sessions.get(sessionResource);
996
return !!session?.setOption(optionId, value);
997
}
998
999
public notifySessionItemsChanged(chatSessionType: string): void {
1000
this._onDidChangeSessionItems.fire(chatSessionType);
1001
}
1002
1003
/**
1004
* Store option groups for a session type
1005
*/
1006
public setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void {
1007
if (optionGroups) {
1008
this._sessionTypeOptions.set(chatSessionType, optionGroups);
1009
} else {
1010
this._sessionTypeOptions.delete(chatSessionType);
1011
}
1012
this._onDidChangeOptionGroups.fire(chatSessionType);
1013
}
1014
1015
/**
1016
* Get available option groups for a session type
1017
*/
1018
public getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined {
1019
return this._sessionTypeOptions.get(chatSessionType);
1020
}
1021
1022
private _optionsChangeCallback?: SessionOptionsChangedCallback;
1023
1024
/**
1025
* Set the callback for notifying extensions about option changes
1026
*/
1027
public setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void {
1028
this._optionsChangeCallback = callback;
1029
}
1030
1031
/**
1032
* Notify extension about option changes for a session
1033
*/
1034
public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void> {
1035
if (!updates.length) {
1036
return;
1037
}
1038
if (this._optionsChangeCallback) {
1039
await this._optionsChangeCallback(sessionResource, updates);
1040
}
1041
for (const u of updates) {
1042
this.setSessionOption(sessionResource, u.optionId, u.value);
1043
}
1044
this._onDidChangeSessionOptions.fire(sessionResource);
1045
}
1046
1047
/**
1048
* Get the icon for a specific session type
1049
*/
1050
public getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined {
1051
const sessionTypeIcon = this._sessionTypeIcons.get(chatSessionType);
1052
1053
if (ThemeIcon.isThemeIcon(sessionTypeIcon)) {
1054
return sessionTypeIcon;
1055
}
1056
1057
if (isDark(this._themeService.getColorTheme().type)) {
1058
return sessionTypeIcon?.dark;
1059
} else {
1060
return sessionTypeIcon?.light;
1061
}
1062
}
1063
1064
/**
1065
* Get the welcome title for a specific session type
1066
*/
1067
public getWelcomeTitleForSessionType(chatSessionType: string): string | undefined {
1068
return this._sessionTypeWelcomeTitles.get(chatSessionType);
1069
}
1070
1071
/**
1072
* Get the welcome message for a specific session type
1073
*/
1074
public getWelcomeMessageForSessionType(chatSessionType: string): string | undefined {
1075
return this._sessionTypeWelcomeMessages.get(chatSessionType);
1076
}
1077
1078
/**
1079
* Get the input placeholder for a specific session type
1080
*/
1081
public getInputPlaceholderForSessionType(chatSessionType: string): string | undefined {
1082
return this._sessionTypeInputPlaceholders.get(chatSessionType);
1083
}
1084
1085
/**
1086
* Get the capabilities for a specific session type
1087
*/
1088
public getCapabilitiesForSessionType(chatSessionType: string): IChatAgentAttachmentCapabilities | undefined {
1089
const contribution = this._contributions.get(chatSessionType)?.contribution;
1090
return contribution?.capabilities;
1091
}
1092
1093
public getContentProviderSchemes(): string[] {
1094
return Array.from(this._contentProviders.keys());
1095
}
1096
}
1097
1098
registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);
1099
1100