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
5319 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, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { AsyncEmitter, 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, IChatSessionItemController, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } 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, observableFromEvent, 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, backgroundAgentDisplayName, getAgentSessionProviderName } from '../agentSessions/agentSessions.js';
48
import { BugIndicatingError, isCancellationError } from '../../../../../base/common/errors.js';
49
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
50
import { LocalChatSessionUri } from '../../common/model/chatUri.js';
51
import { assertNever } from '../../../../../base/common/assert.js';
52
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
53
import { Target } from '../../common/promptSyntax/service/promptsService.js';
54
55
const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsExtensionPoint[]>({
56
extensionPoint: 'chatSessions',
57
jsonSchema: {
58
description: localize('chatSessionsExtPoint', 'Contributes chat session integrations to the chat widget.'),
59
type: 'array',
60
items: {
61
type: 'object',
62
additionalProperties: false,
63
properties: {
64
type: {
65
description: localize('chatSessionsExtPoint.chatSessionType', 'Unique identifier for the type of chat session.'),
66
type: 'string',
67
},
68
name: {
69
description: localize('chatSessionsExtPoint.name', 'Name of the dynamically registered chat participant (eg: @agent). Must not contain whitespace.'),
70
type: 'string',
71
pattern: '^[\\w-]+$'
72
},
73
displayName: {
74
description: localize('chatSessionsExtPoint.displayName', 'A longer name for this item which is used for display in menus.'),
75
type: 'string',
76
},
77
description: {
78
description: localize('chatSessionsExtPoint.description', 'Description of the chat session for use in menus and tooltips.'),
79
type: 'string'
80
},
81
when: {
82
description: localize('chatSessionsExtPoint.when', 'Condition which must be true to show this item.'),
83
type: 'string'
84
},
85
icon: {
86
description: localize('chatSessionsExtPoint.icon', 'Icon identifier (codicon ID) for the chat session editor tab. For example, "$(github)" or "$(cloud)".'),
87
anyOf: [{
88
type: 'string'
89
},
90
{
91
type: 'object',
92
properties: {
93
light: {
94
description: localize('icon.light', 'Icon path when a light theme is used'),
95
type: 'string'
96
},
97
dark: {
98
description: localize('icon.dark', 'Icon path when a dark theme is used'),
99
type: 'string'
100
}
101
}
102
}]
103
},
104
order: {
105
description: localize('chatSessionsExtPoint.order', 'Order in which this item should be displayed.'),
106
type: 'integer'
107
},
108
alternativeIds: {
109
description: localize('chatSessionsExtPoint.alternativeIds', 'Alternative identifiers for backward compatibility.'),
110
type: 'array',
111
items: {
112
type: 'string'
113
}
114
},
115
welcomeTitle: {
116
description: localize('chatSessionsExtPoint.welcomeTitle', 'Title text to display in the chat welcome view for this session type.'),
117
type: 'string'
118
},
119
welcomeMessage: {
120
description: localize('chatSessionsExtPoint.welcomeMessage', 'Message text (supports markdown) to display in the chat welcome view for this session type.'),
121
type: 'string'
122
},
123
welcomeTips: {
124
description: localize('chatSessionsExtPoint.welcomeTips', 'Tips text (supports markdown and theme icons) to display in the chat welcome view for this session type.'),
125
type: 'string'
126
},
127
inputPlaceholder: {
128
description: localize('chatSessionsExtPoint.inputPlaceholder', 'Placeholder text to display in the chat input box for this session type.'),
129
type: 'string'
130
},
131
capabilities: {
132
description: localize('chatSessionsExtPoint.capabilities', 'Optional capabilities for this chat session.'),
133
type: 'object',
134
additionalProperties: false,
135
properties: {
136
supportsFileAttachments: {
137
description: localize('chatSessionsExtPoint.supportsFileAttachments', 'Whether this chat session supports attaching files or file references.'),
138
type: 'boolean'
139
},
140
supportsToolAttachments: {
141
description: localize('chatSessionsExtPoint.supportsToolAttachments', 'Whether this chat session supports attaching tools or tool references.'),
142
type: 'boolean'
143
},
144
supportsMCPAttachments: {
145
description: localize('chatSessionsExtPoint.supportsMCPAttachments', 'Whether this chat session supports attaching MCP resources.'),
146
type: 'boolean'
147
},
148
supportsImageAttachments: {
149
description: localize('chatSessionsExtPoint.supportsImageAttachments', 'Whether this chat session supports attaching images.'),
150
type: 'boolean'
151
},
152
supportsSearchResultAttachments: {
153
description: localize('chatSessionsExtPoint.supportsSearchResultAttachments', 'Whether this chat session supports attaching search results.'),
154
type: 'boolean'
155
},
156
supportsInstructionAttachments: {
157
description: localize('chatSessionsExtPoint.supportsInstructionAttachments', 'Whether this chat session supports attaching instructions.'),
158
type: 'boolean'
159
},
160
supportsSourceControlAttachments: {
161
description: localize('chatSessionsExtPoint.supportsSourceControlAttachments', 'Whether this chat session supports attaching source control changes.'),
162
type: 'boolean'
163
},
164
supportsProblemAttachments: {
165
description: localize('chatSessionsExtPoint.supportsProblemAttachments', 'Whether this chat session supports attaching problems.'),
166
type: 'boolean'
167
},
168
supportsSymbolAttachments: {
169
description: localize('chatSessionsExtPoint.supportsSymbolAttachments', 'Whether this chat session supports attaching symbols.'),
170
type: 'boolean'
171
},
172
supportsPromptAttachments: {
173
description: localize('chatSessionsExtPoint.supportsPromptAttachments', 'Whether this chat session supports attaching prompts.'),
174
type: 'boolean'
175
}
176
}
177
},
178
commands: {
179
markdownDescription: localize('chatCommandsDescription', "Commands available for this chat session, which the user can invoke with a `/`."),
180
type: 'array',
181
items: {
182
additionalProperties: false,
183
type: 'object',
184
defaultSnippets: [{ body: { name: '', description: '' } }],
185
required: ['name'],
186
properties: {
187
name: {
188
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."),
189
type: 'string'
190
},
191
description: {
192
description: localize('chatCommandDescription', "A description of this command."),
193
type: 'string'
194
},
195
when: {
196
description: localize('chatCommandWhen', "A condition which must be true to enable this command."),
197
type: 'string'
198
},
199
}
200
}
201
},
202
canDelegate: {
203
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.'),
204
type: 'boolean',
205
default: false
206
},
207
isReadOnly: {
208
description: localize('chatSessionsExtPoint.isReadOnly', 'Whether this session type is for read-only agents that do not support interactive chat. This flag is incompatible with \'canDelegate\'.'),
209
type: 'boolean',
210
default: false
211
},
212
customAgentTarget: {
213
description: localize('chatSessionsExtPoint.customAgentTarget', 'When set, the chat session will show a filtered mode picker that prefers custom agents whose target property matches this value. Custom agents without a target property are still shown in all session types. This enables the use of standard agent/mode with contributed sessions.'),
214
type: 'string'
215
}
216
},
217
required: ['type', 'name', 'displayName', 'description'],
218
}
219
},
220
activationEventsGenerator: function* (contribs) {
221
for (const contrib of contribs) {
222
yield `onChatSession:${contrib.type}`;
223
}
224
}
225
});
226
227
class ContributedChatSessionData extends Disposable {
228
229
private readonly _optionsCache: Map<string /* 'models' */, string | IChatSessionProviderOptionItem>;
230
public getOption(optionId: string): string | IChatSessionProviderOptionItem | undefined {
231
return this._optionsCache.get(optionId);
232
}
233
public setOption(optionId: string, value: string | IChatSessionProviderOptionItem): void {
234
this._optionsCache.set(optionId, value);
235
}
236
237
constructor(
238
readonly session: IChatSession,
239
readonly chatSessionType: string,
240
readonly resource: URI,
241
readonly options: Record<string, string | IChatSessionProviderOptionItem> | undefined,
242
private readonly onWillDispose: (resource: URI) => void
243
) {
244
super();
245
246
this._optionsCache = new Map<string, string | IChatSessionProviderOptionItem>();
247
if (options) {
248
for (const [key, value] of Object.entries(options)) {
249
this._optionsCache.set(key, value);
250
}
251
}
252
253
this._register(this.session.onWillDispose(() => {
254
this.onWillDispose(this.resource);
255
}));
256
}
257
}
258
259
260
export class ChatSessionsService extends Disposable implements IChatSessionsService {
261
readonly _serviceBrand: undefined;
262
263
private readonly _itemControllers = new Map</* type */ string, { readonly controller: IChatSessionItemController; readonly initialRefresh: Promise<void> }>();
264
265
private readonly _contributions: Map</* type */ string, { readonly contribution: IChatSessionsExtensionPoint; readonly extension: IRelaxedExtensionDescription }> = new Map();
266
private readonly _contributionDisposables = this._register(new DisposableMap</* type */ string>());
267
268
private readonly _contentProviders: Map</* scheme */ string, IChatSessionContentProvider> = new Map();
269
private readonly _alternativeIdMap: Map</* alternativeId */ string, /* primaryType */ string> = new Map();
270
private readonly _contextKeys = new Set<string>();
271
272
private readonly _onDidChangeItemsProviders = this._register(new Emitter<{ readonly chatSessionType: string }>());
273
readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event;
274
275
private readonly _onDidChangeSessionItems = this._register(new Emitter<{ readonly chatSessionType: string }>());
276
readonly onDidChangeSessionItems = this._onDidChangeSessionItems.event;
277
278
private readonly _onDidChangeAvailability = this._register(new Emitter<void>());
279
readonly onDidChangeAvailability: Event<void> = this._onDidChangeAvailability.event;
280
281
private readonly _onDidChangeInProgress = this._register(new Emitter<void>());
282
public get onDidChangeInProgress() { return this._onDidChangeInProgress.event; }
283
284
private readonly _onDidChangeContentProviderSchemes = this._register(new Emitter<{ readonly added: string[]; readonly removed: string[] }>());
285
public get onDidChangeContentProviderSchemes() { return this._onDidChangeContentProviderSchemes.event; }
286
private readonly _onDidChangeSessionOptions = this._register(new Emitter<URI>());
287
public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; }
288
private readonly _onDidChangeOptionGroups = this._register(new Emitter<string>());
289
public get onDidChangeOptionGroups() { return this._onDidChangeOptionGroups.event; }
290
private readonly _onRequestNotifyExtension = this._register(new AsyncEmitter<IChatSessionOptionsWillNotifyExtensionEvent>());
291
public get onRequestNotifyExtension() { return this._onRequestNotifyExtension.event; }
292
293
private readonly inProgressMap: Map<string, number> = new Map();
294
private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();
295
private readonly _sessionTypeIcons: Map<string, ThemeIcon | { light: URI; dark: URI }> = new Map();
296
private readonly _sessionTypeWelcomeTitles: Map<string, string> = new Map();
297
private readonly _sessionTypeWelcomeMessages: Map<string, string> = new Map();
298
private readonly _sessionTypeWelcomeTips: Map<string, string> = new Map();
299
private readonly _sessionTypeInputPlaceholders: Map<string, string> = new Map();
300
301
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
302
303
private readonly _hasCanDelegateProvidersKey: IContextKey<boolean>;
304
305
constructor(
306
@ILogService private readonly _logService: ILogService,
307
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
308
@IExtensionService private readonly _extensionService: IExtensionService,
309
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
310
@IMenuService private readonly _menuService: IMenuService,
311
@IThemeService private readonly _themeService: IThemeService,
312
@ILabelService private readonly _labelService: ILabelService
313
) {
314
super();
315
316
this._hasCanDelegateProvidersKey = ChatContextKeys.hasCanDelegateProviders.bindTo(this._contextKeyService);
317
318
this._register(extensionPoint.setHandler(extensions => {
319
for (const ext of extensions) {
320
if (!isProposedApiEnabled(ext.description, 'chatSessionsProvider')) {
321
continue;
322
}
323
if (!Array.isArray(ext.value)) {
324
continue;
325
}
326
for (const contribution of ext.value) {
327
this._register(this.registerContribution(contribution, ext.description));
328
}
329
}
330
}));
331
332
// Listen for context changes and re-evaluate contributions
333
this._register(Event.filter(this._contextKeyService.onDidChangeContext, e => e.affectsSome(this._contextKeys))(() => {
334
this._evaluateAvailability();
335
}));
336
337
const builtinSessionProviders = [AgentSessionProviders.Local];
338
const contributedSessionProviders = observableFromEvent(
339
this.onDidChangeAvailability,
340
() => Array.from(this._contributions.keys()).filter(key => this._contributionDisposables.has(key) && isAgentSessionProviderType(key)) as AgentSessionProviders[],
341
).recomputeInitiallyAndOnChange(this._store);
342
343
this._register(autorun(reader => {
344
backgroundAgentDisplayName.read(reader);
345
const activatedProviders = [...builtinSessionProviders, ...contributedSessionProviders.read(reader)];
346
for (const provider of Object.values(AgentSessionProviders)) {
347
if (activatedProviders.includes(provider)) {
348
reader.store.add(registerNewSessionInPlaceAction(provider, getAgentSessionProviderName(provider)));
349
}
350
}
351
}));
352
353
this._register(this.onDidChangeSessionItems(({ chatSessionType }) => {
354
this.updateInProgressStatus(chatSessionType).catch(error => {
355
this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error);
356
});
357
}));
358
359
this._register(this._labelService.registerFormatter({
360
scheme: Schemas.copilotPr,
361
formatting: {
362
label: '${authority}${path}',
363
separator: sep,
364
stripPathStartingSeparator: true,
365
}
366
}));
367
}
368
369
public reportInProgress(chatSessionType: string, count: number): void {
370
let displayName: string | undefined;
371
372
if (chatSessionType === AgentSessionProviders.Local) {
373
displayName = localize('chat.session.inProgress.local', "Local Agent");
374
} else if (chatSessionType === AgentSessionProviders.Background) {
375
displayName = localize('chat.session.inProgress.background', "Background Agent");
376
} else if (chatSessionType === AgentSessionProviders.Cloud) {
377
displayName = localize('chat.session.inProgress.cloud', "Cloud Agent");
378
} else {
379
displayName = this._contributions.get(chatSessionType)?.contribution.displayName;
380
}
381
382
if (displayName) {
383
this.inProgressMap.set(displayName, count);
384
}
385
this._onDidChangeInProgress.fire();
386
}
387
388
public getInProgress(): { displayName: string; count: number }[] {
389
return Array.from(this.inProgressMap.entries()).map(([displayName, count]) => ({ displayName, count }));
390
}
391
392
private async updateInProgressStatus(chatSessionType: string): Promise<void> {
393
try {
394
const results = await this.getChatSessionItems([chatSessionType], CancellationToken.None);
395
const items = results.flatMap(r => r.items);
396
const inProgress = items.filter(item => item.status && isSessionInProgressStatus(item.status));
397
this.reportInProgress(chatSessionType, inProgress.length);
398
} catch (error) {
399
this._logService.warn(`Failed to update in-progress status for chat session type '${chatSessionType}':`, error);
400
}
401
}
402
403
private registerContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {
404
if (this._contributions.has(contribution.type)) {
405
return { dispose: () => { } };
406
}
407
408
// Track context keys from the when condition
409
if (contribution.when) {
410
const whenExpr = ContextKeyExpr.deserialize(contribution.when);
411
if (whenExpr) {
412
for (const key of whenExpr.keys()) {
413
this._contextKeys.add(key);
414
}
415
}
416
}
417
418
this._contributions.set(contribution.type, { contribution, extension: ext });
419
420
// Register alternative IDs if provided
421
if (contribution.alternativeIds) {
422
for (const altId of contribution.alternativeIds) {
423
if (this._alternativeIdMap.has(altId)) {
424
this._logService.warn(`Alternative ID '${altId}' is already mapped to '${this._alternativeIdMap.get(altId)}'. Remapping to '${contribution.type}'.`);
425
}
426
this._alternativeIdMap.set(altId, contribution.type);
427
}
428
}
429
430
// Store icon mapping if provided
431
let icon: ThemeIcon | { dark: URI; light: URI } | undefined;
432
433
if (contribution.icon) {
434
// Parse icon string - support ThemeIcon format or file path from extension
435
if (typeof contribution.icon === 'string') {
436
icon = contribution.icon.startsWith('$(') && contribution.icon.endsWith(')')
437
? ThemeIcon.fromString(contribution.icon)
438
: ThemeIcon.fromId(contribution.icon);
439
} else {
440
icon = {
441
dark: resources.joinPath(ext.extensionLocation, contribution.icon.dark),
442
light: resources.joinPath(ext.extensionLocation, contribution.icon.light)
443
};
444
}
445
}
446
447
if (icon) {
448
this._sessionTypeIcons.set(contribution.type, icon);
449
}
450
451
// Store welcome title, message, tips, and input placeholder if provided
452
if (contribution.welcomeTitle) {
453
this._sessionTypeWelcomeTitles.set(contribution.type, contribution.welcomeTitle);
454
}
455
if (contribution.welcomeMessage) {
456
this._sessionTypeWelcomeMessages.set(contribution.type, contribution.welcomeMessage);
457
}
458
if (contribution.welcomeTips) {
459
this._sessionTypeWelcomeTips.set(contribution.type, contribution.welcomeTips);
460
}
461
if (contribution.inputPlaceholder) {
462
this._sessionTypeInputPlaceholders.set(contribution.type, contribution.inputPlaceholder);
463
}
464
465
this._evaluateAvailability();
466
467
return {
468
dispose: () => {
469
this._contributions.delete(contribution.type);
470
// Remove alternative ID mappings
471
if (contribution.alternativeIds) {
472
for (const altId of contribution.alternativeIds) {
473
if (this._alternativeIdMap.get(altId) === contribution.type) {
474
this._alternativeIdMap.delete(altId);
475
}
476
}
477
}
478
this._sessionTypeIcons.delete(contribution.type);
479
this._sessionTypeWelcomeTitles.delete(contribution.type);
480
this._sessionTypeWelcomeMessages.delete(contribution.type);
481
this._sessionTypeWelcomeTips.delete(contribution.type);
482
this._sessionTypeInputPlaceholders.delete(contribution.type);
483
this._contributionDisposables.deleteAndDispose(contribution.type);
484
this._updateHasCanDelegateProvidersContextKey();
485
}
486
};
487
}
488
489
private _isContributionAvailable(contribution: IChatSessionsExtensionPoint): boolean {
490
if (!contribution.when) {
491
return true;
492
}
493
const whenExpr = ContextKeyExpr.deserialize(contribution.when);
494
return !whenExpr || this._contextKeyService.contextMatchesRules(whenExpr);
495
}
496
497
/**
498
* Resolves a session type to its primary type, checking for alternative IDs.
499
* @param sessionType The session type or alternative ID to resolve
500
* @returns The primary session type, or undefined if not found or not available
501
*/
502
private _resolveToPrimaryType(sessionType: string): string | undefined {
503
// Try to find the primary type first
504
const contribution = this._contributions.get(sessionType)?.contribution;
505
if (contribution) {
506
// If the contribution is available, use it
507
if (this._isContributionAvailable(contribution)) {
508
return sessionType;
509
}
510
// If not available, fall through to check for alternatives
511
}
512
513
// Check if this is an alternative ID, or if the primary type is not available
514
const primaryType = this._alternativeIdMap.get(sessionType);
515
if (primaryType) {
516
const altContribution = this._contributions.get(primaryType)?.contribution;
517
if (altContribution && this._isContributionAvailable(altContribution)) {
518
return primaryType;
519
}
520
}
521
522
return undefined;
523
}
524
525
private _registerMenuItems(contribution: IChatSessionsExtensionPoint, extensionDescription: IRelaxedExtensionDescription): IDisposable {
526
// If provider registers anything for the create submenu, let it fully control the creation
527
const contextKeyService = this._contextKeyService.createOverlay([
528
['chatSessionType', contribution.type]
529
]);
530
531
const rawMenuActions = this._menuService.getMenuActions(MenuId.AgentSessionsCreateSubMenu, contextKeyService);
532
const menuActions = rawMenuActions.map(value => value[1]).flat();
533
534
const disposables = new DisposableStore();
535
536
// Mirror all create submenu actions into the global Chat New menu
537
for (let i = 0; i < menuActions.length; i++) {
538
const action = menuActions[i];
539
if (action instanceof MenuItemAction) {
540
// TODO: This is an odd way to do this, but the best we can do currently
541
if (i === 0 && !contribution.canDelegate) {
542
disposables.add(registerNewSessionExternalAction(contribution.type, contribution.displayName, action.item.id));
543
} else {
544
disposables.add(MenuRegistry.appendMenuItem(MenuId.ChatNewMenu, {
545
command: action.item,
546
group: '4_externally_contributed',
547
}));
548
}
549
}
550
}
551
return {
552
dispose: () => disposables.dispose()
553
};
554
}
555
556
private _registerCommands(contribution: IChatSessionsExtensionPoint): IDisposable {
557
const isAvailableInSessionTypePicker = isAgentSessionProviderType(contribution.type);
558
559
return combinedDisposable(
560
registerAction2(class OpenChatSessionAction extends Action2 {
561
constructor() {
562
super({
563
id: `workbench.action.chat.openSessionWithPrompt.${contribution.type}`,
564
title: localize2('interactiveSession.openSessionWithPrompt', "New {0} with Prompt", contribution.displayName),
565
category: CHAT_CATEGORY,
566
icon: Codicon.plus,
567
f1: false,
568
precondition: ChatContextKeys.enabled
569
});
570
}
571
572
async run(accessor: ServicesAccessor, chatOptions?: { resource: UriComponents; prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {
573
const chatService = accessor.get(IChatService);
574
const { type } = contribution;
575
576
if (chatOptions) {
577
const resource = URI.revive(chatOptions.resource);
578
const ref = await chatService.loadSessionForResource(resource, ChatAgentLocation.Chat, CancellationToken.None);
579
await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });
580
ref?.dispose();
581
}
582
}
583
}),
584
// Creates a chat editor
585
registerAction2(class OpenNewChatSessionEditorAction extends Action2 {
586
constructor() {
587
super({
588
id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`,
589
title: localize2('interactiveSession.openNewSessionEditor', "New {0}", contribution.displayName),
590
category: CHAT_CATEGORY,
591
icon: Codicon.plus,
592
f1: true,
593
precondition: ChatContextKeys.enabled,
594
});
595
}
596
597
async run(accessor: ServicesAccessor, chatOptions?: { prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {
598
const { type, displayName } = contribution;
599
await openChatSession(accessor, { type, displayName, position: ChatSessionPosition.Editor }, chatOptions);
600
}
601
}),
602
// New chat in sidebar chat (+ button)
603
registerAction2(class OpenNewChatSessionSidebarAction extends Action2 {
604
constructor() {
605
super({
606
id: `workbench.action.chat.openNewSessionSidebar.${contribution.type}`,
607
title: localize2('interactiveSession.openNewSessionSidebar', "New {0}", contribution.displayName),
608
category: CHAT_CATEGORY,
609
icon: Codicon.plus,
610
f1: false, // Hide from Command Palette
611
precondition: ChatContextKeys.enabled,
612
menu: !isAvailableInSessionTypePicker ? {
613
id: MenuId.ChatNewMenu,
614
group: '3_new_special',
615
} : undefined,
616
});
617
}
618
619
async run(accessor: ServicesAccessor, chatOptions?: { prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {
620
const { type, displayName } = contribution;
621
await openChatSession(accessor, { type, displayName, position: ChatSessionPosition.Sidebar }, chatOptions);
622
}
623
})
624
);
625
}
626
627
private _evaluateAvailability(): void {
628
let hasChanges = false;
629
for (const { contribution, extension } of this._contributions.values()) {
630
const isCurrentlyRegistered = this._contributionDisposables.has(contribution.type);
631
const shouldBeRegistered = this._isContributionAvailable(contribution);
632
if (isCurrentlyRegistered && !shouldBeRegistered) {
633
// Disable the contribution by disposing its disposable store
634
this._contributionDisposables.deleteAndDispose(contribution.type);
635
636
// Also dispose any cached sessions for this contribution
637
this._disposeSessionsForContribution(contribution.type);
638
hasChanges = true;
639
} else if (!isCurrentlyRegistered && shouldBeRegistered) {
640
// Enable the contribution by registering it
641
this._enableContribution(contribution, extension);
642
hasChanges = true;
643
}
644
}
645
if (hasChanges) {
646
this._onDidChangeAvailability.fire();
647
for (const chatSessionType of this._itemControllers.keys()) {
648
this._onDidChangeItemsProviders.fire({ chatSessionType });
649
}
650
for (const { contribution } of this._contributions.values()) {
651
this._onDidChangeSessionItems.fire({ chatSessionType: contribution.type });
652
}
653
}
654
this._updateHasCanDelegateProvidersContextKey();
655
}
656
657
private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void {
658
const disposableStore = new DisposableStore();
659
this._contributionDisposables.set(contribution.type, disposableStore);
660
if (contribution.isReadOnly || contribution.canDelegate) {
661
disposableStore.add(this._registerAgent(contribution, ext));
662
disposableStore.add(this._registerCommands(contribution));
663
}
664
disposableStore.add(this._registerMenuItems(contribution, ext));
665
}
666
667
private _disposeSessionsForContribution(contributionId: string): void {
668
// Find and dispose all sessions that belong to this contribution
669
const sessionsToDispose: URI[] = [];
670
for (const [sessionResource, sessionData] of this._sessions) {
671
if (sessionData.chatSessionType === contributionId) {
672
sessionsToDispose.push(sessionResource);
673
}
674
}
675
676
if (sessionsToDispose.length > 0) {
677
this._logService.info(`Disposing ${sessionsToDispose.length} cached sessions for contribution '${contributionId}' due to when clause change`);
678
}
679
680
for (const sessionKey of sessionsToDispose) {
681
const sessionData = this._sessions.get(sessionKey);
682
if (sessionData) {
683
sessionData.dispose(); // This will call _onWillDisposeSession and clean up
684
}
685
}
686
}
687
688
private _registerAgent(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {
689
const { type: id, name, displayName, description } = contribution;
690
const storedIcon = this._sessionTypeIcons.get(id);
691
const icons = ThemeIcon.isThemeIcon(storedIcon)
692
? { themeIcon: storedIcon, icon: undefined, iconDark: undefined }
693
: storedIcon
694
? { icon: storedIcon.light, iconDark: storedIcon.dark }
695
: { themeIcon: Codicon.sendToRemoteAgent };
696
697
const agentData: IChatAgentData = {
698
id,
699
name,
700
fullName: displayName,
701
description: description,
702
isDefault: false,
703
isCore: false,
704
isDynamic: true,
705
slashCommands: contribution.commands ?? [],
706
locations: [ChatAgentLocation.Chat],
707
modes: [ChatModeKind.Agent, ChatModeKind.Ask],
708
disambiguation: [],
709
metadata: {
710
...icons,
711
},
712
capabilities: contribution.capabilities,
713
canAccessPreviousChatHistory: true,
714
extensionId: ext.identifier,
715
extensionVersion: ext.version,
716
extensionDisplayName: ext.displayName || ext.name,
717
extensionPublisherId: ext.publisher,
718
};
719
720
return this._chatAgentService.registerAgent(id, agentData);
721
}
722
723
getAllChatSessionContributions(): IChatSessionsExtensionPoint[] {
724
return Array.from(this._contributions.values(), x => x.contribution)
725
.filter(contribution => this._isContributionAvailable(contribution));
726
}
727
728
private _updateHasCanDelegateProvidersContextKey(): void {
729
const hasCanDelegate = this.getAllChatSessionContributions().filter(c => c.canDelegate);
730
const canDelegateEnabled = hasCanDelegate.length > 0;
731
this._logService.trace(`[ChatSessionsService] hasCanDelegateProvidersAvailable=${canDelegateEnabled} (${hasCanDelegate.map(c => c.type).join(', ')})`);
732
this._hasCanDelegateProvidersKey.set(canDelegateEnabled);
733
}
734
735
getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {
736
const contribution = this._contributions.get(chatSessionType)?.contribution;
737
if (!contribution) {
738
return undefined;
739
}
740
741
return this._isContributionAvailable(contribution) ? contribution : undefined;
742
}
743
744
async activateChatSessionItemProvider(chatViewType: string): Promise<void> {
745
await this.doActivateChatSessionItemController(chatViewType);
746
}
747
748
private async doActivateChatSessionItemController(chatViewType: string): Promise<boolean> {
749
await this._extensionService.whenInstalledExtensionsRegistered();
750
const resolvedType = this._resolveToPrimaryType(chatViewType);
751
if (resolvedType) {
752
chatViewType = resolvedType;
753
}
754
755
const contribution = this._contributions.get(chatViewType)?.contribution;
756
if (contribution && !this._isContributionAvailable(contribution)) {
757
return false;
758
}
759
760
if (this._itemControllers.has(chatViewType)) {
761
return true;
762
}
763
764
await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`);
765
766
const controller = this._itemControllers.get(chatViewType)!;
767
return !!controller;
768
}
769
770
async canResolveChatSession(chatSessionResource: URI) {
771
await this._extensionService.whenInstalledExtensionsRegistered();
772
const resolvedType = this._resolveToPrimaryType(chatSessionResource.scheme) || chatSessionResource.scheme;
773
const contribution = this._contributions.get(resolvedType)?.contribution;
774
if (contribution && !this._isContributionAvailable(contribution)) {
775
return false;
776
}
777
778
if (this._contentProviders.has(chatSessionResource.scheme)) {
779
return true;
780
}
781
782
await this._extensionService.activateByEvent(`onChatSession:${chatSessionResource.scheme}`);
783
return this._contentProviders.has(chatSessionResource.scheme);
784
}
785
786
private async tryActivateControllers(providersToResolve: readonly string[] | undefined): Promise<void> {
787
await Promise.all(this.getAllChatSessionContributions().map(async (contrib) => {
788
if (providersToResolve && !providersToResolve.includes(contrib.type)) {
789
return; // skip: not considered for resolving
790
}
791
792
if (!await this.doActivateChatSessionItemController(contrib.type)) {
793
// We requested this provider but it is not available
794
if (providersToResolve?.includes(contrib.type)) {
795
this._logService.trace(`[ChatSessionsService] No enabled provider found for chat session type ${contrib.type}`);
796
}
797
}
798
}));
799
}
800
801
public async getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise<Array<{ readonly chatSessionType: string; readonly items: readonly IChatSessionItem[] }>> {
802
// First, make sure contributed controller are active
803
await this.tryActivateControllers(providersToResolve);
804
805
// Then actually resolve items for all active controllers
806
const results: Array<{ readonly chatSessionType: string; readonly items: readonly IChatSessionItem[] }> = [];
807
await Promise.all(Array.from(this._itemControllers).map(async ([chatSessionType, controllerEntry]) => {
808
const resolvedType = this._resolveToPrimaryType(chatSessionType) ?? chatSessionType;
809
if (providersToResolve && !providersToResolve.includes(resolvedType)) {
810
return; // skip: not considered for resolving
811
}
812
813
try {
814
await controllerEntry.initialRefresh; // Ensure initial refresh is complete before accessing items
815
816
const providerSessions = controllerEntry.controller.items;
817
this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for provider ${resolvedType}`);
818
results.push({ chatSessionType: resolvedType, items: providerSessions });
819
} catch (err) {
820
if (!isCancellationError(err)) {
821
// Log error but continue with other providers
822
this._logService.error(`[ChatSessionsService] Failed to resolve sessions for provider ${resolvedType}`, err);
823
}
824
}
825
}));
826
827
return results;
828
}
829
830
public async refreshChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise<void> {
831
await this.tryActivateControllers(providersToResolve);
832
833
await Promise.all(Array.from(this._itemControllers).map(async ([chatSessionType, controllerEntry]) => {
834
try {
835
await controllerEntry.controller.refresh(token);
836
} catch (err) {
837
if (!isCancellationError(err)) {
838
// Log error but continue with other providers
839
this._logService.error(`[ChatSessionsService] Failed to resolve sessions for provider ${chatSessionType}`, err);
840
}
841
}
842
}));
843
}
844
845
registerChatSessionItemController(chatSessionType: string, controller: IChatSessionItemController): IDisposable {
846
const disposables = new DisposableStore();
847
848
849
// Register and trigger an initial refresh to populate the provider's items
850
const initialRefreshCts = disposables.add(new CancellationTokenSource());
851
this._itemControllers.set(chatSessionType, { controller, initialRefresh: controller.refresh(initialRefreshCts.token) });
852
this._onDidChangeItemsProviders.fire({ chatSessionType });
853
854
disposables.add(controller.onDidChangeChatSessionItems(() => {
855
this._onDidChangeSessionItems.fire({ chatSessionType });
856
}));
857
858
this.updateInProgressStatus(chatSessionType).catch(error => {
859
this._logService.warn(`Failed to update initial progress status for '${chatSessionType}':`, error);
860
});
861
862
return {
863
dispose: () => {
864
initialRefreshCts.cancel();
865
disposables.dispose();
866
867
const controller = this._itemControllers.get(chatSessionType);
868
if (controller) {
869
this._itemControllers.delete(chatSessionType);
870
this._onDidChangeItemsProviders.fire({ chatSessionType });
871
}
872
}
873
};
874
}
875
876
registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable {
877
if (this._contentProviders.has(chatSessionType)) {
878
throw new Error(`Content provider for ${chatSessionType} is already registered.`);
879
}
880
881
this._contentProviders.set(chatSessionType, provider);
882
this._onDidChangeContentProviderSchemes.fire({ added: [chatSessionType], removed: [] });
883
884
return {
885
dispose: () => {
886
this._contentProviders.delete(chatSessionType);
887
888
this._onDidChangeContentProviderSchemes.fire({ added: [], removed: [chatSessionType] });
889
890
// Remove all sessions that were created by this provider
891
for (const [key, session] of this._sessions) {
892
if (session.chatSessionType === chatSessionType) {
893
session.dispose();
894
this._sessions.delete(key);
895
}
896
}
897
}
898
};
899
}
900
901
public registerChatModelChangeListeners(
902
chatService: IChatService,
903
chatSessionType: string,
904
onChange: () => void
905
): IDisposable {
906
const disposableStore = new DisposableStore();
907
const chatModelsICareAbout = chatService.chatModels.map(models =>
908
Array.from(models).filter((model: IChatModel) => model.sessionResource.scheme === chatSessionType)
909
);
910
911
const listeners = new ResourceMap<IDisposable>();
912
const autoRunDisposable = autorunIterableDelta(
913
reader => chatModelsICareAbout.read(reader),
914
({ addedValues, removedValues }) => {
915
removedValues.forEach((removed) => {
916
const listener = listeners.get(removed.sessionResource);
917
if (listener) {
918
listeners.delete(removed.sessionResource);
919
listener.dispose();
920
}
921
});
922
addedValues.forEach((added) => {
923
const requestChangeListener = added.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange));
924
const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', added.onDidChange);
925
listeners.set(added.sessionResource, autorun(reader => {
926
requestChangeListener.read(reader)?.read(reader);
927
modelChangeListener.read(reader);
928
onChange();
929
}));
930
});
931
}
932
);
933
disposableStore.add(toDisposable(() => {
934
for (const listener of listeners.values()) { listener.dispose(); }
935
}));
936
disposableStore.add(autoRunDisposable);
937
return disposableStore;
938
}
939
940
941
public getInProgressSessionDescription(chatModel: IChatModel): string | undefined {
942
const requests = chatModel.getRequests();
943
if (requests.length === 0) {
944
return undefined;
945
}
946
947
// Get the last request to check its response status
948
const lastRequest = requests.at(-1);
949
const response = lastRequest?.response;
950
if (!response) {
951
return undefined;
952
}
953
954
// If the response is complete, show Finished
955
if (response.isComplete) {
956
return undefined;
957
}
958
959
// Get the response parts to find tool invocations and progress messages
960
const responseParts = response.response.value;
961
let description: string | IMarkdownString | undefined = '';
962
963
for (let i = responseParts.length - 1; i >= 0; i--) {
964
const part = responseParts[i];
965
if (description) {
966
break;
967
}
968
969
if (part.kind === 'confirmation' && typeof part.message === 'string') {
970
description = part.message;
971
} else if (part.kind === 'toolInvocation') {
972
const toolInvocation = part as IChatToolInvocation;
973
const state = toolInvocation.state.get();
974
description = toolInvocation.generatedTitle || toolInvocation.pastTenseMessage || toolInvocation.invocationMessage;
975
if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
976
const confirmationTitle = state.confirmationMessages?.title;
977
const titleMessage = confirmationTitle && (typeof confirmationTitle === 'string'
978
? confirmationTitle
979
: confirmationTitle.value);
980
const descriptionValue = typeof description === 'string' ? description : description.value;
981
description = titleMessage ?? localize('chat.sessions.description.waitingForConfirmation', "Waiting for confirmation: {0}", descriptionValue);
982
}
983
} else if (part.kind === 'toolInvocationSerialized') {
984
description = part.invocationMessage;
985
} else if (part.kind === 'progressMessage') {
986
description = part.content;
987
} else if (part.kind === 'thinking') {
988
description = localize('chat.sessions.description.thinking', 'Thinking...');
989
}
990
}
991
992
return description ? renderAsPlaintext(description, { useLinkFormatter: true }) : '';
993
}
994
995
public async getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise<IChatSession> {
996
const existingSessionData = this._sessions.get(sessionResource);
997
if (existingSessionData) {
998
return existingSessionData.session;
999
}
1000
1001
if (!(await raceCancellationError(this.canResolveChatSession(sessionResource), token))) {
1002
throw Error(`Can not find provider for ${sessionResource}`);
1003
}
1004
1005
const resolvedType = this._resolveToPrimaryType(sessionResource.scheme) || sessionResource.scheme;
1006
const provider = this._contentProviders.get(resolvedType);
1007
if (!provider) {
1008
throw Error(`Can not find provider for ${sessionResource}`);
1009
}
1010
1011
const session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token);
1012
const sessionData = new ContributedChatSessionData(session, sessionResource.scheme, sessionResource, session.options, resource => {
1013
sessionData.dispose();
1014
this._sessions.delete(resource);
1015
});
1016
1017
this._sessions.set(sessionResource, sessionData);
1018
1019
return session;
1020
}
1021
1022
public hasAnySessionOptions(sessionResource: URI): boolean {
1023
const session = this._sessions.get(sessionResource);
1024
return !!session && !!session.options && Object.keys(session.options).length > 0;
1025
}
1026
1027
public getSessionOption(sessionResource: URI, optionId: string): string | IChatSessionProviderOptionItem | undefined {
1028
const session = this._sessions.get(sessionResource);
1029
return session?.getOption(optionId);
1030
}
1031
1032
public setSessionOption(sessionResource: URI, optionId: string, value: string | IChatSessionProviderOptionItem): boolean {
1033
const session = this._sessions.get(sessionResource);
1034
return !!session?.setOption(optionId, value);
1035
}
1036
1037
/**
1038
* Store option groups for a session type
1039
*/
1040
public setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void {
1041
if (optionGroups) {
1042
this._sessionTypeOptions.set(chatSessionType, optionGroups);
1043
} else {
1044
this._sessionTypeOptions.delete(chatSessionType);
1045
}
1046
this._onDidChangeOptionGroups.fire(chatSessionType);
1047
}
1048
1049
/**
1050
* Get available option groups for a session type
1051
*/
1052
public getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined {
1053
return this._sessionTypeOptions.get(chatSessionType);
1054
}
1055
1056
/**
1057
* Notify extension about option changes for a session
1058
*/
1059
public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void> {
1060
if (!updates.length) {
1061
return;
1062
}
1063
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: starting for ${sessionResource}, ${updates.length} update(s): [${updates.map(u => u.optionId).join(', ')}]`);
1064
// Fire event to notify MainThreadChatSessions (which forwards to extension host)
1065
// Uses fireAsync to properly await async listener work via waitUntil pattern
1066
await this._onRequestNotifyExtension.fireAsync({ sessionResource, updates }, CancellationToken.None);
1067
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: fireAsync completed for ${sessionResource}`);
1068
for (const u of updates) {
1069
this.setSessionOption(sessionResource, u.optionId, u.value);
1070
}
1071
this._onDidChangeSessionOptions.fire(sessionResource);
1072
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: finished for ${sessionResource}`);
1073
}
1074
1075
/**
1076
* Get the icon for a specific session type
1077
*/
1078
public getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined {
1079
const sessionTypeIcon = this._sessionTypeIcons.get(chatSessionType);
1080
1081
if (ThemeIcon.isThemeIcon(sessionTypeIcon)) {
1082
return sessionTypeIcon;
1083
}
1084
1085
if (isDark(this._themeService.getColorTheme().type)) {
1086
return sessionTypeIcon?.dark;
1087
} else {
1088
return sessionTypeIcon?.light;
1089
}
1090
}
1091
1092
/**
1093
* Get the welcome title for a specific session type
1094
*/
1095
public getWelcomeTitleForSessionType(chatSessionType: string): string | undefined {
1096
return this._sessionTypeWelcomeTitles.get(chatSessionType);
1097
}
1098
1099
/**
1100
* Get the welcome message for a specific session type
1101
*/
1102
public getWelcomeMessageForSessionType(chatSessionType: string): string | undefined {
1103
return this._sessionTypeWelcomeMessages.get(chatSessionType);
1104
}
1105
1106
/**
1107
* Get the input placeholder for a specific session type
1108
*/
1109
public getInputPlaceholderForSessionType(chatSessionType: string): string | undefined {
1110
return this._sessionTypeInputPlaceholders.get(chatSessionType);
1111
}
1112
1113
/**
1114
* Get the capabilities for a specific session type
1115
*/
1116
public getCapabilitiesForSessionType(chatSessionType: string): IChatAgentAttachmentCapabilities | undefined {
1117
const contribution = this._contributions.get(chatSessionType)?.contribution;
1118
return contribution?.capabilities;
1119
}
1120
1121
/**
1122
* Get the customAgentTarget for a specific session type.
1123
* When set, the mode picker should show filtered custom agents matching this target.
1124
*/
1125
public getCustomAgentTargetForSessionType(chatSessionType: string): Target {
1126
const contribution = this._contributions.get(chatSessionType)?.contribution;
1127
return contribution?.customAgentTarget ?? Target.Undefined;
1128
}
1129
1130
public getContentProviderSchemes(): string[] {
1131
return Array.from(this._contentProviders.keys());
1132
}
1133
}
1134
1135
registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);
1136
1137
function registerNewSessionInPlaceAction(type: string, displayName: string): IDisposable {
1138
return registerAction2(class NewChatSessionInPlaceAction extends Action2 {
1139
constructor() {
1140
super({
1141
id: `workbench.action.chat.openNewChatSessionInPlace.${type}`,
1142
title: localize2('interactiveSession.openNewChatSessionInPlace', "New {0}", displayName),
1143
category: CHAT_CATEGORY,
1144
f1: false,
1145
precondition: ChatContextKeys.enabled,
1146
});
1147
}
1148
1149
// Expected args: [chatSessionPosition: 'sidebar' | 'editor']
1150
async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
1151
if (args.length === 0) {
1152
throw new BugIndicatingError('Expected chat session position argument');
1153
}
1154
1155
const chatSessionPosition = args[0];
1156
if (chatSessionPosition !== ChatSessionPosition.Sidebar && chatSessionPosition !== ChatSessionPosition.Editor) {
1157
throw new BugIndicatingError(`Invalid chat session position argument: ${chatSessionPosition}`);
1158
}
1159
1160
await openChatSession(accessor, { type: type, displayName: localize('chat', "Chat"), position: chatSessionPosition, replaceEditor: true });
1161
}
1162
});
1163
}
1164
1165
function registerNewSessionExternalAction(type: string, displayName: string, commandId: string): IDisposable {
1166
return registerAction2(class NewChatSessionExternalAction extends Action2 {
1167
constructor() {
1168
super({
1169
id: `workbench.action.chat.openNewChatSessionExternal.${type}`,
1170
title: localize2('interactiveSession.openNewChatSessionExternal', "New {0}", displayName),
1171
category: CHAT_CATEGORY,
1172
f1: false,
1173
precondition: ChatContextKeys.enabled,
1174
});
1175
}
1176
async run(accessor: ServicesAccessor): Promise<void> {
1177
const commandService = accessor.get(ICommandService);
1178
await commandService.executeCommand(commandId);
1179
}
1180
});
1181
}
1182
1183
export enum ChatSessionPosition {
1184
Editor = 'editor',
1185
Sidebar = 'sidebar'
1186
}
1187
1188
type NewChatSessionSendOptions = {
1189
readonly prompt: string;
1190
readonly attachedContext?: IChatRequestVariableEntry[];
1191
};
1192
1193
export type NewChatSessionOpenOptions = {
1194
readonly type: string;
1195
readonly position: ChatSessionPosition;
1196
readonly displayName: string;
1197
readonly chatResource?: UriComponents;
1198
readonly replaceEditor?: boolean;
1199
};
1200
1201
async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise<void> {
1202
const viewsService = accessor.get(IViewsService);
1203
const chatService = accessor.get(IChatService);
1204
const logService = accessor.get(ILogService);
1205
const editorGroupService = accessor.get(IEditorGroupsService);
1206
const editorService = accessor.get(IEditorService);
1207
1208
// Determine resource to open
1209
const resource = getResourceForNewChatSession(openOptions);
1210
1211
// Open chat session
1212
try {
1213
switch (openOptions.position) {
1214
case ChatSessionPosition.Sidebar: {
1215
const view = await viewsService.openView(ChatViewId) as ChatViewPane;
1216
if (openOptions.type === AgentSessionProviders.Local) {
1217
await view.widget.clear();
1218
} else {
1219
await view.loadSession(resource);
1220
}
1221
view.focus();
1222
break;
1223
}
1224
case ChatSessionPosition.Editor: {
1225
const options: IChatEditorOptions = {
1226
override: ChatEditorInput.EditorID,
1227
pinned: true,
1228
title: {
1229
fallback: localize('chatEditorContributionName', "{0}", openOptions.displayName),
1230
}
1231
};
1232
if (openOptions.replaceEditor) {
1233
// TODO: Do not rely on active editor
1234
const activeEditor = editorGroupService.activeGroup.activeEditor;
1235
if (!activeEditor || !(activeEditor instanceof ChatEditorInput)) {
1236
throw new Error('No active chat editor to replace');
1237
}
1238
await editorService.replaceEditors([{ editor: activeEditor, replacement: { resource, options } }], editorGroupService.activeGroup);
1239
} else {
1240
await editorService.openEditor({ resource, options });
1241
}
1242
break;
1243
}
1244
default: assertNever(openOptions.position, `Unknown chat session position: ${openOptions.position}`);
1245
}
1246
} catch (e) {
1247
logService.error(`Failed to open '${openOptions.type}' chat session with openOptions: ${JSON.stringify(openOptions)}`, e);
1248
return;
1249
}
1250
1251
// Send initial prompt if provided
1252
if (chatSendOptions) {
1253
try {
1254
await chatService.sendRequest(resource, chatSendOptions.prompt, { agentIdSilent: openOptions.type, attachedContext: chatSendOptions.attachedContext });
1255
} catch (e) {
1256
logService.error(`Failed to send initial request to '${openOptions.type}' chat session with contextOptions: ${JSON.stringify(chatSendOptions)}`, e);
1257
}
1258
}
1259
}
1260
1261
export function getResourceForNewChatSession(options: NewChatSessionOpenOptions): URI {
1262
if (options.chatResource) {
1263
return URI.revive(options.chatResource);
1264
}
1265
1266
const isRemoteSession = options.type !== AgentSessionProviders.Local;
1267
if (isRemoteSession) {
1268
return URI.from({
1269
scheme: options.type,
1270
path: `/untitled-${generateUuid()}`,
1271
});
1272
}
1273
1274
const isEditorPosition = options.position === ChatSessionPosition.Editor;
1275
if (isEditorPosition) {
1276
return ChatEditorInput.getNewEditorUri();
1277
}
1278
1279
return LocalChatSessionUri.forSession(generateUuid());
1280
}
1281
1282
function isAgentSessionProviderType(type: string): boolean {
1283
return Object.values(AgentSessionProviders).includes(type as AgentSessionProviders);
1284
}
1285
1286