Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts
5251 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 './media/chatModelsWidget.css';
7
import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
8
import { Emitter } from '../../../../../base/common/event.js';
9
import * as DOM from '../../../../../base/browser/dom.js';
10
import { Button, IButtonOptions } from '../../../../../base/browser/ui/button/button.js';
11
import { ThemeIcon } from '../../../../../base/common/themables.js';
12
import { ILanguageModelsService, ILanguageModelProviderDescriptor } from '../../../chat/common/languageModels.js';
13
import { localize } from '../../../../../nls.js';
14
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
15
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { WorkbenchTable } from '../../../../../platform/list/browser/listService.js';
17
import { ITableVirtualDelegate, ITableRenderer } from '../../../../../base/browser/ui/table/table.js';
18
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
19
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
20
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
21
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
22
import { IAction, toAction, Action, Separator, SubmenuAction } from '../../../../../base/common/actions.js';
23
import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js';
24
import { Codicon } from '../../../../../base/common/codicons.js';
25
import { ChatModelsViewModel, ILanguageModel, ILanguageModelEntry, ILanguageModelProviderEntry, ILanguageModelGroupEntry, SEARCH_SUGGESTIONS, isLanguageModelProviderEntry, isLanguageModelGroupEntry, ChatModelGroup, IViewModelEntry, isStatusEntry, IStatusEntry } from './chatModelsViewModel.js';
26
import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
27
import { SuggestEnabledInput } from '../../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';
28
import { Delayer } from '../../../../../base/common/async.js';
29
import { settingsTextInputBorder } from '../../../preferences/common/settingsEditorColorRegistry.js';
30
import { IChatEntitlementService, ChatEntitlement } from '../../../../services/chat/common/chatEntitlementService.js';
31
import { DropdownMenuActionViewItem } from '../../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
32
import { IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
33
import { AnchorAlignment } from '../../../../../base/browser/ui/contextview/contextview.js';
34
import { ToolBar } from '../../../../../base/browser/ui/toolbar/toolbar.js';
35
import { preferencesClearInputIcon } from '../../../preferences/browser/preferencesIcons.js';
36
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
37
import { IEditorProgressService } from '../../../../../platform/progress/common/progress.js';
38
import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
39
import { CONTEXT_MODELS_SEARCH_FOCUS } from '../../common/constants.js';
40
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
41
import Severity from '../../../../../base/common/severity.js';
42
43
const $ = DOM.$;
44
45
const HEADER_HEIGHT = 30;
46
const VENDOR_ROW_HEIGHT = 30;
47
const MODEL_ROW_HEIGHT = 26;
48
49
export function getModelHoverContent(model: ILanguageModel): MarkdownString {
50
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
51
markdown.appendMarkdown(`**${model.metadata.name}**`);
52
if (model.metadata.id !== model.metadata.version) {
53
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${model.metadata.id}@${model.metadata.version}_&nbsp;</span>`);
54
} else {
55
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${model.metadata.id}_&nbsp;</span>`);
56
}
57
markdown.appendText(`\n`);
58
59
if (model.metadata.statusIcon && model.metadata.tooltip) {
60
if (model.metadata.statusIcon) {
61
markdown.appendMarkdown(`$(${model.metadata.statusIcon.id})&nbsp;`);
62
}
63
markdown.appendMarkdown(`${model.metadata.tooltip}`);
64
markdown.appendText(`\n`);
65
}
66
67
if (model.metadata.multiplier) {
68
markdown.appendMarkdown(`${localize('models.cost', 'Multiplier')}: `);
69
markdown.appendMarkdown(model.metadata.multiplier);
70
markdown.appendText(`\n`);
71
}
72
73
if (model.metadata.maxInputTokens || model.metadata.maxOutputTokens) {
74
markdown.appendMarkdown(`${localize('models.contextSize', 'Context Size')}: `);
75
let addSeparator = false;
76
if (model.metadata.maxInputTokens) {
77
markdown.appendMarkdown(`$(arrow-down) ${formatTokenCount(model.metadata.maxInputTokens)} (${localize('models.input', 'Input')})`);
78
addSeparator = true;
79
}
80
if (model.metadata.maxOutputTokens) {
81
if (addSeparator) {
82
markdown.appendText(` | `);
83
}
84
markdown.appendMarkdown(`$(arrow-up) ${formatTokenCount(model.metadata.maxOutputTokens)} (${localize('models.output', 'Output')})`);
85
}
86
markdown.appendText(`\n`);
87
}
88
89
if (model.metadata.capabilities) {
90
markdown.appendMarkdown(`${localize('models.capabilities', 'Capabilities')}: `);
91
if (model.metadata.capabilities?.toolCalling) {
92
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${localize('models.toolCalling', 'Tools')}_&nbsp;</span>`);
93
}
94
if (model.metadata.capabilities?.vision) {
95
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${localize('models.vision', 'Vision')}_&nbsp;</span>`);
96
}
97
if (model.metadata.capabilities?.agentMode) {
98
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${localize('models.agentMode', 'Agent Mode')}_&nbsp;</span>`);
99
}
100
for (const editTool of model.metadata.capabilities.editTools ?? []) {
101
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${editTool}_&nbsp;</span>`);
102
}
103
markdown.appendText(`\n`);
104
}
105
106
return markdown;
107
}
108
109
class ModelsFilterAction extends Action {
110
constructor() {
111
super('workbench.models.filter', localize('filter', "Filter"), ThemeIcon.asClassName(Codicon.filter));
112
}
113
override async run(): Promise<void> {
114
}
115
}
116
117
interface IFilterQuery {
118
/** The primary filter query string */
119
query: string;
120
/** Alternative query strings that are treated as synonyms of the primary query */
121
synonyms?: string[];
122
/** Query strings that should be removed when adding this filter (mutually exclusive filters) */
123
excludes?: string[];
124
}
125
126
function toggleFilter(currentQuery: string, filter: IFilterQuery): string {
127
const { query, synonyms = [], excludes = [] } = filter;
128
const allSynonyms = [query, ...synonyms];
129
const isChecked = allSynonyms.some(q => currentQuery.includes(q));
130
const hasExcludedQuery = excludes.some(q => currentQuery.includes(q));
131
132
if (isChecked) {
133
// Query or synonym is already set, remove all of them (toggle off)
134
let queryWithRemovedFilter = currentQuery;
135
for (const q of allSynonyms) {
136
queryWithRemovedFilter = queryWithRemovedFilter.replace(q, '');
137
}
138
return queryWithRemovedFilter.replace(/\s+/g, ' ').trim();
139
} else if (hasExcludedQuery) {
140
// An excluded query is set, replace it with the new query
141
let newQuery = currentQuery;
142
for (const q of excludes) {
143
newQuery = newQuery.replace(q, '');
144
}
145
newQuery = newQuery.replace(/\s+/g, ' ').trim();
146
return newQuery ? `${newQuery} ${query}` : query;
147
} else {
148
// No filter is set, add the new query
149
const trimmedQuery = currentQuery.trim();
150
return trimmedQuery ? `${trimmedQuery} ${query}` : query;
151
}
152
}
153
154
class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem {
155
156
constructor(
157
action: IAction,
158
options: IActionViewItemOptions,
159
private readonly search: {
160
getValue(): string;
161
setValue(newValue: string): void;
162
},
163
private readonly viewModel: ChatModelsViewModel,
164
@IContextMenuService contextMenuService: IContextMenuService
165
) {
166
super(action,
167
{ getActions: () => this.getActions() },
168
contextMenuService,
169
{
170
...options,
171
classNames: action.class,
172
anchorAlignmentProvider: () => AnchorAlignment.RIGHT,
173
menuAsChild: true
174
}
175
);
176
}
177
178
private createGroupByAction(grouping: ChatModelGroup, label: string): IAction {
179
return {
180
id: `groupBy.${grouping}`,
181
label,
182
class: undefined,
183
enabled: true,
184
tooltip: localize('groupByTooltip', "Group by {0}", label),
185
checked: this.viewModel.groupBy === grouping,
186
run: () => {
187
this.viewModel.groupBy = grouping;
188
}
189
};
190
}
191
192
private createProviderAction(vendor: string, displayName: string): IAction {
193
const query = `@provider:"${displayName}"`;
194
const currentQuery = this.search.getValue();
195
const isChecked = currentQuery.includes(query) || currentQuery.includes(`@provider:${vendor}`);
196
197
return {
198
id: `provider-${vendor}`,
199
label: displayName,
200
tooltip: localize('filterByProvider', "Filter by {0}", displayName),
201
class: undefined,
202
enabled: true,
203
checked: isChecked,
204
run: () => this.toggleFilterAndSearch({ query, synonyms: [`@provider:${vendor}`] })
205
};
206
}
207
208
private createCapabilityAction(capability: string, label: string): IAction {
209
const query = `@capability:${capability}`;
210
const currentQuery = this.search.getValue();
211
const isChecked = currentQuery.includes(query);
212
213
return {
214
id: `capability-${capability}`,
215
label,
216
tooltip: localize('filterByCapability', "Filter by {0}", label),
217
class: undefined,
218
enabled: true,
219
checked: isChecked,
220
run: () => this.toggleFilterAndSearch({ query })
221
};
222
}
223
224
private createVisibleAction(visible: boolean, label: string): IAction {
225
const query = `@visible:${visible}`;
226
const currentQuery = this.search.getValue();
227
const isChecked = currentQuery.includes(query);
228
229
return {
230
id: `visible-${visible}`,
231
label,
232
tooltip: localize('filterByVisible', "Filter by {0}", label),
233
class: undefined,
234
enabled: true,
235
checked: isChecked,
236
run: () => this.toggleFilterAndSearch({ query, excludes: [`@visible:${!visible}`] })
237
};
238
}
239
240
private toggleFilterAndSearch(filter: IFilterQuery): void {
241
const currentQuery = this.search.getValue();
242
const newQuery = toggleFilter(currentQuery, filter);
243
this.search.setValue(newQuery);
244
}
245
246
private getActions(): IAction[] {
247
const actions: IAction[] = [];
248
249
// Capability filters
250
actions.push(
251
this.createCapabilityAction('tools', localize('capability.tools', "Tools")),
252
this.createCapabilityAction('vision', localize('capability.vision', "Vision")),
253
this.createCapabilityAction('agent', localize('capability.agent', "Agent Mode"))
254
);
255
256
// Visibility filters
257
actions.push(new Separator());
258
actions.push(this.createVisibleAction(true, localize('filter.visible', "Visible in Chat Model Picker")));
259
actions.push(this.createVisibleAction(false, localize('filter.hidden', "Hidden in Chat Model Picker")));
260
261
// Provider filters - only show providers with configured models
262
const configuredVendors = this.viewModel.getConfiguredVendors();
263
if (configuredVendors.length > 1) {
264
actions.push(new Separator());
265
actions.push(...configuredVendors.map(vendor => this.createProviderAction(vendor.vendor.vendor, vendor.group.name)));
266
}
267
268
// Group By
269
actions.push(new Separator());
270
const groupByActions: IAction[] = [];
271
groupByActions.push(this.createGroupByAction(ChatModelGroup.Vendor, localize('groupBy.provider', "Provider")));
272
groupByActions.push(this.createGroupByAction(ChatModelGroup.Visibility, localize('groupBy.visibility', "Visibility (Chat Model Picker)")));
273
actions.push(new SubmenuAction('groupBy', localize('groupBy', "Group By"), groupByActions));
274
275
return actions;
276
}
277
}
278
279
class Delegate implements ITableVirtualDelegate<IViewModelEntry> {
280
readonly headerRowHeight = HEADER_HEIGHT;
281
getHeight(element: IViewModelEntry): number {
282
return isLanguageModelProviderEntry(element) || isLanguageModelGroupEntry(element) ? VENDOR_ROW_HEIGHT : MODEL_ROW_HEIGHT;
283
}
284
}
285
286
interface IModelTableColumnTemplateData {
287
readonly container: HTMLElement;
288
readonly disposables: DisposableStore;
289
readonly elementDisposables: DisposableStore;
290
}
291
292
abstract class ModelsTableColumnRenderer<T extends IModelTableColumnTemplateData> implements ITableRenderer<IViewModelEntry, T> {
293
abstract readonly templateId: string;
294
abstract renderTemplate(container: HTMLElement): T;
295
296
renderElement(element: IViewModelEntry, index: number, templateData: T): void {
297
templateData.elementDisposables.clear();
298
const isVendor = isLanguageModelProviderEntry(element);
299
const isGroup = isLanguageModelGroupEntry(element);
300
const isStatus = isStatusEntry(element);
301
templateData.container.classList.add('models-table-column');
302
templateData.container.parentElement!.classList.toggle('models-vendor-row', isVendor || isGroup);
303
templateData.container.parentElement!.classList.toggle('models-model-row', !isVendor && !isGroup);
304
templateData.container.parentElement!.classList.toggle('models-status-row', isStatus);
305
templateData.container.parentElement!.classList.toggle('model-hidden', !isVendor && !isGroup && !isStatus && !element.model.visible);
306
if (isVendor) {
307
this.renderVendorElement(element, index, templateData);
308
} else if (isGroup) {
309
this.renderGroupElement(element, index, templateData);
310
} else if (isStatus) {
311
this.renderStatusElement(element, index, templateData);
312
} else {
313
this.renderModelElement(element, index, templateData);
314
}
315
}
316
317
abstract renderVendorElement(element: ILanguageModelProviderEntry, index: number, templateData: T): void;
318
abstract renderGroupElement(element: ILanguageModelGroupEntry, index: number, templateData: T): void;
319
abstract renderModelElement(element: ILanguageModelEntry, index: number, templateData: T): void;
320
321
protected renderStatusElement(element: IStatusEntry, index: number, templateData: T): void { }
322
323
disposeTemplate(templateData: T): void {
324
templateData.elementDisposables.dispose();
325
templateData.disposables.dispose();
326
}
327
}
328
329
interface IToggleCollapseColumnTemplateData extends IModelTableColumnTemplateData {
330
readonly listRowElement: HTMLElement | null;
331
readonly container: HTMLElement;
332
readonly actionBar: ActionBar;
333
}
334
335
class GutterColumnRenderer extends ModelsTableColumnRenderer<IToggleCollapseColumnTemplateData> {
336
337
static readonly TEMPLATE_ID = 'gutter';
338
339
readonly templateId: string = GutterColumnRenderer.TEMPLATE_ID;
340
341
constructor(
342
private readonly viewModel: ChatModelsViewModel,
343
) {
344
super();
345
}
346
347
renderTemplate(container: HTMLElement): IToggleCollapseColumnTemplateData {
348
const disposables = new DisposableStore();
349
const elementDisposables = new DisposableStore();
350
container.classList.add('models-gutter-column');
351
const actionBar = disposables.add(new ActionBar(container));
352
return {
353
listRowElement: container.parentElement?.parentElement ?? null,
354
container,
355
actionBar,
356
disposables,
357
elementDisposables
358
};
359
}
360
361
override renderElement(entry: IViewModelEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {
362
templateData.actionBar.clear();
363
super.renderElement(entry, index, templateData);
364
}
365
366
override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {
367
this.renderCollapsableElement(entry, templateData);
368
}
369
370
override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {
371
this.renderCollapsableElement(entry, templateData);
372
}
373
374
private renderCollapsableElement(entry: ILanguageModelProviderEntry | ILanguageModelGroupEntry, templateData: IToggleCollapseColumnTemplateData): void {
375
if (templateData.listRowElement) {
376
templateData.listRowElement.setAttribute('aria-expanded', entry.collapsed ? 'false' : 'true');
377
}
378
379
const label = entry.collapsed ? localize('expand', 'Expand') : localize('collapse', 'Collapse');
380
const toggleCollapseAction = {
381
id: 'toggleCollapse',
382
label,
383
tooltip: label,
384
enabled: true,
385
class: ThemeIcon.asClassName(entry.collapsed ? Codicon.chevronRight : Codicon.chevronDown),
386
run: () => this.viewModel.toggleCollapsed(entry)
387
};
388
templateData.actionBar.push(toggleCollapseAction, { icon: true, label: false });
389
}
390
391
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {
392
const { model: modelEntry } = entry;
393
const isVisible = modelEntry.visible;
394
const toggleVisibilityAction = toAction({
395
id: 'toggleVisibility',
396
label: isVisible ? localize('models.hide', 'Hide') : localize('models.show', 'Show'),
397
class: `model-visibility-toggle ${isVisible ? `${ThemeIcon.asClassName(Codicon.eye)} model-visible` : `${ThemeIcon.asClassName(Codicon.eyeClosed)} model-hidden`}`,
398
tooltip: isVisible ? localize('models.visible', 'Hide in the chat model picker') : localize('models.hidden', 'Show in the chat model picker'),
399
checked: !isVisible,
400
run: async () => this.viewModel.toggleVisibility(entry)
401
});
402
templateData.actionBar.push(toggleVisibilityAction, { icon: true, label: false });
403
}
404
}
405
406
interface IModelNameColumnTemplateData extends IModelTableColumnTemplateData {
407
readonly statusIcon: HTMLElement;
408
readonly nameLabel: HighlightedLabel;
409
readonly modelStatusIcon: HTMLElement;
410
readonly actionBar: ActionBar;
411
}
412
413
class ModelNameColumnRenderer extends ModelsTableColumnRenderer<IModelNameColumnTemplateData> {
414
static readonly TEMPLATE_ID = 'modelName';
415
416
readonly templateId: string = ModelNameColumnRenderer.TEMPLATE_ID;
417
418
constructor(
419
@IHoverService private readonly hoverService: IHoverService
420
) {
421
super();
422
}
423
424
renderTemplate(container: HTMLElement): IModelNameColumnTemplateData {
425
const disposables = new DisposableStore();
426
const elementDisposables = new DisposableStore();
427
const nameContainer = DOM.append(container, $('.model-name-container'));
428
const statusIcon = DOM.append(nameContainer, $('.status-icon'));
429
const nameLabel = disposables.add(new HighlightedLabel(DOM.append(nameContainer, $('.model-name'))));
430
const modelStatusIcon = DOM.append(nameContainer, $('.model-status-icon'));
431
const actionBar = disposables.add(new ActionBar(DOM.append(nameContainer, $('.model-name-actions'))));
432
return {
433
container,
434
statusIcon,
435
nameLabel,
436
modelStatusIcon,
437
actionBar,
438
disposables,
439
elementDisposables
440
};
441
}
442
443
override renderElement(entry: IViewModelEntry, index: number, templateData: IModelNameColumnTemplateData): void {
444
DOM.clearNode(templateData.modelStatusIcon);
445
templateData.actionBar.clear();
446
templateData.nameLabel.element.classList.remove('error-status', 'warning-status', 'info-status');
447
super.renderElement(entry, index, templateData);
448
}
449
450
override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IModelNameColumnTemplateData): void {
451
templateData.nameLabel.set(entry.vendorEntry.group.name, undefined);
452
}
453
454
override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IModelNameColumnTemplateData): void {
455
templateData.nameLabel.set(entry.label, undefined);
456
}
457
458
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IModelNameColumnTemplateData): void {
459
const { model: modelEntry, modelNameMatches } = entry;
460
461
templateData.statusIcon.style.display = 'none';
462
templateData.modelStatusIcon.className = 'model-status-icon';
463
if (modelEntry.metadata.statusIcon) {
464
templateData.modelStatusIcon.classList.add(...ThemeIcon.asClassNameArray(modelEntry.metadata.statusIcon));
465
templateData.modelStatusIcon.style.display = '';
466
} else {
467
templateData.modelStatusIcon.style.display = 'none';
468
}
469
470
templateData.nameLabel.set(modelEntry.metadata.name, modelNameMatches);
471
472
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
473
markdown.appendMarkdown(`**${entry.model.metadata.name}**`);
474
if (entry.model.metadata.id !== entry.model.metadata.version) {
475
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${entry.model.metadata.id}@${entry.model.metadata.version}_&nbsp;</span>`);
476
} else {
477
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">&nbsp;_${entry.model.metadata.id}_&nbsp;</span>`);
478
}
479
markdown.appendText(`\n`);
480
481
if (entry.model.metadata.statusIcon && entry.model.metadata.tooltip) {
482
if (entry.model.metadata.statusIcon) {
483
markdown.appendMarkdown(`$(${entry.model.metadata.statusIcon.id})&nbsp;`);
484
}
485
markdown.appendMarkdown(`${entry.model.metadata.tooltip}`);
486
markdown.appendText(`\n`);
487
}
488
489
if (!entry.model.visible) {
490
markdown.appendMarkdown(`\n\n${localize('models.userSelectable', 'This model is hidden in the chat model picker')}`);
491
}
492
493
templateData.elementDisposables.add(this.hoverService.setupDelayedHoverAtMouse(templateData.container!, () => ({
494
content: markdown,
495
appearance: {
496
compact: true,
497
skipFadeInAnimation: true,
498
}
499
})));
500
}
501
502
protected override renderStatusElement(entry: IStatusEntry, index: number, templateData: IModelNameColumnTemplateData): void {
503
templateData.statusIcon.style.display = '';
504
templateData.statusIcon.className = 'status-icon';
505
switch (entry.severity) {
506
case Severity.Error:
507
templateData.nameLabel.element.classList.add('error-status');
508
templateData.statusIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.error));
509
break;
510
case Severity.Warning:
511
templateData.nameLabel.element.classList.add('warning-status');
512
templateData.statusIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning));
513
break;
514
case Severity.Info:
515
templateData.nameLabel.element.classList.add('info-status');
516
templateData.statusIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info));
517
break;
518
}
519
templateData.nameLabel.set(entry.message, undefined, entry.message);
520
}
521
}
522
523
interface IMultiplierColumnTemplateData extends IModelTableColumnTemplateData {
524
readonly multiplierElement: HTMLElement;
525
}
526
527
class MultiplierColumnRenderer extends ModelsTableColumnRenderer<IMultiplierColumnTemplateData> {
528
static readonly TEMPLATE_ID = 'multiplier';
529
530
readonly templateId: string = MultiplierColumnRenderer.TEMPLATE_ID;
531
532
constructor(
533
@IHoverService private readonly hoverService: IHoverService
534
) {
535
super();
536
}
537
538
renderTemplate(container: HTMLElement): IMultiplierColumnTemplateData {
539
const disposables = new DisposableStore();
540
const elementDisposables = new DisposableStore();
541
const multiplierElement = DOM.append(container, $('.model-multiplier'));
542
return {
543
container,
544
multiplierElement,
545
disposables,
546
elementDisposables
547
};
548
}
549
550
override renderElement(entry: IViewModelEntry, index: number, templateData: IMultiplierColumnTemplateData): void {
551
templateData.multiplierElement.textContent = '';
552
super.renderElement(entry, index, templateData);
553
}
554
555
override renderGroupElement(element: ILanguageModelGroupEntry, index: number, templateData: IMultiplierColumnTemplateData): void {
556
}
557
558
override renderVendorElement(element: ILanguageModelProviderEntry, index: number, templateData: IMultiplierColumnTemplateData): void {
559
560
}
561
562
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IMultiplierColumnTemplateData): void {
563
const multiplierText = entry.model.metadata.multiplier ?? '-';
564
templateData.multiplierElement.textContent = multiplierText;
565
566
if (multiplierText !== '-') {
567
templateData.elementDisposables.add(this.hoverService.setupDelayedHoverAtMouse(templateData.container, () => ({
568
content: localize('multiplier.tooltip', "Every chat message counts {0} towards your premium model request quota", multiplierText),
569
appearance: {
570
compact: true,
571
skipFadeInAnimation: true
572
}
573
})));
574
}
575
}
576
}
577
578
interface ITokenLimitsColumnTemplateData extends IModelTableColumnTemplateData {
579
readonly tokenLimitsElement: HTMLElement;
580
}
581
582
class TokenLimitsColumnRenderer extends ModelsTableColumnRenderer<ITokenLimitsColumnTemplateData> {
583
static readonly TEMPLATE_ID = 'tokenLimits';
584
585
readonly templateId: string = TokenLimitsColumnRenderer.TEMPLATE_ID;
586
587
constructor(
588
@IHoverService private readonly hoverService: IHoverService
589
) {
590
super();
591
}
592
593
renderTemplate(container: HTMLElement): ITokenLimitsColumnTemplateData {
594
const disposables = new DisposableStore();
595
const elementDisposables = new DisposableStore();
596
const tokenLimitsElement = DOM.append(container, $('.model-token-limits'));
597
return {
598
container,
599
tokenLimitsElement,
600
disposables,
601
elementDisposables
602
};
603
}
604
605
override renderElement(entry: IViewModelEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {
606
DOM.clearNode(templateData.tokenLimitsElement);
607
super.renderElement(entry, index, templateData);
608
}
609
610
override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {
611
}
612
613
override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {
614
}
615
616
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {
617
const { model: modelEntry } = entry;
618
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
619
if (modelEntry.metadata.maxInputTokens || modelEntry.metadata.maxOutputTokens) {
620
let addSeparator = false;
621
markdown.appendMarkdown(`${localize('models.contextSize', 'Context Size')}: `);
622
if (modelEntry.metadata.maxInputTokens) {
623
const inputDiv = DOM.append(templateData.tokenLimitsElement, $('.token-limit-item'));
624
DOM.append(inputDiv, $('span.codicon.codicon-arrow-down'));
625
const inputText = DOM.append(inputDiv, $('span'));
626
inputText.textContent = formatTokenCount(modelEntry.metadata.maxInputTokens);
627
628
markdown.appendMarkdown(`$(arrow-down) ${modelEntry.metadata.maxInputTokens} (${localize('models.input', 'Input')})`);
629
addSeparator = true;
630
}
631
if (modelEntry.metadata.maxOutputTokens) {
632
const outputDiv = DOM.append(templateData.tokenLimitsElement, $('.token-limit-item'));
633
DOM.append(outputDiv, $('span.codicon.codicon-arrow-up'));
634
const outputText = DOM.append(outputDiv, $('span'));
635
outputText.textContent = formatTokenCount(modelEntry.metadata.maxOutputTokens);
636
if (addSeparator) {
637
markdown.appendText(` | `);
638
}
639
markdown.appendMarkdown(`$(arrow-up) ${modelEntry.metadata.maxOutputTokens} (${localize('models.output', 'Output')})`);
640
}
641
}
642
643
templateData.elementDisposables.add(this.hoverService.setupDelayedHoverAtMouse(templateData.container, () => ({
644
content: markdown,
645
appearance: {
646
compact: true,
647
skipFadeInAnimation: true,
648
}
649
})));
650
}
651
}
652
653
interface ICapabilitiesColumnTemplateData extends IModelTableColumnTemplateData {
654
readonly metadataRow: HTMLElement;
655
}
656
657
class CapabilitiesColumnRenderer extends ModelsTableColumnRenderer<ICapabilitiesColumnTemplateData> {
658
static readonly TEMPLATE_ID = 'capabilities';
659
660
readonly templateId: string = CapabilitiesColumnRenderer.TEMPLATE_ID;
661
662
private readonly _onDidClickCapability = new Emitter<string>();
663
readonly onDidClickCapability = this._onDidClickCapability.event;
664
665
renderTemplate(container: HTMLElement): ICapabilitiesColumnTemplateData {
666
const disposables = new DisposableStore();
667
const elementDisposables = new DisposableStore();
668
container.classList.add('model-capability-column');
669
const metadataRow = DOM.append(container, $('.model-capabilities'));
670
return {
671
container,
672
metadataRow,
673
disposables,
674
elementDisposables
675
};
676
}
677
678
override renderElement(entry: IViewModelEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {
679
DOM.clearNode(templateData.metadataRow);
680
super.renderElement(entry, index, templateData);
681
}
682
683
override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {
684
}
685
686
override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {
687
}
688
689
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {
690
const { model: modelEntry, capabilityMatches } = entry;
691
692
if (modelEntry.metadata.capabilities?.toolCalling) {
693
templateData.elementDisposables.add(this.createCapabilityButton(
694
templateData.metadataRow,
695
capabilityMatches?.includes('toolCalling') || false,
696
localize('models.tools', 'Tools'),
697
'tools'
698
));
699
}
700
701
if (modelEntry.metadata.capabilities?.vision) {
702
templateData.elementDisposables.add(this.createCapabilityButton(
703
templateData.metadataRow,
704
capabilityMatches?.includes('vision') || false,
705
localize('models.vision', 'Vision'),
706
'vision'
707
));
708
}
709
}
710
711
private createCapabilityButton(container: HTMLElement, isActive: boolean, label: string, capability: string): IDisposable {
712
const disposables = new DisposableStore();
713
const buttonContainer = DOM.append(container, $('.model-badge-container'));
714
const button = disposables.add(new Button(buttonContainer, { secondary: true }));
715
button.element.classList.add('model-capability');
716
button.element.classList.toggle('active', isActive);
717
button.label = label;
718
disposables.add(button.onDidClick(() => this._onDidClickCapability.fire(capability)));
719
return disposables;
720
}
721
}
722
723
interface IActionsColumnTemplateData extends IModelTableColumnTemplateData {
724
readonly actionBar: ToolBar;
725
}
726
727
class ActionsColumnRenderer extends ModelsTableColumnRenderer<IActionsColumnTemplateData> {
728
static readonly TEMPLATE_ID = 'actions';
729
730
readonly templateId: string = ActionsColumnRenderer.TEMPLATE_ID;
731
732
constructor(
733
private readonly viewModel: ChatModelsViewModel,
734
@IInstantiationService private readonly instantiationService: IInstantiationService,
735
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
736
@IDialogService private readonly dialogService: IDialogService,
737
@ICommandService private readonly commandService: ICommandService,
738
@IContextMenuService private readonly contextMenuService: IContextMenuService
739
) {
740
super();
741
}
742
743
renderTemplate(container: HTMLElement): IActionsColumnTemplateData {
744
const disposables = new DisposableStore();
745
const elementDisposables = new DisposableStore();
746
container.classList.add('models-actions-column');
747
const parent = DOM.append(container, $('.actions-container'));
748
const actionBar = disposables.add(this.instantiationService.createInstance(ToolBar,
749
parent,
750
this.contextMenuService,
751
{
752
icon: true,
753
label: false,
754
moreIcon: Codicon.gear,
755
anchorAlignmentProvider: () => AnchorAlignment.RIGHT
756
}
757
));
758
return {
759
container,
760
actionBar,
761
disposables,
762
elementDisposables
763
};
764
}
765
766
override renderElement(entry: IViewModelEntry, index: number, templateData: IActionsColumnTemplateData): void {
767
templateData.actionBar.setActions([]);
768
super.renderElement(entry, index, templateData);
769
}
770
771
override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IActionsColumnTemplateData): void {
772
const { vendorEntry } = entry;
773
const primaryActions: IAction[] = [];
774
const secondaryActions: IAction[] = [];
775
if (vendorEntry.vendor.configuration) {
776
secondaryActions.push(toAction({
777
id: 'configureAction',
778
label: localize('models.configure', 'Configure...'),
779
run: () => this.languageModelsService.configureLanguageModelsProviderGroup(vendorEntry.vendor.vendor, vendorEntry.group.name)
780
}));
781
secondaryActions.push(toAction({
782
id: 'deleteAction',
783
label: localize('models.deleteAction', 'Delete'),
784
class: ThemeIcon.asClassName(Codicon.trash),
785
run: async () => {
786
const result = await this.dialogService.confirm({
787
type: 'info',
788
message: localize('models.deleteConfirmation', "Would you like to delete {0}?", vendorEntry.group.name)
789
});
790
if (!result.confirmed) {
791
return;
792
}
793
await this.languageModelsService.removeLanguageModelsProviderGroup(vendorEntry.vendor.vendor, vendorEntry.group.name);
794
}
795
}));
796
} else if (vendorEntry.vendor.managementCommand) {
797
primaryActions.push(toAction({
798
id: 'manageVendor',
799
label: localize('models.manageProvider', 'Manage {0}...', vendorEntry.group.name),
800
class: ThemeIcon.asClassName(Codicon.gear),
801
run: async () => {
802
await this.commandService.executeCommand(vendorEntry.vendor.managementCommand!, vendorEntry.vendor.vendor);
803
this.viewModel.refresh();
804
}
805
}));
806
}
807
templateData.actionBar.setActions(primaryActions, secondaryActions);
808
}
809
810
override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IActionsColumnTemplateData): void {
811
}
812
813
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IActionsColumnTemplateData): void {
814
}
815
}
816
817
interface IProviderColumnTemplateData extends IModelTableColumnTemplateData {
818
readonly providerElement: HTMLElement;
819
}
820
821
class ProviderColumnRenderer extends ModelsTableColumnRenderer<IProviderColumnTemplateData> {
822
static readonly TEMPLATE_ID = 'provider';
823
824
readonly templateId: string = ProviderColumnRenderer.TEMPLATE_ID;
825
826
renderTemplate(container: HTMLElement): IProviderColumnTemplateData {
827
const disposables = new DisposableStore();
828
const elementDisposables = new DisposableStore();
829
const providerElement = DOM.append(container, $('.model-provider'));
830
return {
831
container,
832
providerElement,
833
disposables,
834
elementDisposables
835
};
836
}
837
838
override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IProviderColumnTemplateData): void {
839
templateData.providerElement.textContent = '';
840
}
841
842
override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IProviderColumnTemplateData): void {
843
templateData.providerElement.textContent = '';
844
}
845
846
override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IProviderColumnTemplateData): void {
847
templateData.providerElement.textContent = entry.model.provider.vendor.displayName;
848
}
849
}
850
851
852
853
function formatTokenCount(count: number): string {
854
if (count >= 1000000) {
855
return `${(count / 1000000).toFixed(1)}M`;
856
} else if (count >= 1000) {
857
return `${(count / 1000).toFixed(0)}K`;
858
}
859
return count.toString();
860
}
861
862
export class ChatModelsWidget extends Disposable {
863
864
private static NUM_INSTANCES: number = 0;
865
866
readonly element: HTMLElement;
867
private searchWidget!: SuggestEnabledInput;
868
private searchActionsContainer!: HTMLElement;
869
private table!: WorkbenchTable<IViewModelEntry>;
870
private tableContainer!: HTMLElement;
871
private addButtonContainer!: HTMLElement;
872
private addButton!: Button;
873
private dropdownActions: IAction[] = [];
874
private viewModel: ChatModelsViewModel;
875
private delayedFiltering: Delayer<void>;
876
877
private readonly searchFocusContextKey: IContextKey<boolean>;
878
879
private tableDisposables = this._register(new DisposableStore());
880
881
constructor(
882
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
883
@IInstantiationService private readonly instantiationService: IInstantiationService,
884
@IExtensionService private readonly extensionService: IExtensionService,
885
@IContextMenuService private readonly contextMenuService: IContextMenuService,
886
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
887
@IEditorProgressService private readonly editorProgressService: IEditorProgressService,
888
@ICommandService private readonly commandService: ICommandService,
889
@IContextKeyService contextKeyService: IContextKeyService,
890
) {
891
super();
892
893
this.searchFocusContextKey = CONTEXT_MODELS_SEARCH_FOCUS.bindTo(contextKeyService);
894
this.delayedFiltering = this._register(new Delayer<void>(200));
895
this.viewModel = this._register(this.instantiationService.createInstance(ChatModelsViewModel));
896
this.element = DOM.$('.models-widget');
897
this.create(this.element);
898
899
const loadingPromise = this.extensionService.whenInstalledExtensionsRegistered().then(() => this.viewModel.refresh());
900
this.editorProgressService.showWhile(loadingPromise, 300);
901
}
902
903
private create(container: HTMLElement): void {
904
const searchAndButtonContainer = DOM.append(container, $('.models-search-and-button-container'));
905
906
const placeholder = localize('Search.FullTextSearchPlaceholder', "Type to search...");
907
const searchContainer = DOM.append(searchAndButtonContainer, $('.models-search-container'));
908
this.searchWidget = this._register(this.instantiationService.createInstance(
909
SuggestEnabledInput,
910
'chatModelsWidget.searchbox',
911
searchContainer,
912
{
913
triggerCharacters: ['@', ':'],
914
provideResults: (query: string) => {
915
const providerSuggestions = this.viewModel.getVendors().map(v => `@provider:"${v.displayName}"`);
916
const allSuggestions = [
917
...providerSuggestions,
918
...SEARCH_SUGGESTIONS.CAPABILITIES,
919
...SEARCH_SUGGESTIONS.VISIBILITY,
920
];
921
if (!query.trim()) {
922
return allSuggestions;
923
}
924
const queryParts = query.split(/\s/g);
925
const lastPart = queryParts[queryParts.length - 1];
926
if (lastPart.startsWith('@provider:')) {
927
return providerSuggestions;
928
} else if (lastPart.startsWith('@capability:')) {
929
return SEARCH_SUGGESTIONS.CAPABILITIES;
930
} else if (lastPart.startsWith('@visible:')) {
931
return SEARCH_SUGGESTIONS.VISIBILITY;
932
} else if (lastPart.startsWith('@')) {
933
return allSuggestions;
934
}
935
return [];
936
}
937
},
938
placeholder,
939
`chatModelsWidget:searchinput:${ChatModelsWidget.NUM_INSTANCES++}`,
940
{
941
placeholderText: placeholder,
942
styleOverrides: {
943
inputBorder: settingsTextInputBorder
944
},
945
focusContextKey: this.searchFocusContextKey,
946
},
947
));
948
949
const filterAction = this._register(new ModelsFilterAction());
950
const clearSearchAction = this._register(new Action(
951
'workbench.models.clearSearch',
952
localize('clearSearch', "Clear Search"),
953
ThemeIcon.asClassName(preferencesClearInputIcon),
954
false,
955
() => this.clearSearch()
956
));
957
const collapseAllAction = this._register(new Action(
958
'workbench.models.collapseAll',
959
localize('collapseAll', "Collapse All"),
960
ThemeIcon.asClassName(Codicon.collapseAll),
961
false,
962
() => {
963
this.viewModel.collapseAll();
964
}
965
));
966
collapseAllAction.enabled = this.viewModel.viewModelEntries.some(e => isLanguageModelGroupEntry(e) || isLanguageModelProviderEntry(e));
967
this._register(this.viewModel.onDidChange(() => collapseAllAction.enabled = this.viewModel.viewModelEntries.some(e => isLanguageModelProviderEntry(e) || isLanguageModelGroupEntry(e))));
968
969
this._register(this.searchWidget.onInputDidChange(() => {
970
clearSearchAction.enabled = !!this.searchWidget.getValue();
971
this.filterModels();
972
}));
973
974
this.searchActionsContainer = DOM.append(searchContainer, $('.models-search-actions'));
975
const actions = [clearSearchAction, collapseAllAction, filterAction];
976
const toolBar = this._register(new ToolBar(this.searchActionsContainer, this.contextMenuService, {
977
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
978
if (action.id === filterAction.id) {
979
return this.instantiationService.createInstance(ModelsSearchFilterDropdownMenuActionViewItem, action, options, {
980
getValue: () => this.searchWidget.getValue(),
981
setValue: (searchValue) => this.search(searchValue)
982
}, this.viewModel);
983
}
984
return undefined;
985
},
986
getKeyBinding: () => undefined
987
}));
988
toolBar.setActions(actions);
989
990
// Add padding to input box for toolbar
991
this.searchWidget.inputWidget.getContainerDomNode().style.paddingRight = `${DOM.getTotalWidth(this.searchActionsContainer) + 12}px`;
992
993
this.addButtonContainer = DOM.append(searchAndButtonContainer, $('.section-title-actions'));
994
const buttonOptions: IButtonOptions = {
995
...defaultButtonStyles,
996
supportIcons: true,
997
};
998
this.addButton = this._register(new Button(this.addButtonContainer, buttonOptions));
999
this.addButton.label = `$(${Codicon.add.id}) ${localize('models.enableModelProvider', 'Add Models...')}`;
1000
this.addButton.element.classList.add('models-add-model-button');
1001
this.updateAddModelsButton();
1002
this._register(this.addButton.onDidClick((e) => {
1003
if (this.dropdownActions.length > 0) {
1004
this.contextMenuService.showContextMenu({
1005
getAnchor: () => this.addButton.element,
1006
getActions: () => this.dropdownActions,
1007
});
1008
}
1009
}));
1010
1011
// Table container
1012
this.tableContainer = DOM.append(container, $('.models-table-container'));
1013
1014
// Create table
1015
this.createTable();
1016
this._register(this.viewModel.onDidChangeGrouping(() => this.createTable()));
1017
this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.updateAddModelsButton()));
1018
this._register(this.languageModelsService.onDidChangeLanguageModelVendors(() => this.updateAddModelsButton()));
1019
}
1020
1021
private createTable(): void {
1022
this.tableDisposables.clear();
1023
DOM.clearNode(this.tableContainer);
1024
1025
const gutterColumnRenderer = this.instantiationService.createInstance(GutterColumnRenderer, this.viewModel);
1026
const modelNameColumnRenderer = this.instantiationService.createInstance(ModelNameColumnRenderer);
1027
const costColumnRenderer = this.instantiationService.createInstance(MultiplierColumnRenderer);
1028
const tokenLimitsColumnRenderer = this.instantiationService.createInstance(TokenLimitsColumnRenderer);
1029
const capabilitiesColumnRenderer = this.instantiationService.createInstance(CapabilitiesColumnRenderer);
1030
const actionsColumnRenderer = this.instantiationService.createInstance(ActionsColumnRenderer, this.viewModel);
1031
const providerColumnRenderer = this.instantiationService.createInstance(ProviderColumnRenderer);
1032
1033
this.tableDisposables.add(capabilitiesColumnRenderer.onDidClickCapability(capability => {
1034
const currentQuery = this.searchWidget.getValue();
1035
const query = `@capability:${capability}`;
1036
const newQuery = toggleFilter(currentQuery, { query });
1037
this.search(newQuery);
1038
}));
1039
1040
const columns = [
1041
{
1042
label: '',
1043
tooltip: '',
1044
weight: 0.05,
1045
minimumWidth: 40,
1046
maximumWidth: 40,
1047
templateId: GutterColumnRenderer.TEMPLATE_ID,
1048
project(row: IViewModelEntry): IViewModelEntry { return row; }
1049
},
1050
{
1051
label: localize('modelName', 'Name'),
1052
tooltip: '',
1053
weight: 0.35,
1054
minimumWidth: 200,
1055
templateId: ModelNameColumnRenderer.TEMPLATE_ID,
1056
project(row: IViewModelEntry): IViewModelEntry { return row; }
1057
}
1058
];
1059
1060
if (this.viewModel.groupBy === ChatModelGroup.Visibility) {
1061
columns.push({
1062
label: localize('provider', 'Provider'),
1063
tooltip: '',
1064
weight: 0.15,
1065
minimumWidth: 100,
1066
templateId: ProviderColumnRenderer.TEMPLATE_ID,
1067
project(row: IViewModelEntry): IViewModelEntry { return row; }
1068
});
1069
}
1070
1071
columns.push(
1072
{
1073
label: localize('tokenLimits', 'Context Size'),
1074
tooltip: '',
1075
weight: 0.1,
1076
minimumWidth: 140,
1077
templateId: TokenLimitsColumnRenderer.TEMPLATE_ID,
1078
project(row: IViewModelEntry): IViewModelEntry { return row; }
1079
},
1080
{
1081
label: localize('capabilities', 'Capabilities'),
1082
tooltip: '',
1083
weight: 0.2,
1084
minimumWidth: 180,
1085
templateId: CapabilitiesColumnRenderer.TEMPLATE_ID,
1086
project(row: IViewModelEntry): IViewModelEntry { return row; }
1087
},
1088
{
1089
label: localize('cost', 'Request Multiplier'),
1090
tooltip: '',
1091
weight: 0.1,
1092
minimumWidth: 60,
1093
templateId: MultiplierColumnRenderer.TEMPLATE_ID,
1094
project(row: IViewModelEntry): IViewModelEntry { return row; }
1095
},
1096
{
1097
label: '',
1098
tooltip: '',
1099
weight: 0.05,
1100
minimumWidth: 64,
1101
maximumWidth: 64,
1102
templateId: ActionsColumnRenderer.TEMPLATE_ID,
1103
project(row: IViewModelEntry): IViewModelEntry { return row; }
1104
}
1105
);
1106
1107
this.table = this.tableDisposables.add(this.instantiationService.createInstance(
1108
WorkbenchTable,
1109
'ModelsWidget',
1110
this.tableContainer,
1111
new Delegate(),
1112
columns,
1113
[
1114
gutterColumnRenderer,
1115
modelNameColumnRenderer,
1116
costColumnRenderer,
1117
tokenLimitsColumnRenderer,
1118
capabilitiesColumnRenderer,
1119
actionsColumnRenderer,
1120
providerColumnRenderer
1121
],
1122
{
1123
identityProvider: { getId: (e: IViewModelEntry) => e.id },
1124
horizontalScrolling: false,
1125
accessibilityProvider: {
1126
getAriaLabel: (e: IViewModelEntry) => {
1127
if (isLanguageModelProviderEntry(e)) {
1128
return localize('vendor.ariaLabel', '{0} Models', e.vendorEntry.group.name);
1129
} else if (isLanguageModelGroupEntry(e)) {
1130
return e.id === 'visible' ? localize('visible.ariaLabel', 'Visible Models') : localize('hidden.ariaLabel', 'Hidden Models');
1131
} else if (isStatusEntry(e)) {
1132
return localize('status.ariaLabel', 'Status: {0}', e.message);
1133
}
1134
const ariaLabels = [];
1135
ariaLabels.push(localize('model.name', '{0} from {1}', e.model.metadata.name, e.model.provider.vendor.displayName));
1136
if (e.model.metadata.maxInputTokens && e.model.metadata.maxOutputTokens) {
1137
ariaLabels.push(localize('model.contextSize', 'Context size: {0} input tokens and {1} output tokens', formatTokenCount(e.model.metadata.maxInputTokens), formatTokenCount(e.model.metadata.maxOutputTokens)));
1138
}
1139
if (e.model.metadata.capabilities) {
1140
ariaLabels.push(localize('model.capabilities', 'Capabilities: {0}', Object.keys(e.model.metadata.capabilities).join(', ')));
1141
}
1142
const multiplierText = e.model.metadata.multiplier ?? '-';
1143
if (multiplierText !== '-') {
1144
ariaLabels.push(localize('multiplier.tooltip', "Every chat message counts {0} towards your premium model request quota", multiplierText));
1145
}
1146
if (e.model.visible) {
1147
ariaLabels.push(localize('model.visible', 'This model is visible in the chat model picker'));
1148
} else {
1149
ariaLabels.push(localize('model.hidden', 'This model is hidden in the chat model picker'));
1150
}
1151
return ariaLabels.join('. ');
1152
},
1153
getWidgetAriaLabel: () => localize('modelsTable.ariaLabel', 'Language Models')
1154
},
1155
multipleSelectionSupport: true,
1156
setRowLineHeight: false,
1157
openOnSingleClick: true,
1158
alwaysConsumeMouseWheel: false,
1159
}
1160
)) as WorkbenchTable<IViewModelEntry>;
1161
1162
this.tableDisposables.add(this.table.onContextMenu(e => {
1163
if (!e.element) {
1164
return;
1165
}
1166
1167
const selection = this.table.getSelection();
1168
const selectedEntries = selection.every(i => i !== e.index) ? [e.element] : selection.map(i => this.viewModel.viewModelEntries[i]).filter(e => !!e);
1169
1170
// Get model entries from selection (filter out vendor/group/status entries)
1171
const selectedModelEntries = selectedEntries.filter((entry): entry is ILanguageModelEntry =>
1172
!isLanguageModelProviderEntry(entry) && !isLanguageModelGroupEntry(entry) && !isStatusEntry(entry)
1173
);
1174
1175
const actions: IAction[] = [];
1176
let configureGroup: string | undefined;
1177
let configureVendor: ILanguageModelProviderDescriptor | undefined;
1178
1179
if (selectedModelEntries.length) {
1180
const visibleModels = selectedModelEntries.filter(entry => entry.model.visible);
1181
const hiddenModels = selectedModelEntries.filter(entry => !entry.model.visible);
1182
1183
actions.push(toAction({
1184
id: 'hideSelectedModels',
1185
label: localize('models.hideSelected', 'Hide in the Chat Model Picker'),
1186
enabled: visibleModels.length > 0,
1187
run: () => this.viewModel.setModelsVisibility(selectedModelEntries, false)
1188
}));
1189
1190
actions.push(toAction({
1191
id: 'showSelectedModels',
1192
label: localize('models.showSelected', 'Show in the Chat Model Picker'),
1193
enabled: hiddenModels.length > 0,
1194
run: () => this.viewModel.setModelsVisibility(selectedModelEntries, true)
1195
}));
1196
1197
// Show configure action if all models are from the same group
1198
configureGroup = selectedModelEntries[0].model.provider.group.name;
1199
configureVendor = selectedModelEntries[0].model.provider.vendor;
1200
if (selectedModelEntries.some(entry => entry.model.provider.vendor.isDefault || entry.model.provider.group.name !== configureGroup)) {
1201
configureGroup = undefined;
1202
configureVendor = undefined;
1203
}
1204
} else if (selectedEntries.length === 1) {
1205
const entry = e.element;
1206
if (isLanguageModelProviderEntry(entry)) {
1207
if (!entry.vendorEntry.vendor.isDefault) {
1208
actions.push(toAction({
1209
id: 'hideAllModels',
1210
label: localize('models.hideAll', 'Hide in the Chat Model Picker'),
1211
run: () => this.viewModel.setGroupVisibility(entry, false)
1212
}));
1213
actions.push(toAction({
1214
id: 'showAllModels',
1215
label: localize('models.showAll', 'Show in the Chat Model Picker'),
1216
run: () => this.viewModel.setGroupVisibility(entry, true)
1217
}));
1218
}
1219
configureGroup = entry.vendorEntry.group.name;
1220
configureVendor = entry.vendorEntry.vendor;
1221
}
1222
}
1223
1224
if (configureGroup && configureVendor) {
1225
if (configureVendor.managementCommand || configureVendor.configuration) {
1226
if (actions.length) {
1227
actions.push(new Separator());
1228
}
1229
if (configureVendor.managementCommand) {
1230
actions.push(toAction({
1231
id: 'configureVendor',
1232
label: localize('models.configureContextMenu', 'Configure'),
1233
run: async () => {
1234
await this.commandService.executeCommand(configureVendor.managementCommand!, configureVendor.vendor);
1235
await this.viewModel.refresh();
1236
}
1237
}));
1238
} else {
1239
actions.push(toAction({
1240
id: 'configureVendor',
1241
label: localize('models.configureContextMenu', 'Configure'),
1242
run: () => this.languageModelsService.configureLanguageModelsProviderGroup(configureVendor.vendor, configureGroup!)
1243
}));
1244
}
1245
}
1246
}
1247
1248
if (actions.length > 0) {
1249
this.contextMenuService.showContextMenu({
1250
getAnchor: () => e.anchor,
1251
getActions: () => actions
1252
});
1253
}
1254
}));
1255
1256
this.table.splice(0, this.table.length, this.viewModel.viewModelEntries);
1257
this.tableDisposables.add(this.viewModel.onDidChange(({ at, removed, added }) => {
1258
this.table.splice(at, removed, added);
1259
if (this.viewModel.selectedEntry) {
1260
const selectedEntryIndex = this.viewModel.viewModelEntries.indexOf(this.viewModel.selectedEntry);
1261
this.table.setFocus([selectedEntryIndex]);
1262
this.table.setSelection([selectedEntryIndex]);
1263
}
1264
}));
1265
1266
this.tableDisposables.add(this.table.onDidOpen(async ({ element, browserEvent }) => {
1267
if (!element) {
1268
return;
1269
}
1270
if (isStatusEntry(element)) {
1271
return;
1272
}
1273
if (isLanguageModelProviderEntry(element) || isLanguageModelGroupEntry(element)) {
1274
this.viewModel.toggleCollapsed(element);
1275
} else if (!DOM.isMouseEvent(browserEvent) || browserEvent.detail === 2) {
1276
this.viewModel.toggleVisibility(element);
1277
}
1278
}));
1279
1280
this.tableDisposables.add(this.table.onDidChangeSelection(e => this.viewModel.selectedEntry = e.elements[0]));
1281
1282
this.tableDisposables.add(this.table.onDidBlur(() => {
1283
if (this.viewModel.shouldRefilter()) {
1284
this.viewModel.filter(this.searchWidget.getValue());
1285
}
1286
}));
1287
1288
this.layout(this.element.clientHeight, this.element.clientWidth);
1289
}
1290
1291
private updateAddModelsButton(): void {
1292
const configurableVendors = this.languageModelsService.getVendors().filter(vendor => vendor.managementCommand || vendor.configuration);
1293
1294
const entitlement = this.chatEntitlementService.entitlement;
1295
const isManagedEntitlement = entitlement === ChatEntitlement.Business || entitlement === ChatEntitlement.Enterprise;
1296
const supportsAddingModels = this.chatEntitlementService.isInternal
1297
|| (entitlement !== ChatEntitlement.Unknown
1298
&& entitlement !== ChatEntitlement.Available
1299
&& !isManagedEntitlement);
1300
1301
this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0;
1302
this.addButton.setTitle(!supportsAddingModels && isManagedEntitlement ? localize('models.managedByOrganization', "Adding models is managed by your organization") : '');
1303
1304
this.dropdownActions = configurableVendors.map(vendor => toAction({
1305
id: `enable-${vendor.vendor}`,
1306
label: vendor.displayName,
1307
run: async () => {
1308
await this.addModelsForVendor(vendor);
1309
}
1310
}));
1311
}
1312
1313
private filterModels(): void {
1314
this.delayedFiltering.trigger(() => {
1315
this.viewModel.filter(this.searchWidget.getValue());
1316
});
1317
}
1318
1319
private async addModelsForVendor(vendor: ILanguageModelProviderDescriptor): Promise<void> {
1320
this.languageModelsService.configureLanguageModelsProviderGroup(vendor.vendor);
1321
}
1322
1323
public layout(height: number, width: number): void {
1324
width = width - 24;
1325
this.searchWidget.layout(new DOM.Dimension(width - this.searchActionsContainer.clientWidth - this.addButtonContainer.clientWidth - 8, 22));
1326
const tableHeight = height - 40;
1327
this.tableContainer.style.height = `${tableHeight}px`;
1328
this.table.layout(tableHeight, width);
1329
}
1330
1331
public focusSearch(): void {
1332
this.searchWidget.focus();
1333
}
1334
1335
public search(filter: string): void {
1336
this.focusSearch();
1337
this.searchWidget.setValue(filter);
1338
this.viewModel.filter(filter);
1339
}
1340
1341
public clearSearch(): void {
1342
this.focusSearch();
1343
this.searchWidget.setValue('');
1344
}
1345
1346
public render(): void {
1347
if (this.viewModel.shouldRefilter()) {
1348
this.viewModel.filter(this.searchWidget.getValue());
1349
}
1350
}
1351
1352
}
1353
1354