Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts
5270 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 { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js';
7
import { Codicon } from '../../../../base/common/codicons.js';
8
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
9
import { Event } from '../../../../base/common/event.js';
10
import { createCommandUri, MarkdownString } from '../../../../base/common/htmlContent.js';
11
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
12
import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js';
13
import * as strings from '../../../../base/common/strings.js';
14
import { localize, localize2 } from '../../../../nls.js';
15
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
16
import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
17
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
18
import { IProductService } from '../../../../platform/product/common/productService.js';
19
import { Registry } from '../../../../platform/registry/common/platform.js';
20
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
21
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
22
import { IWorkbenchContribution } from '../../../common/contributions.js';
23
import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js';
24
import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../services/extensionManagement/common/extensionFeatures.js';
25
import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
26
import * as extensionsRegistry from '../../../services/extensions/common/extensionsRegistry.js';
27
import { showExtensionsWithIdsCommandId } from '../../extensions/browser/extensionsActions.js';
28
import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
29
import { IChatAgentData, IChatAgentService } from '../common/participants/chatAgents.js';
30
import { ChatContextKeys } from '../common/actions/chatContextKeys.js';
31
import { IRawChatParticipantContribution } from '../common/participants/chatParticipantContribTypes.js';
32
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
33
import { ChatViewId, ChatViewContainerId } from './chat.js';
34
import { ChatViewPane } from './widgetHosts/viewPane/chatViewPane.js';
35
36
// --- Chat Container & View Registration
37
38
const chatViewIcon = registerIcon('chat-view-icon', Codicon.chatSparkle, localize('chatViewIcon', 'View icon of the chat view.'));
39
40
const chatViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
41
id: ChatViewContainerId,
42
title: localize2('chat.viewContainer.label', "Chat"),
43
icon: chatViewIcon,
44
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [ChatViewContainerId, { mergeViewWithContainerWhenSingleView: true }]),
45
storageId: ChatViewContainerId,
46
hideIfEmpty: true,
47
order: 1,
48
}, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true });
49
50
const chatViewDescriptor: IViewDescriptor = {
51
id: ChatViewId,
52
containerIcon: chatViewContainer.icon,
53
containerTitle: chatViewContainer.title.value,
54
singleViewPaneContainerTitle: chatViewContainer.title.value,
55
name: localize2('chat.viewContainer.label', "Chat"),
56
canToggleVisibility: false,
57
canMoveView: true,
58
openCommandActionDescriptor: {
59
id: ChatViewContainerId,
60
title: chatViewContainer.title,
61
mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"),
62
keybindings: {
63
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
64
mac: {
65
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
66
}
67
},
68
order: 1
69
},
70
ctorDescriptor: new SyncDescriptor(ChatViewPane),
71
when: ContextKeyExpr.or(
72
ContextKeyExpr.or(
73
ChatContextKeys.Setup.hidden,
74
ChatContextKeys.Setup.disabled
75
)?.negate(),
76
ChatContextKeys.panelParticipantRegistered,
77
ChatContextKeys.extensionInvalid
78
)
79
};
80
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([chatViewDescriptor], chatViewContainer);
81
82
const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
83
extensionPoint: 'chatParticipants',
84
jsonSchema: {
85
description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'),
86
type: 'array',
87
items: {
88
additionalProperties: false,
89
type: 'object',
90
defaultSnippets: [{ body: { name: '', description: '' } }],
91
required: ['name', 'id'],
92
properties: {
93
id: {
94
description: localize('chatParticipantId', "A unique id for this chat participant."),
95
type: 'string'
96
},
97
name: {
98
description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant. Name must not contain whitespace."),
99
type: 'string',
100
pattern: '^[\\w-]+$'
101
},
102
fullName: {
103
markdownDescription: localize('chatParticipantFullName', "The full name of this chat participant, which is shown as the label for responses coming from this participant. If not provided, {0} is used.", '`name`'),
104
type: 'string'
105
},
106
description: {
107
description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."),
108
type: 'string'
109
},
110
isSticky: {
111
description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."),
112
type: 'boolean'
113
},
114
sampleRequest: {
115
description: localize('chatSampleRequest', "When the user clicks this participant in `/help`, this text will be submitted to the participant."),
116
type: 'string'
117
},
118
when: {
119
description: localize('chatParticipantWhen', "A condition which must be true to enable this participant."),
120
type: 'string'
121
},
122
disambiguation: {
123
description: localize('chatParticipantDisambiguation', "Metadata to help with automatically routing user questions to this chat participant."),
124
type: 'array',
125
items: {
126
additionalProperties: false,
127
type: 'object',
128
defaultSnippets: [{ body: { category: '', description: '', examples: [] } }],
129
required: ['category', 'description', 'examples'],
130
properties: {
131
category: {
132
markdownDescription: localize('chatParticipantDisambiguationCategory', "A detailed name for this category, e.g. `workspace_questions` or `web_questions`."),
133
type: 'string'
134
},
135
description: {
136
description: localize('chatParticipantDisambiguationDescription', "A detailed description of the kinds of questions that are suitable for this chat participant."),
137
type: 'string'
138
},
139
examples: {
140
description: localize('chatParticipantDisambiguationExamples', "A list of representative example questions that are suitable for this chat participant."),
141
type: 'array'
142
},
143
}
144
}
145
},
146
commands: {
147
markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."),
148
type: 'array',
149
items: {
150
additionalProperties: false,
151
type: 'object',
152
defaultSnippets: [{ body: { name: '', description: '' } }],
153
required: ['name'],
154
properties: {
155
name: {
156
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."),
157
type: 'string'
158
},
159
description: {
160
description: localize('chatCommandDescription', "A description of this command."),
161
type: 'string'
162
},
163
when: {
164
description: localize('chatCommandWhen', "A condition which must be true to enable this command."),
165
type: 'string'
166
},
167
sampleRequest: {
168
description: localize('chatCommandSampleRequest', "When the user clicks this command in `/help`, this text will be submitted to the participant."),
169
type: 'string'
170
},
171
isSticky: {
172
description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."),
173
type: 'boolean'
174
},
175
disambiguation: {
176
description: localize('chatCommandDisambiguation', "Metadata to help with automatically routing user questions to this chat command."),
177
type: 'array',
178
items: {
179
additionalProperties: false,
180
type: 'object',
181
defaultSnippets: [{ body: { category: '', description: '', examples: [] } }],
182
required: ['category', 'description', 'examples'],
183
properties: {
184
category: {
185
markdownDescription: localize('chatCommandDisambiguationCategory', "A detailed name for this category, e.g. `workspace_questions` or `web_questions`."),
186
type: 'string'
187
},
188
description: {
189
description: localize('chatCommandDisambiguationDescription', "A detailed description of the kinds of questions that are suitable for this chat command."),
190
type: 'string'
191
},
192
examples: {
193
description: localize('chatCommandDisambiguationExamples', "A list of representative example questions that are suitable for this chat command."),
194
type: 'array'
195
},
196
}
197
}
198
}
199
}
200
}
201
},
202
}
203
}
204
},
205
activationEventsGenerator: function* (contributions: readonly IRawChatParticipantContribution[]) {
206
for (const contrib of contributions) {
207
yield `onChatParticipant:${contrib.id}`;
208
}
209
},
210
});
211
212
export class ChatExtensionPointHandler implements IWorkbenchContribution {
213
214
static readonly ID = 'workbench.contrib.chatExtensionPointHandler';
215
216
private _participantRegistrationDisposables = new DisposableMap<string>();
217
218
constructor(
219
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
220
) {
221
this.handleAndRegisterChatExtensions();
222
}
223
224
private handleAndRegisterChatExtensions(): void {
225
chatParticipantExtensionPoint.setHandler((extensions, delta) => {
226
for (const extension of delta.added) {
227
for (const providerDescriptor of extension.value) {
228
if (!providerDescriptor.name?.match(/^[\w-]+$/)) {
229
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w-]+$/.`);
230
continue;
231
}
232
233
if (providerDescriptor.fullName && strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter(providerDescriptor.fullName)) {
234
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains ambiguous characters: ${providerDescriptor.fullName}.`);
235
continue;
236
}
237
238
// Spaces are allowed but considered "invisible"
239
if (providerDescriptor.fullName && strings.InvisibleCharacters.containsInvisibleCharacter(providerDescriptor.fullName.replace(/ /g, ''))) {
240
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains invisible characters: ${providerDescriptor.fullName}.`);
241
continue;
242
}
243
244
if ((providerDescriptor.isDefault || providerDescriptor.modes) && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) {
245
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`);
246
continue;
247
}
248
249
if (providerDescriptor.locations && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) {
250
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`);
251
continue;
252
}
253
254
if (!providerDescriptor.id || !providerDescriptor.name) {
255
extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`);
256
continue;
257
}
258
259
const participantsDisambiguation: {
260
category: string;
261
description: string;
262
examples: string[];
263
}[] = [];
264
265
if (providerDescriptor.disambiguation?.length) {
266
participantsDisambiguation.push(...providerDescriptor.disambiguation.map((d) => ({
267
...d, category: d.category ?? d.categoryName
268
})));
269
}
270
271
try {
272
const store = new DisposableStore();
273
store.add(this._chatAgentService.registerAgent(
274
providerDescriptor.id,
275
{
276
extensionId: extension.description.identifier,
277
extensionVersion: extension.description.version,
278
publisherDisplayName: extension.description.publisherDisplayName ?? extension.description.publisher, // May not be present in OSS
279
extensionPublisherId: extension.description.publisher,
280
extensionDisplayName: extension.description.displayName ?? extension.description.name,
281
id: providerDescriptor.id,
282
description: providerDescriptor.description,
283
when: providerDescriptor.when,
284
metadata: {
285
isSticky: providerDescriptor.isSticky,
286
sampleRequest: providerDescriptor.sampleRequest,
287
},
288
name: providerDescriptor.name,
289
fullName: providerDescriptor.fullName,
290
isDefault: providerDescriptor.isDefault,
291
locations: isNonEmptyArray(providerDescriptor.locations) ?
292
providerDescriptor.locations.map(ChatAgentLocation.fromRaw) :
293
[ChatAgentLocation.Chat],
294
modes: providerDescriptor.isDefault ? (providerDescriptor.modes ?? [ChatModeKind.Ask]) : [ChatModeKind.Agent, ChatModeKind.Ask, ChatModeKind.Edit],
295
slashCommands: providerDescriptor.commands ?? [],
296
disambiguation: coalesce(participantsDisambiguation.flat()),
297
} satisfies IChatAgentData));
298
299
this._participantRegistrationDisposables.set(
300
getParticipantKey(extension.description.identifier, providerDescriptor.id),
301
store
302
);
303
} catch (e) {
304
extension.collector.error(`Failed to register participant ${providerDescriptor.id}: ${toErrorMessage(e, true)}`);
305
}
306
}
307
}
308
309
for (const extension of delta.removed) {
310
for (const providerDescriptor of extension.value) {
311
this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.id));
312
}
313
}
314
});
315
}
316
}
317
318
function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
319
return `${extensionId.value}_${participantName}`;
320
}
321
322
export class ChatCompatibilityNotifier extends Disposable implements IWorkbenchContribution {
323
static readonly ID = 'workbench.contrib.chatCompatNotifier';
324
325
private registeredWelcomeView = false;
326
327
constructor(
328
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
329
@IContextKeyService contextKeyService: IContextKeyService,
330
@IProductService private readonly productService: IProductService,
331
) {
332
super();
333
334
// It may be better to have some generic UI for this, for any extension that is incompatible,
335
// but this is only enabled for Chat now and it needs to be obvious.
336
const isInvalid = ChatContextKeys.extensionInvalid.bindTo(contextKeyService);
337
this._register(Event.runAndSubscribe(
338
extensionsWorkbenchService.onDidChangeExtensionsNotification,
339
() => {
340
const notification = extensionsWorkbenchService.getExtensionsNotification();
341
const chatExtension = notification?.extensions.find(ext => ExtensionIdentifier.equals(ext.identifier.id, this.productService.defaultChatAgent?.chatExtensionId));
342
if (chatExtension) {
343
isInvalid.set(true);
344
this.registerWelcomeView(chatExtension);
345
} else {
346
isInvalid.set(false);
347
}
348
}
349
));
350
}
351
352
private registerWelcomeView(chatExtension: IExtension) {
353
if (this.registeredWelcomeView) {
354
return;
355
}
356
357
this.registeredWelcomeView = true;
358
const showExtensionLabel = localize('showExtension', "Show Extension");
359
const mainMessage = localize('chatFailErrorMessage', "Chat failed to load because the installed version of the Copilot Chat extension is not compatible with this version of {0}. Please ensure that the Copilot Chat extension is up to date.", this.productService.nameLong);
360
const commandButton = `[${showExtensionLabel}](${createCommandUri(showExtensionsWithIdsCommandId, [this.productService.defaultChatAgent?.chatExtensionId])})`;
361
const versionMessage = `Copilot Chat version: ${chatExtension.version}`;
362
const viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
363
this._register(viewsRegistry.registerViewWelcomeContent(ChatViewId, {
364
content: [mainMessage, commandButton, versionMessage].join('\n\n'),
365
when: ChatContextKeys.extensionInvalid,
366
}));
367
}
368
}
369
370
class ChatParticipantDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
371
readonly type = 'table';
372
373
shouldRender(manifest: IExtensionManifest): boolean {
374
return !!manifest.contributes?.chatParticipants;
375
}
376
377
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
378
const nonDefaultContributions = manifest.contributes?.chatParticipants?.filter(c => !c.isDefault) ?? [];
379
if (!nonDefaultContributions.length) {
380
return { data: { headers: [], rows: [] }, dispose: () => { } };
381
}
382
383
const headers = [
384
localize('participantName', "Name"),
385
localize('participantFullName', "Full Name"),
386
localize('participantDescription', "Description"),
387
localize('participantCommands', "Commands"),
388
];
389
390
const rows: IRowData[][] = nonDefaultContributions.map(d => {
391
return [
392
'@' + d.name,
393
d.fullName,
394
d.description ?? '-',
395
d.commands?.length ? new MarkdownString(d.commands.map(c => `- /` + c.name).join('\n')) : '-'
396
];
397
});
398
399
return {
400
data: {
401
headers,
402
rows
403
},
404
dispose: () => { }
405
};
406
}
407
}
408
409
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
410
id: 'chatParticipants',
411
label: localize('chatParticipants', "Chat Participants"),
412
access: {
413
canToggle: false
414
},
415
renderer: new SyncDescriptor(ChatParticipantDataRenderer),
416
});
417
418