Path: blob/main/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts
5251 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/chatModelsWidget.css';6import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';7import { Emitter } from '../../../../../base/common/event.js';8import * as DOM from '../../../../../base/browser/dom.js';9import { Button, IButtonOptions } from '../../../../../base/browser/ui/button/button.js';10import { ThemeIcon } from '../../../../../base/common/themables.js';11import { ILanguageModelsService, ILanguageModelProviderDescriptor } from '../../../chat/common/languageModels.js';12import { localize } from '../../../../../nls.js';13import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';14import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';15import { WorkbenchTable } from '../../../../../platform/list/browser/listService.js';16import { ITableVirtualDelegate, ITableRenderer } from '../../../../../base/browser/ui/table/table.js';17import { IHoverService } from '../../../../../platform/hover/browser/hover.js';18import { MarkdownString } from '../../../../../base/common/htmlContent.js';19import { IExtensionService } from '../../../../services/extensions/common/extensions.js';20import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';21import { IAction, toAction, Action, Separator, SubmenuAction } from '../../../../../base/common/actions.js';22import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js';23import { Codicon } from '../../../../../base/common/codicons.js';24import { ChatModelsViewModel, ILanguageModel, ILanguageModelEntry, ILanguageModelProviderEntry, ILanguageModelGroupEntry, SEARCH_SUGGESTIONS, isLanguageModelProviderEntry, isLanguageModelGroupEntry, ChatModelGroup, IViewModelEntry, isStatusEntry, IStatusEntry } from './chatModelsViewModel.js';25import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';26import { SuggestEnabledInput } from '../../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';27import { Delayer } from '../../../../../base/common/async.js';28import { settingsTextInputBorder } from '../../../preferences/common/settingsEditorColorRegistry.js';29import { IChatEntitlementService, ChatEntitlement } from '../../../../services/chat/common/chatEntitlementService.js';30import { DropdownMenuActionViewItem } from '../../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';31import { IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';32import { AnchorAlignment } from '../../../../../base/browser/ui/contextview/contextview.js';33import { ToolBar } from '../../../../../base/browser/ui/toolbar/toolbar.js';34import { preferencesClearInputIcon } from '../../../preferences/browser/preferencesIcons.js';35import { ICommandService } from '../../../../../platform/commands/common/commands.js';36import { IEditorProgressService } from '../../../../../platform/progress/common/progress.js';37import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';38import { CONTEXT_MODELS_SEARCH_FOCUS } from '../../common/constants.js';39import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';40import Severity from '../../../../../base/common/severity.js';4142const $ = DOM.$;4344const HEADER_HEIGHT = 30;45const VENDOR_ROW_HEIGHT = 30;46const MODEL_ROW_HEIGHT = 26;4748export function getModelHoverContent(model: ILanguageModel): MarkdownString {49const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });50markdown.appendMarkdown(`**${model.metadata.name}**`);51if (model.metadata.id !== model.metadata.version) {52markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${model.metadata.id}@${model.metadata.version}_ </span>`);53} else {54markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${model.metadata.id}_ </span>`);55}56markdown.appendText(`\n`);5758if (model.metadata.statusIcon && model.metadata.tooltip) {59if (model.metadata.statusIcon) {60markdown.appendMarkdown(`$(${model.metadata.statusIcon.id}) `);61}62markdown.appendMarkdown(`${model.metadata.tooltip}`);63markdown.appendText(`\n`);64}6566if (model.metadata.multiplier) {67markdown.appendMarkdown(`${localize('models.cost', 'Multiplier')}: `);68markdown.appendMarkdown(model.metadata.multiplier);69markdown.appendText(`\n`);70}7172if (model.metadata.maxInputTokens || model.metadata.maxOutputTokens) {73markdown.appendMarkdown(`${localize('models.contextSize', 'Context Size')}: `);74let addSeparator = false;75if (model.metadata.maxInputTokens) {76markdown.appendMarkdown(`$(arrow-down) ${formatTokenCount(model.metadata.maxInputTokens)} (${localize('models.input', 'Input')})`);77addSeparator = true;78}79if (model.metadata.maxOutputTokens) {80if (addSeparator) {81markdown.appendText(` | `);82}83markdown.appendMarkdown(`$(arrow-up) ${formatTokenCount(model.metadata.maxOutputTokens)} (${localize('models.output', 'Output')})`);84}85markdown.appendText(`\n`);86}8788if (model.metadata.capabilities) {89markdown.appendMarkdown(`${localize('models.capabilities', 'Capabilities')}: `);90if (model.metadata.capabilities?.toolCalling) {91markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${localize('models.toolCalling', 'Tools')}_ </span>`);92}93if (model.metadata.capabilities?.vision) {94markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${localize('models.vision', 'Vision')}_ </span>`);95}96if (model.metadata.capabilities?.agentMode) {97markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${localize('models.agentMode', 'Agent Mode')}_ </span>`);98}99for (const editTool of model.metadata.capabilities.editTools ?? []) {100markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${editTool}_ </span>`);101}102markdown.appendText(`\n`);103}104105return markdown;106}107108class ModelsFilterAction extends Action {109constructor() {110super('workbench.models.filter', localize('filter', "Filter"), ThemeIcon.asClassName(Codicon.filter));111}112override async run(): Promise<void> {113}114}115116interface IFilterQuery {117/** The primary filter query string */118query: string;119/** Alternative query strings that are treated as synonyms of the primary query */120synonyms?: string[];121/** Query strings that should be removed when adding this filter (mutually exclusive filters) */122excludes?: string[];123}124125function toggleFilter(currentQuery: string, filter: IFilterQuery): string {126const { query, synonyms = [], excludes = [] } = filter;127const allSynonyms = [query, ...synonyms];128const isChecked = allSynonyms.some(q => currentQuery.includes(q));129const hasExcludedQuery = excludes.some(q => currentQuery.includes(q));130131if (isChecked) {132// Query or synonym is already set, remove all of them (toggle off)133let queryWithRemovedFilter = currentQuery;134for (const q of allSynonyms) {135queryWithRemovedFilter = queryWithRemovedFilter.replace(q, '');136}137return queryWithRemovedFilter.replace(/\s+/g, ' ').trim();138} else if (hasExcludedQuery) {139// An excluded query is set, replace it with the new query140let newQuery = currentQuery;141for (const q of excludes) {142newQuery = newQuery.replace(q, '');143}144newQuery = newQuery.replace(/\s+/g, ' ').trim();145return newQuery ? `${newQuery} ${query}` : query;146} else {147// No filter is set, add the new query148const trimmedQuery = currentQuery.trim();149return trimmedQuery ? `${trimmedQuery} ${query}` : query;150}151}152153class ModelsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem {154155constructor(156action: IAction,157options: IActionViewItemOptions,158private readonly search: {159getValue(): string;160setValue(newValue: string): void;161},162private readonly viewModel: ChatModelsViewModel,163@IContextMenuService contextMenuService: IContextMenuService164) {165super(action,166{ getActions: () => this.getActions() },167contextMenuService,168{169...options,170classNames: action.class,171anchorAlignmentProvider: () => AnchorAlignment.RIGHT,172menuAsChild: true173}174);175}176177private createGroupByAction(grouping: ChatModelGroup, label: string): IAction {178return {179id: `groupBy.${grouping}`,180label,181class: undefined,182enabled: true,183tooltip: localize('groupByTooltip', "Group by {0}", label),184checked: this.viewModel.groupBy === grouping,185run: () => {186this.viewModel.groupBy = grouping;187}188};189}190191private createProviderAction(vendor: string, displayName: string): IAction {192const query = `@provider:"${displayName}"`;193const currentQuery = this.search.getValue();194const isChecked = currentQuery.includes(query) || currentQuery.includes(`@provider:${vendor}`);195196return {197id: `provider-${vendor}`,198label: displayName,199tooltip: localize('filterByProvider', "Filter by {0}", displayName),200class: undefined,201enabled: true,202checked: isChecked,203run: () => this.toggleFilterAndSearch({ query, synonyms: [`@provider:${vendor}`] })204};205}206207private createCapabilityAction(capability: string, label: string): IAction {208const query = `@capability:${capability}`;209const currentQuery = this.search.getValue();210const isChecked = currentQuery.includes(query);211212return {213id: `capability-${capability}`,214label,215tooltip: localize('filterByCapability', "Filter by {0}", label),216class: undefined,217enabled: true,218checked: isChecked,219run: () => this.toggleFilterAndSearch({ query })220};221}222223private createVisibleAction(visible: boolean, label: string): IAction {224const query = `@visible:${visible}`;225const currentQuery = this.search.getValue();226const isChecked = currentQuery.includes(query);227228return {229id: `visible-${visible}`,230label,231tooltip: localize('filterByVisible', "Filter by {0}", label),232class: undefined,233enabled: true,234checked: isChecked,235run: () => this.toggleFilterAndSearch({ query, excludes: [`@visible:${!visible}`] })236};237}238239private toggleFilterAndSearch(filter: IFilterQuery): void {240const currentQuery = this.search.getValue();241const newQuery = toggleFilter(currentQuery, filter);242this.search.setValue(newQuery);243}244245private getActions(): IAction[] {246const actions: IAction[] = [];247248// Capability filters249actions.push(250this.createCapabilityAction('tools', localize('capability.tools', "Tools")),251this.createCapabilityAction('vision', localize('capability.vision', "Vision")),252this.createCapabilityAction('agent', localize('capability.agent', "Agent Mode"))253);254255// Visibility filters256actions.push(new Separator());257actions.push(this.createVisibleAction(true, localize('filter.visible', "Visible in Chat Model Picker")));258actions.push(this.createVisibleAction(false, localize('filter.hidden', "Hidden in Chat Model Picker")));259260// Provider filters - only show providers with configured models261const configuredVendors = this.viewModel.getConfiguredVendors();262if (configuredVendors.length > 1) {263actions.push(new Separator());264actions.push(...configuredVendors.map(vendor => this.createProviderAction(vendor.vendor.vendor, vendor.group.name)));265}266267// Group By268actions.push(new Separator());269const groupByActions: IAction[] = [];270groupByActions.push(this.createGroupByAction(ChatModelGroup.Vendor, localize('groupBy.provider', "Provider")));271groupByActions.push(this.createGroupByAction(ChatModelGroup.Visibility, localize('groupBy.visibility', "Visibility (Chat Model Picker)")));272actions.push(new SubmenuAction('groupBy', localize('groupBy', "Group By"), groupByActions));273274return actions;275}276}277278class Delegate implements ITableVirtualDelegate<IViewModelEntry> {279readonly headerRowHeight = HEADER_HEIGHT;280getHeight(element: IViewModelEntry): number {281return isLanguageModelProviderEntry(element) || isLanguageModelGroupEntry(element) ? VENDOR_ROW_HEIGHT : MODEL_ROW_HEIGHT;282}283}284285interface IModelTableColumnTemplateData {286readonly container: HTMLElement;287readonly disposables: DisposableStore;288readonly elementDisposables: DisposableStore;289}290291abstract class ModelsTableColumnRenderer<T extends IModelTableColumnTemplateData> implements ITableRenderer<IViewModelEntry, T> {292abstract readonly templateId: string;293abstract renderTemplate(container: HTMLElement): T;294295renderElement(element: IViewModelEntry, index: number, templateData: T): void {296templateData.elementDisposables.clear();297const isVendor = isLanguageModelProviderEntry(element);298const isGroup = isLanguageModelGroupEntry(element);299const isStatus = isStatusEntry(element);300templateData.container.classList.add('models-table-column');301templateData.container.parentElement!.classList.toggle('models-vendor-row', isVendor || isGroup);302templateData.container.parentElement!.classList.toggle('models-model-row', !isVendor && !isGroup);303templateData.container.parentElement!.classList.toggle('models-status-row', isStatus);304templateData.container.parentElement!.classList.toggle('model-hidden', !isVendor && !isGroup && !isStatus && !element.model.visible);305if (isVendor) {306this.renderVendorElement(element, index, templateData);307} else if (isGroup) {308this.renderGroupElement(element, index, templateData);309} else if (isStatus) {310this.renderStatusElement(element, index, templateData);311} else {312this.renderModelElement(element, index, templateData);313}314}315316abstract renderVendorElement(element: ILanguageModelProviderEntry, index: number, templateData: T): void;317abstract renderGroupElement(element: ILanguageModelGroupEntry, index: number, templateData: T): void;318abstract renderModelElement(element: ILanguageModelEntry, index: number, templateData: T): void;319320protected renderStatusElement(element: IStatusEntry, index: number, templateData: T): void { }321322disposeTemplate(templateData: T): void {323templateData.elementDisposables.dispose();324templateData.disposables.dispose();325}326}327328interface IToggleCollapseColumnTemplateData extends IModelTableColumnTemplateData {329readonly listRowElement: HTMLElement | null;330readonly container: HTMLElement;331readonly actionBar: ActionBar;332}333334class GutterColumnRenderer extends ModelsTableColumnRenderer<IToggleCollapseColumnTemplateData> {335336static readonly TEMPLATE_ID = 'gutter';337338readonly templateId: string = GutterColumnRenderer.TEMPLATE_ID;339340constructor(341private readonly viewModel: ChatModelsViewModel,342) {343super();344}345346renderTemplate(container: HTMLElement): IToggleCollapseColumnTemplateData {347const disposables = new DisposableStore();348const elementDisposables = new DisposableStore();349container.classList.add('models-gutter-column');350const actionBar = disposables.add(new ActionBar(container));351return {352listRowElement: container.parentElement?.parentElement ?? null,353container,354actionBar,355disposables,356elementDisposables357};358}359360override renderElement(entry: IViewModelEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {361templateData.actionBar.clear();362super.renderElement(entry, index, templateData);363}364365override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {366this.renderCollapsableElement(entry, templateData);367}368369override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {370this.renderCollapsableElement(entry, templateData);371}372373private renderCollapsableElement(entry: ILanguageModelProviderEntry | ILanguageModelGroupEntry, templateData: IToggleCollapseColumnTemplateData): void {374if (templateData.listRowElement) {375templateData.listRowElement.setAttribute('aria-expanded', entry.collapsed ? 'false' : 'true');376}377378const label = entry.collapsed ? localize('expand', 'Expand') : localize('collapse', 'Collapse');379const toggleCollapseAction = {380id: 'toggleCollapse',381label,382tooltip: label,383enabled: true,384class: ThemeIcon.asClassName(entry.collapsed ? Codicon.chevronRight : Codicon.chevronDown),385run: () => this.viewModel.toggleCollapsed(entry)386};387templateData.actionBar.push(toggleCollapseAction, { icon: true, label: false });388}389390override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IToggleCollapseColumnTemplateData): void {391const { model: modelEntry } = entry;392const isVisible = modelEntry.visible;393const toggleVisibilityAction = toAction({394id: 'toggleVisibility',395label: isVisible ? localize('models.hide', 'Hide') : localize('models.show', 'Show'),396class: `model-visibility-toggle ${isVisible ? `${ThemeIcon.asClassName(Codicon.eye)} model-visible` : `${ThemeIcon.asClassName(Codicon.eyeClosed)} model-hidden`}`,397tooltip: isVisible ? localize('models.visible', 'Hide in the chat model picker') : localize('models.hidden', 'Show in the chat model picker'),398checked: !isVisible,399run: async () => this.viewModel.toggleVisibility(entry)400});401templateData.actionBar.push(toggleVisibilityAction, { icon: true, label: false });402}403}404405interface IModelNameColumnTemplateData extends IModelTableColumnTemplateData {406readonly statusIcon: HTMLElement;407readonly nameLabel: HighlightedLabel;408readonly modelStatusIcon: HTMLElement;409readonly actionBar: ActionBar;410}411412class ModelNameColumnRenderer extends ModelsTableColumnRenderer<IModelNameColumnTemplateData> {413static readonly TEMPLATE_ID = 'modelName';414415readonly templateId: string = ModelNameColumnRenderer.TEMPLATE_ID;416417constructor(418@IHoverService private readonly hoverService: IHoverService419) {420super();421}422423renderTemplate(container: HTMLElement): IModelNameColumnTemplateData {424const disposables = new DisposableStore();425const elementDisposables = new DisposableStore();426const nameContainer = DOM.append(container, $('.model-name-container'));427const statusIcon = DOM.append(nameContainer, $('.status-icon'));428const nameLabel = disposables.add(new HighlightedLabel(DOM.append(nameContainer, $('.model-name'))));429const modelStatusIcon = DOM.append(nameContainer, $('.model-status-icon'));430const actionBar = disposables.add(new ActionBar(DOM.append(nameContainer, $('.model-name-actions'))));431return {432container,433statusIcon,434nameLabel,435modelStatusIcon,436actionBar,437disposables,438elementDisposables439};440}441442override renderElement(entry: IViewModelEntry, index: number, templateData: IModelNameColumnTemplateData): void {443DOM.clearNode(templateData.modelStatusIcon);444templateData.actionBar.clear();445templateData.nameLabel.element.classList.remove('error-status', 'warning-status', 'info-status');446super.renderElement(entry, index, templateData);447}448449override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IModelNameColumnTemplateData): void {450templateData.nameLabel.set(entry.vendorEntry.group.name, undefined);451}452453override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IModelNameColumnTemplateData): void {454templateData.nameLabel.set(entry.label, undefined);455}456457override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IModelNameColumnTemplateData): void {458const { model: modelEntry, modelNameMatches } = entry;459460templateData.statusIcon.style.display = 'none';461templateData.modelStatusIcon.className = 'model-status-icon';462if (modelEntry.metadata.statusIcon) {463templateData.modelStatusIcon.classList.add(...ThemeIcon.asClassNameArray(modelEntry.metadata.statusIcon));464templateData.modelStatusIcon.style.display = '';465} else {466templateData.modelStatusIcon.style.display = 'none';467}468469templateData.nameLabel.set(modelEntry.metadata.name, modelNameMatches);470471const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });472markdown.appendMarkdown(`**${entry.model.metadata.name}**`);473if (entry.model.metadata.id !== entry.model.metadata.version) {474markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${entry.model.metadata.id}@${entry.model.metadata.version}_ </span>`);475} else {476markdown.appendMarkdown(` <span style="background-color:#8080802B;"> _${entry.model.metadata.id}_ </span>`);477}478markdown.appendText(`\n`);479480if (entry.model.metadata.statusIcon && entry.model.metadata.tooltip) {481if (entry.model.metadata.statusIcon) {482markdown.appendMarkdown(`$(${entry.model.metadata.statusIcon.id}) `);483}484markdown.appendMarkdown(`${entry.model.metadata.tooltip}`);485markdown.appendText(`\n`);486}487488if (!entry.model.visible) {489markdown.appendMarkdown(`\n\n${localize('models.userSelectable', 'This model is hidden in the chat model picker')}`);490}491492templateData.elementDisposables.add(this.hoverService.setupDelayedHoverAtMouse(templateData.container!, () => ({493content: markdown,494appearance: {495compact: true,496skipFadeInAnimation: true,497}498})));499}500501protected override renderStatusElement(entry: IStatusEntry, index: number, templateData: IModelNameColumnTemplateData): void {502templateData.statusIcon.style.display = '';503templateData.statusIcon.className = 'status-icon';504switch (entry.severity) {505case Severity.Error:506templateData.nameLabel.element.classList.add('error-status');507templateData.statusIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.error));508break;509case Severity.Warning:510templateData.nameLabel.element.classList.add('warning-status');511templateData.statusIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning));512break;513case Severity.Info:514templateData.nameLabel.element.classList.add('info-status');515templateData.statusIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info));516break;517}518templateData.nameLabel.set(entry.message, undefined, entry.message);519}520}521522interface IMultiplierColumnTemplateData extends IModelTableColumnTemplateData {523readonly multiplierElement: HTMLElement;524}525526class MultiplierColumnRenderer extends ModelsTableColumnRenderer<IMultiplierColumnTemplateData> {527static readonly TEMPLATE_ID = 'multiplier';528529readonly templateId: string = MultiplierColumnRenderer.TEMPLATE_ID;530531constructor(532@IHoverService private readonly hoverService: IHoverService533) {534super();535}536537renderTemplate(container: HTMLElement): IMultiplierColumnTemplateData {538const disposables = new DisposableStore();539const elementDisposables = new DisposableStore();540const multiplierElement = DOM.append(container, $('.model-multiplier'));541return {542container,543multiplierElement,544disposables,545elementDisposables546};547}548549override renderElement(entry: IViewModelEntry, index: number, templateData: IMultiplierColumnTemplateData): void {550templateData.multiplierElement.textContent = '';551super.renderElement(entry, index, templateData);552}553554override renderGroupElement(element: ILanguageModelGroupEntry, index: number, templateData: IMultiplierColumnTemplateData): void {555}556557override renderVendorElement(element: ILanguageModelProviderEntry, index: number, templateData: IMultiplierColumnTemplateData): void {558559}560561override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IMultiplierColumnTemplateData): void {562const multiplierText = entry.model.metadata.multiplier ?? '-';563templateData.multiplierElement.textContent = multiplierText;564565if (multiplierText !== '-') {566templateData.elementDisposables.add(this.hoverService.setupDelayedHoverAtMouse(templateData.container, () => ({567content: localize('multiplier.tooltip', "Every chat message counts {0} towards your premium model request quota", multiplierText),568appearance: {569compact: true,570skipFadeInAnimation: true571}572})));573}574}575}576577interface ITokenLimitsColumnTemplateData extends IModelTableColumnTemplateData {578readonly tokenLimitsElement: HTMLElement;579}580581class TokenLimitsColumnRenderer extends ModelsTableColumnRenderer<ITokenLimitsColumnTemplateData> {582static readonly TEMPLATE_ID = 'tokenLimits';583584readonly templateId: string = TokenLimitsColumnRenderer.TEMPLATE_ID;585586constructor(587@IHoverService private readonly hoverService: IHoverService588) {589super();590}591592renderTemplate(container: HTMLElement): ITokenLimitsColumnTemplateData {593const disposables = new DisposableStore();594const elementDisposables = new DisposableStore();595const tokenLimitsElement = DOM.append(container, $('.model-token-limits'));596return {597container,598tokenLimitsElement,599disposables,600elementDisposables601};602}603604override renderElement(entry: IViewModelEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {605DOM.clearNode(templateData.tokenLimitsElement);606super.renderElement(entry, index, templateData);607}608609override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {610}611612override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {613}614615override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: ITokenLimitsColumnTemplateData): void {616const { model: modelEntry } = entry;617const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });618if (modelEntry.metadata.maxInputTokens || modelEntry.metadata.maxOutputTokens) {619let addSeparator = false;620markdown.appendMarkdown(`${localize('models.contextSize', 'Context Size')}: `);621if (modelEntry.metadata.maxInputTokens) {622const inputDiv = DOM.append(templateData.tokenLimitsElement, $('.token-limit-item'));623DOM.append(inputDiv, $('span.codicon.codicon-arrow-down'));624const inputText = DOM.append(inputDiv, $('span'));625inputText.textContent = formatTokenCount(modelEntry.metadata.maxInputTokens);626627markdown.appendMarkdown(`$(arrow-down) ${modelEntry.metadata.maxInputTokens} (${localize('models.input', 'Input')})`);628addSeparator = true;629}630if (modelEntry.metadata.maxOutputTokens) {631const outputDiv = DOM.append(templateData.tokenLimitsElement, $('.token-limit-item'));632DOM.append(outputDiv, $('span.codicon.codicon-arrow-up'));633const outputText = DOM.append(outputDiv, $('span'));634outputText.textContent = formatTokenCount(modelEntry.metadata.maxOutputTokens);635if (addSeparator) {636markdown.appendText(` | `);637}638markdown.appendMarkdown(`$(arrow-up) ${modelEntry.metadata.maxOutputTokens} (${localize('models.output', 'Output')})`);639}640}641642templateData.elementDisposables.add(this.hoverService.setupDelayedHoverAtMouse(templateData.container, () => ({643content: markdown,644appearance: {645compact: true,646skipFadeInAnimation: true,647}648})));649}650}651652interface ICapabilitiesColumnTemplateData extends IModelTableColumnTemplateData {653readonly metadataRow: HTMLElement;654}655656class CapabilitiesColumnRenderer extends ModelsTableColumnRenderer<ICapabilitiesColumnTemplateData> {657static readonly TEMPLATE_ID = 'capabilities';658659readonly templateId: string = CapabilitiesColumnRenderer.TEMPLATE_ID;660661private readonly _onDidClickCapability = new Emitter<string>();662readonly onDidClickCapability = this._onDidClickCapability.event;663664renderTemplate(container: HTMLElement): ICapabilitiesColumnTemplateData {665const disposables = new DisposableStore();666const elementDisposables = new DisposableStore();667container.classList.add('model-capability-column');668const metadataRow = DOM.append(container, $('.model-capabilities'));669return {670container,671metadataRow,672disposables,673elementDisposables674};675}676677override renderElement(entry: IViewModelEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {678DOM.clearNode(templateData.metadataRow);679super.renderElement(entry, index, templateData);680}681682override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {683}684685override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {686}687688override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: ICapabilitiesColumnTemplateData): void {689const { model: modelEntry, capabilityMatches } = entry;690691if (modelEntry.metadata.capabilities?.toolCalling) {692templateData.elementDisposables.add(this.createCapabilityButton(693templateData.metadataRow,694capabilityMatches?.includes('toolCalling') || false,695localize('models.tools', 'Tools'),696'tools'697));698}699700if (modelEntry.metadata.capabilities?.vision) {701templateData.elementDisposables.add(this.createCapabilityButton(702templateData.metadataRow,703capabilityMatches?.includes('vision') || false,704localize('models.vision', 'Vision'),705'vision'706));707}708}709710private createCapabilityButton(container: HTMLElement, isActive: boolean, label: string, capability: string): IDisposable {711const disposables = new DisposableStore();712const buttonContainer = DOM.append(container, $('.model-badge-container'));713const button = disposables.add(new Button(buttonContainer, { secondary: true }));714button.element.classList.add('model-capability');715button.element.classList.toggle('active', isActive);716button.label = label;717disposables.add(button.onDidClick(() => this._onDidClickCapability.fire(capability)));718return disposables;719}720}721722interface IActionsColumnTemplateData extends IModelTableColumnTemplateData {723readonly actionBar: ToolBar;724}725726class ActionsColumnRenderer extends ModelsTableColumnRenderer<IActionsColumnTemplateData> {727static readonly TEMPLATE_ID = 'actions';728729readonly templateId: string = ActionsColumnRenderer.TEMPLATE_ID;730731constructor(732private readonly viewModel: ChatModelsViewModel,733@IInstantiationService private readonly instantiationService: IInstantiationService,734@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,735@IDialogService private readonly dialogService: IDialogService,736@ICommandService private readonly commandService: ICommandService,737@IContextMenuService private readonly contextMenuService: IContextMenuService738) {739super();740}741742renderTemplate(container: HTMLElement): IActionsColumnTemplateData {743const disposables = new DisposableStore();744const elementDisposables = new DisposableStore();745container.classList.add('models-actions-column');746const parent = DOM.append(container, $('.actions-container'));747const actionBar = disposables.add(this.instantiationService.createInstance(ToolBar,748parent,749this.contextMenuService,750{751icon: true,752label: false,753moreIcon: Codicon.gear,754anchorAlignmentProvider: () => AnchorAlignment.RIGHT755}756));757return {758container,759actionBar,760disposables,761elementDisposables762};763}764765override renderElement(entry: IViewModelEntry, index: number, templateData: IActionsColumnTemplateData): void {766templateData.actionBar.setActions([]);767super.renderElement(entry, index, templateData);768}769770override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IActionsColumnTemplateData): void {771const { vendorEntry } = entry;772const primaryActions: IAction[] = [];773const secondaryActions: IAction[] = [];774if (vendorEntry.vendor.configuration) {775secondaryActions.push(toAction({776id: 'configureAction',777label: localize('models.configure', 'Configure...'),778run: () => this.languageModelsService.configureLanguageModelsProviderGroup(vendorEntry.vendor.vendor, vendorEntry.group.name)779}));780secondaryActions.push(toAction({781id: 'deleteAction',782label: localize('models.deleteAction', 'Delete'),783class: ThemeIcon.asClassName(Codicon.trash),784run: async () => {785const result = await this.dialogService.confirm({786type: 'info',787message: localize('models.deleteConfirmation', "Would you like to delete {0}?", vendorEntry.group.name)788});789if (!result.confirmed) {790return;791}792await this.languageModelsService.removeLanguageModelsProviderGroup(vendorEntry.vendor.vendor, vendorEntry.group.name);793}794}));795} else if (vendorEntry.vendor.managementCommand) {796primaryActions.push(toAction({797id: 'manageVendor',798label: localize('models.manageProvider', 'Manage {0}...', vendorEntry.group.name),799class: ThemeIcon.asClassName(Codicon.gear),800run: async () => {801await this.commandService.executeCommand(vendorEntry.vendor.managementCommand!, vendorEntry.vendor.vendor);802this.viewModel.refresh();803}804}));805}806templateData.actionBar.setActions(primaryActions, secondaryActions);807}808809override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IActionsColumnTemplateData): void {810}811812override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IActionsColumnTemplateData): void {813}814}815816interface IProviderColumnTemplateData extends IModelTableColumnTemplateData {817readonly providerElement: HTMLElement;818}819820class ProviderColumnRenderer extends ModelsTableColumnRenderer<IProviderColumnTemplateData> {821static readonly TEMPLATE_ID = 'provider';822823readonly templateId: string = ProviderColumnRenderer.TEMPLATE_ID;824825renderTemplate(container: HTMLElement): IProviderColumnTemplateData {826const disposables = new DisposableStore();827const elementDisposables = new DisposableStore();828const providerElement = DOM.append(container, $('.model-provider'));829return {830container,831providerElement,832disposables,833elementDisposables834};835}836837override renderVendorElement(entry: ILanguageModelProviderEntry, index: number, templateData: IProviderColumnTemplateData): void {838templateData.providerElement.textContent = '';839}840841override renderGroupElement(entry: ILanguageModelGroupEntry, index: number, templateData: IProviderColumnTemplateData): void {842templateData.providerElement.textContent = '';843}844845override renderModelElement(entry: ILanguageModelEntry, index: number, templateData: IProviderColumnTemplateData): void {846templateData.providerElement.textContent = entry.model.provider.vendor.displayName;847}848}849850851852function formatTokenCount(count: number): string {853if (count >= 1000000) {854return `${(count / 1000000).toFixed(1)}M`;855} else if (count >= 1000) {856return `${(count / 1000).toFixed(0)}K`;857}858return count.toString();859}860861export class ChatModelsWidget extends Disposable {862863private static NUM_INSTANCES: number = 0;864865readonly element: HTMLElement;866private searchWidget!: SuggestEnabledInput;867private searchActionsContainer!: HTMLElement;868private table!: WorkbenchTable<IViewModelEntry>;869private tableContainer!: HTMLElement;870private addButtonContainer!: HTMLElement;871private addButton!: Button;872private dropdownActions: IAction[] = [];873private viewModel: ChatModelsViewModel;874private delayedFiltering: Delayer<void>;875876private readonly searchFocusContextKey: IContextKey<boolean>;877878private tableDisposables = this._register(new DisposableStore());879880constructor(881@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,882@IInstantiationService private readonly instantiationService: IInstantiationService,883@IExtensionService private readonly extensionService: IExtensionService,884@IContextMenuService private readonly contextMenuService: IContextMenuService,885@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,886@IEditorProgressService private readonly editorProgressService: IEditorProgressService,887@ICommandService private readonly commandService: ICommandService,888@IContextKeyService contextKeyService: IContextKeyService,889) {890super();891892this.searchFocusContextKey = CONTEXT_MODELS_SEARCH_FOCUS.bindTo(contextKeyService);893this.delayedFiltering = this._register(new Delayer<void>(200));894this.viewModel = this._register(this.instantiationService.createInstance(ChatModelsViewModel));895this.element = DOM.$('.models-widget');896this.create(this.element);897898const loadingPromise = this.extensionService.whenInstalledExtensionsRegistered().then(() => this.viewModel.refresh());899this.editorProgressService.showWhile(loadingPromise, 300);900}901902private create(container: HTMLElement): void {903const searchAndButtonContainer = DOM.append(container, $('.models-search-and-button-container'));904905const placeholder = localize('Search.FullTextSearchPlaceholder', "Type to search...");906const searchContainer = DOM.append(searchAndButtonContainer, $('.models-search-container'));907this.searchWidget = this._register(this.instantiationService.createInstance(908SuggestEnabledInput,909'chatModelsWidget.searchbox',910searchContainer,911{912triggerCharacters: ['@', ':'],913provideResults: (query: string) => {914const providerSuggestions = this.viewModel.getVendors().map(v => `@provider:"${v.displayName}"`);915const allSuggestions = [916...providerSuggestions,917...SEARCH_SUGGESTIONS.CAPABILITIES,918...SEARCH_SUGGESTIONS.VISIBILITY,919];920if (!query.trim()) {921return allSuggestions;922}923const queryParts = query.split(/\s/g);924const lastPart = queryParts[queryParts.length - 1];925if (lastPart.startsWith('@provider:')) {926return providerSuggestions;927} else if (lastPart.startsWith('@capability:')) {928return SEARCH_SUGGESTIONS.CAPABILITIES;929} else if (lastPart.startsWith('@visible:')) {930return SEARCH_SUGGESTIONS.VISIBILITY;931} else if (lastPart.startsWith('@')) {932return allSuggestions;933}934return [];935}936},937placeholder,938`chatModelsWidget:searchinput:${ChatModelsWidget.NUM_INSTANCES++}`,939{940placeholderText: placeholder,941styleOverrides: {942inputBorder: settingsTextInputBorder943},944focusContextKey: this.searchFocusContextKey,945},946));947948const filterAction = this._register(new ModelsFilterAction());949const clearSearchAction = this._register(new Action(950'workbench.models.clearSearch',951localize('clearSearch', "Clear Search"),952ThemeIcon.asClassName(preferencesClearInputIcon),953false,954() => this.clearSearch()955));956const collapseAllAction = this._register(new Action(957'workbench.models.collapseAll',958localize('collapseAll', "Collapse All"),959ThemeIcon.asClassName(Codicon.collapseAll),960false,961() => {962this.viewModel.collapseAll();963}964));965collapseAllAction.enabled = this.viewModel.viewModelEntries.some(e => isLanguageModelGroupEntry(e) || isLanguageModelProviderEntry(e));966this._register(this.viewModel.onDidChange(() => collapseAllAction.enabled = this.viewModel.viewModelEntries.some(e => isLanguageModelProviderEntry(e) || isLanguageModelGroupEntry(e))));967968this._register(this.searchWidget.onInputDidChange(() => {969clearSearchAction.enabled = !!this.searchWidget.getValue();970this.filterModels();971}));972973this.searchActionsContainer = DOM.append(searchContainer, $('.models-search-actions'));974const actions = [clearSearchAction, collapseAllAction, filterAction];975const toolBar = this._register(new ToolBar(this.searchActionsContainer, this.contextMenuService, {976actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {977if (action.id === filterAction.id) {978return this.instantiationService.createInstance(ModelsSearchFilterDropdownMenuActionViewItem, action, options, {979getValue: () => this.searchWidget.getValue(),980setValue: (searchValue) => this.search(searchValue)981}, this.viewModel);982}983return undefined;984},985getKeyBinding: () => undefined986}));987toolBar.setActions(actions);988989// Add padding to input box for toolbar990this.searchWidget.inputWidget.getContainerDomNode().style.paddingRight = `${DOM.getTotalWidth(this.searchActionsContainer) + 12}px`;991992this.addButtonContainer = DOM.append(searchAndButtonContainer, $('.section-title-actions'));993const buttonOptions: IButtonOptions = {994...defaultButtonStyles,995supportIcons: true,996};997this.addButton = this._register(new Button(this.addButtonContainer, buttonOptions));998this.addButton.label = `$(${Codicon.add.id}) ${localize('models.enableModelProvider', 'Add Models...')}`;999this.addButton.element.classList.add('models-add-model-button');1000this.updateAddModelsButton();1001this._register(this.addButton.onDidClick((e) => {1002if (this.dropdownActions.length > 0) {1003this.contextMenuService.showContextMenu({1004getAnchor: () => this.addButton.element,1005getActions: () => this.dropdownActions,1006});1007}1008}));10091010// Table container1011this.tableContainer = DOM.append(container, $('.models-table-container'));10121013// Create table1014this.createTable();1015this._register(this.viewModel.onDidChangeGrouping(() => this.createTable()));1016this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.updateAddModelsButton()));1017this._register(this.languageModelsService.onDidChangeLanguageModelVendors(() => this.updateAddModelsButton()));1018}10191020private createTable(): void {1021this.tableDisposables.clear();1022DOM.clearNode(this.tableContainer);10231024const gutterColumnRenderer = this.instantiationService.createInstance(GutterColumnRenderer, this.viewModel);1025const modelNameColumnRenderer = this.instantiationService.createInstance(ModelNameColumnRenderer);1026const costColumnRenderer = this.instantiationService.createInstance(MultiplierColumnRenderer);1027const tokenLimitsColumnRenderer = this.instantiationService.createInstance(TokenLimitsColumnRenderer);1028const capabilitiesColumnRenderer = this.instantiationService.createInstance(CapabilitiesColumnRenderer);1029const actionsColumnRenderer = this.instantiationService.createInstance(ActionsColumnRenderer, this.viewModel);1030const providerColumnRenderer = this.instantiationService.createInstance(ProviderColumnRenderer);10311032this.tableDisposables.add(capabilitiesColumnRenderer.onDidClickCapability(capability => {1033const currentQuery = this.searchWidget.getValue();1034const query = `@capability:${capability}`;1035const newQuery = toggleFilter(currentQuery, { query });1036this.search(newQuery);1037}));10381039const columns = [1040{1041label: '',1042tooltip: '',1043weight: 0.05,1044minimumWidth: 40,1045maximumWidth: 40,1046templateId: GutterColumnRenderer.TEMPLATE_ID,1047project(row: IViewModelEntry): IViewModelEntry { return row; }1048},1049{1050label: localize('modelName', 'Name'),1051tooltip: '',1052weight: 0.35,1053minimumWidth: 200,1054templateId: ModelNameColumnRenderer.TEMPLATE_ID,1055project(row: IViewModelEntry): IViewModelEntry { return row; }1056}1057];10581059if (this.viewModel.groupBy === ChatModelGroup.Visibility) {1060columns.push({1061label: localize('provider', 'Provider'),1062tooltip: '',1063weight: 0.15,1064minimumWidth: 100,1065templateId: ProviderColumnRenderer.TEMPLATE_ID,1066project(row: IViewModelEntry): IViewModelEntry { return row; }1067});1068}10691070columns.push(1071{1072label: localize('tokenLimits', 'Context Size'),1073tooltip: '',1074weight: 0.1,1075minimumWidth: 140,1076templateId: TokenLimitsColumnRenderer.TEMPLATE_ID,1077project(row: IViewModelEntry): IViewModelEntry { return row; }1078},1079{1080label: localize('capabilities', 'Capabilities'),1081tooltip: '',1082weight: 0.2,1083minimumWidth: 180,1084templateId: CapabilitiesColumnRenderer.TEMPLATE_ID,1085project(row: IViewModelEntry): IViewModelEntry { return row; }1086},1087{1088label: localize('cost', 'Request Multiplier'),1089tooltip: '',1090weight: 0.1,1091minimumWidth: 60,1092templateId: MultiplierColumnRenderer.TEMPLATE_ID,1093project(row: IViewModelEntry): IViewModelEntry { return row; }1094},1095{1096label: '',1097tooltip: '',1098weight: 0.05,1099minimumWidth: 64,1100maximumWidth: 64,1101templateId: ActionsColumnRenderer.TEMPLATE_ID,1102project(row: IViewModelEntry): IViewModelEntry { return row; }1103}1104);11051106this.table = this.tableDisposables.add(this.instantiationService.createInstance(1107WorkbenchTable,1108'ModelsWidget',1109this.tableContainer,1110new Delegate(),1111columns,1112[1113gutterColumnRenderer,1114modelNameColumnRenderer,1115costColumnRenderer,1116tokenLimitsColumnRenderer,1117capabilitiesColumnRenderer,1118actionsColumnRenderer,1119providerColumnRenderer1120],1121{1122identityProvider: { getId: (e: IViewModelEntry) => e.id },1123horizontalScrolling: false,1124accessibilityProvider: {1125getAriaLabel: (e: IViewModelEntry) => {1126if (isLanguageModelProviderEntry(e)) {1127return localize('vendor.ariaLabel', '{0} Models', e.vendorEntry.group.name);1128} else if (isLanguageModelGroupEntry(e)) {1129return e.id === 'visible' ? localize('visible.ariaLabel', 'Visible Models') : localize('hidden.ariaLabel', 'Hidden Models');1130} else if (isStatusEntry(e)) {1131return localize('status.ariaLabel', 'Status: {0}', e.message);1132}1133const ariaLabels = [];1134ariaLabels.push(localize('model.name', '{0} from {1}', e.model.metadata.name, e.model.provider.vendor.displayName));1135if (e.model.metadata.maxInputTokens && e.model.metadata.maxOutputTokens) {1136ariaLabels.push(localize('model.contextSize', 'Context size: {0} input tokens and {1} output tokens', formatTokenCount(e.model.metadata.maxInputTokens), formatTokenCount(e.model.metadata.maxOutputTokens)));1137}1138if (e.model.metadata.capabilities) {1139ariaLabels.push(localize('model.capabilities', 'Capabilities: {0}', Object.keys(e.model.metadata.capabilities).join(', ')));1140}1141const multiplierText = e.model.metadata.multiplier ?? '-';1142if (multiplierText !== '-') {1143ariaLabels.push(localize('multiplier.tooltip', "Every chat message counts {0} towards your premium model request quota", multiplierText));1144}1145if (e.model.visible) {1146ariaLabels.push(localize('model.visible', 'This model is visible in the chat model picker'));1147} else {1148ariaLabels.push(localize('model.hidden', 'This model is hidden in the chat model picker'));1149}1150return ariaLabels.join('. ');1151},1152getWidgetAriaLabel: () => localize('modelsTable.ariaLabel', 'Language Models')1153},1154multipleSelectionSupport: true,1155setRowLineHeight: false,1156openOnSingleClick: true,1157alwaysConsumeMouseWheel: false,1158}1159)) as WorkbenchTable<IViewModelEntry>;11601161this.tableDisposables.add(this.table.onContextMenu(e => {1162if (!e.element) {1163return;1164}11651166const selection = this.table.getSelection();1167const selectedEntries = selection.every(i => i !== e.index) ? [e.element] : selection.map(i => this.viewModel.viewModelEntries[i]).filter(e => !!e);11681169// Get model entries from selection (filter out vendor/group/status entries)1170const selectedModelEntries = selectedEntries.filter((entry): entry is ILanguageModelEntry =>1171!isLanguageModelProviderEntry(entry) && !isLanguageModelGroupEntry(entry) && !isStatusEntry(entry)1172);11731174const actions: IAction[] = [];1175let configureGroup: string | undefined;1176let configureVendor: ILanguageModelProviderDescriptor | undefined;11771178if (selectedModelEntries.length) {1179const visibleModels = selectedModelEntries.filter(entry => entry.model.visible);1180const hiddenModels = selectedModelEntries.filter(entry => !entry.model.visible);11811182actions.push(toAction({1183id: 'hideSelectedModels',1184label: localize('models.hideSelected', 'Hide in the Chat Model Picker'),1185enabled: visibleModels.length > 0,1186run: () => this.viewModel.setModelsVisibility(selectedModelEntries, false)1187}));11881189actions.push(toAction({1190id: 'showSelectedModels',1191label: localize('models.showSelected', 'Show in the Chat Model Picker'),1192enabled: hiddenModels.length > 0,1193run: () => this.viewModel.setModelsVisibility(selectedModelEntries, true)1194}));11951196// Show configure action if all models are from the same group1197configureGroup = selectedModelEntries[0].model.provider.group.name;1198configureVendor = selectedModelEntries[0].model.provider.vendor;1199if (selectedModelEntries.some(entry => entry.model.provider.vendor.isDefault || entry.model.provider.group.name !== configureGroup)) {1200configureGroup = undefined;1201configureVendor = undefined;1202}1203} else if (selectedEntries.length === 1) {1204const entry = e.element;1205if (isLanguageModelProviderEntry(entry)) {1206if (!entry.vendorEntry.vendor.isDefault) {1207actions.push(toAction({1208id: 'hideAllModels',1209label: localize('models.hideAll', 'Hide in the Chat Model Picker'),1210run: () => this.viewModel.setGroupVisibility(entry, false)1211}));1212actions.push(toAction({1213id: 'showAllModels',1214label: localize('models.showAll', 'Show in the Chat Model Picker'),1215run: () => this.viewModel.setGroupVisibility(entry, true)1216}));1217}1218configureGroup = entry.vendorEntry.group.name;1219configureVendor = entry.vendorEntry.vendor;1220}1221}12221223if (configureGroup && configureVendor) {1224if (configureVendor.managementCommand || configureVendor.configuration) {1225if (actions.length) {1226actions.push(new Separator());1227}1228if (configureVendor.managementCommand) {1229actions.push(toAction({1230id: 'configureVendor',1231label: localize('models.configureContextMenu', 'Configure'),1232run: async () => {1233await this.commandService.executeCommand(configureVendor.managementCommand!, configureVendor.vendor);1234await this.viewModel.refresh();1235}1236}));1237} else {1238actions.push(toAction({1239id: 'configureVendor',1240label: localize('models.configureContextMenu', 'Configure'),1241run: () => this.languageModelsService.configureLanguageModelsProviderGroup(configureVendor.vendor, configureGroup!)1242}));1243}1244}1245}12461247if (actions.length > 0) {1248this.contextMenuService.showContextMenu({1249getAnchor: () => e.anchor,1250getActions: () => actions1251});1252}1253}));12541255this.table.splice(0, this.table.length, this.viewModel.viewModelEntries);1256this.tableDisposables.add(this.viewModel.onDidChange(({ at, removed, added }) => {1257this.table.splice(at, removed, added);1258if (this.viewModel.selectedEntry) {1259const selectedEntryIndex = this.viewModel.viewModelEntries.indexOf(this.viewModel.selectedEntry);1260this.table.setFocus([selectedEntryIndex]);1261this.table.setSelection([selectedEntryIndex]);1262}1263}));12641265this.tableDisposables.add(this.table.onDidOpen(async ({ element, browserEvent }) => {1266if (!element) {1267return;1268}1269if (isStatusEntry(element)) {1270return;1271}1272if (isLanguageModelProviderEntry(element) || isLanguageModelGroupEntry(element)) {1273this.viewModel.toggleCollapsed(element);1274} else if (!DOM.isMouseEvent(browserEvent) || browserEvent.detail === 2) {1275this.viewModel.toggleVisibility(element);1276}1277}));12781279this.tableDisposables.add(this.table.onDidChangeSelection(e => this.viewModel.selectedEntry = e.elements[0]));12801281this.tableDisposables.add(this.table.onDidBlur(() => {1282if (this.viewModel.shouldRefilter()) {1283this.viewModel.filter(this.searchWidget.getValue());1284}1285}));12861287this.layout(this.element.clientHeight, this.element.clientWidth);1288}12891290private updateAddModelsButton(): void {1291const configurableVendors = this.languageModelsService.getVendors().filter(vendor => vendor.managementCommand || vendor.configuration);12921293const entitlement = this.chatEntitlementService.entitlement;1294const isManagedEntitlement = entitlement === ChatEntitlement.Business || entitlement === ChatEntitlement.Enterprise;1295const supportsAddingModels = this.chatEntitlementService.isInternal1296|| (entitlement !== ChatEntitlement.Unknown1297&& entitlement !== ChatEntitlement.Available1298&& !isManagedEntitlement);12991300this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0;1301this.addButton.setTitle(!supportsAddingModels && isManagedEntitlement ? localize('models.managedByOrganization', "Adding models is managed by your organization") : '');13021303this.dropdownActions = configurableVendors.map(vendor => toAction({1304id: `enable-${vendor.vendor}`,1305label: vendor.displayName,1306run: async () => {1307await this.addModelsForVendor(vendor);1308}1309}));1310}13111312private filterModels(): void {1313this.delayedFiltering.trigger(() => {1314this.viewModel.filter(this.searchWidget.getValue());1315});1316}13171318private async addModelsForVendor(vendor: ILanguageModelProviderDescriptor): Promise<void> {1319this.languageModelsService.configureLanguageModelsProviderGroup(vendor.vendor);1320}13211322public layout(height: number, width: number): void {1323width = width - 24;1324this.searchWidget.layout(new DOM.Dimension(width - this.searchActionsContainer.clientWidth - this.addButtonContainer.clientWidth - 8, 22));1325const tableHeight = height - 40;1326this.tableContainer.style.height = `${tableHeight}px`;1327this.table.layout(tableHeight, width);1328}13291330public focusSearch(): void {1331this.searchWidget.focus();1332}13331334public search(filter: string): void {1335this.focusSearch();1336this.searchWidget.setValue(filter);1337this.viewModel.filter(filter);1338}13391340public clearSearch(): void {1341this.focusSearch();1342this.searchWidget.setValue('');1343}13441345public render(): void {1346if (this.viewModel.shouldRefilter()) {1347this.viewModel.filter(this.searchWidget.getValue());1348}1349}13501351}135213531354