From 4a4e6624c3c0aa57f56d5c6859fdfd0ecd14cb7b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Mar 2026 16:06:15 +0100 Subject: [PATCH] Show manage models action in the search input (#300165) * Add filter actions to ActionList and manage models option in chat model picker * Refactor model picker to support additional entries and improve manage models action handling * Refactor model picker to conditionally add separator for additional entries and streamline manage models action handling * Improve manage models action visibility logic in ModelPickerWidget * Refactor model picker to streamline additional entry handling and improve action management * Refactor model picker to integrate manage models action and streamline additional entry handling --- .../actionWidget/browser/actionList.ts | 15 +++++- .../actionWidget/browser/actionWidget.css | 18 ++++++- .../browser/widget/input/chatModelPicker.ts | 54 +++++++++++-------- .../widget/input/chatModelPicker.test.ts | 26 ++++----- 4 files changed, 77 insertions(+), 36 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 71f8ee665a2..30aa721ddd5 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -359,6 +359,11 @@ export interface IActionListOptions { */ readonly filterPlaceholder?: string; + /** + * Optional actions shown in the filter row, to the right of the input. + */ + readonly filterActions?: readonly IAction[]; + /** * Section IDs that should be collapsed by default. */ @@ -516,13 +521,21 @@ export class ActionList extends Disposable { if (this._options?.showFilter) { this._filterContainer = document.createElement('div'); this._filterContainer.className = 'action-list-filter'; + const filterRow = dom.append(this._filterContainer, dom.$('.action-list-filter-row')); this._filterInput = document.createElement('input'); this._filterInput.type = 'text'; this._filterInput.className = 'action-list-filter-input'; this._filterInput.placeholder = this._options?.filterPlaceholder ?? localize('actionList.filter.placeholder', "Search..."); this._filterInput.setAttribute('aria-label', localize('actionList.filter.ariaLabel', "Filter items")); - this._filterContainer.appendChild(this._filterInput); + filterRow.appendChild(this._filterInput); + + const filterActions = this._options?.filterActions ?? []; + if (filterActions.length > 0) { + const filterActionsContainer = dom.append(filterRow, dom.$('.action-list-filter-actions')); + const filterActionBar = this._register(new ActionBar(filterActionsContainer)); + filterActionBar.push(filterActions, { icon: true, label: false }); + } this._register(dom.addDisposableListener(this._filterInput, 'input', () => { this._filterText = this._filterInput!.value; diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 31c753580b2..8957a85b01f 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -265,7 +265,13 @@ /* Filter input */ .action-widget .action-list-filter { - padding: 2px 2px 4px 2px + padding: 2px 2px 4px 2px; +} + +.action-widget .action-list-filter-row { + display: flex; + align-items: center; + gap: 4px; } .action-widget .action-list-filter:first-child { @@ -278,6 +284,7 @@ .action-widget .action-list-filter-input { width: 100%; + flex: 1; box-sizing: border-box; padding: 4px 8px; border: 1px solid var(--vscode-input-border, transparent); @@ -294,3 +301,12 @@ .action-widget .action-list-filter-input::placeholder { color: var(--vscode-input-placeholderForeground); } + +.action-widget .action-list-filter-actions .action-label { + padding: 3px; + border-radius: 3px; +} + +.action-widget .action-list-filter-actions .action-label:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index ce390754ad2..2c5f04d1e39 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -109,6 +109,27 @@ function createModelAction( }; } +function shouldShowManageModelsAction(chatEntitlementService: IChatEntitlementService): boolean { + return chatEntitlementService.entitlement === ChatEntitlement.Free || + chatEntitlementService.entitlement === ChatEntitlement.Pro || + chatEntitlementService.entitlement === ChatEntitlement.ProPlus || + chatEntitlementService.entitlement === ChatEntitlement.Business || + chatEntitlementService.entitlement === ChatEntitlement.Enterprise || + chatEntitlementService.isInternal; +} + +function createManageModelsAction(commandService: ICommandService): IActionWidgetDropdownAction { + return { + id: 'manageModels', + enabled: true, + checked: false, + class: ThemeIcon.asClassName(Codicon.gear), + tooltip: localize('chat.manageModels.tooltip', "Manage Language Models"), + label: localize('chat.manageModels', "Manage Models..."), + run: () => { commandService.executeCommand(MANAGE_CHAT_COMMAND_ID); } + }; +} + /** * Builds the grouped items for the model picker dropdown. * @@ -118,7 +139,7 @@ function createModelAction( * - Available models sorted alphabetically, followed by unavailable models * - Unavailable models show upgrade/update/admin status * 3. Other Models (collapsible toggle, available first, then sorted by vendor then name) - * - Last item is "Manage Models..." (always visible during filtering) + * 4. Optional "Manage Models..." action shown in Other Models after a separator */ export function buildModelPickerItems( models: ILanguageModelChatMetadataAndIdentifier[], @@ -130,7 +151,7 @@ export function buildModelPickerItems( onSelect: (model: ILanguageModelChatMetadataAndIdentifier) => void, manageSettingsUrl: string | undefined, canManageModels: boolean, - commandService: ICommandService, + manageModelsAction: IActionWidgetDropdownAction | undefined, chatEntitlementService: IChatEntitlementService, ): IActionListItem[] { const items: IActionListItem[] = []; @@ -340,27 +361,12 @@ export function buildModelPickerItems( } } - if ( - chatEntitlementService.entitlement === ChatEntitlement.Free || - chatEntitlementService.entitlement === ChatEntitlement.Pro || - chatEntitlementService.entitlement === ChatEntitlement.ProPlus || - chatEntitlementService.entitlement === ChatEntitlement.Business || - chatEntitlementService.entitlement === ChatEntitlement.Enterprise || - chatEntitlementService.isInternal - ) { + if (manageModelsAction) { items.push({ kind: ActionListItemKind.Separator, section: otherModels.length ? ModelPickerSection.Other : undefined }); items.push({ - item: { - id: 'manageModels', - enabled: true, - checked: false, - class: undefined, - tooltip: localize('chat.manageModels.tooltip', "Manage Language Models"), - label: localize('chat.manageModels', "Manage Models..."), - run: () => { commandService.executeCommand(MANAGE_CHAT_COMMAND_ID); } - }, + item: manageModelsAction, kind: ActionListItemKind.Action, - label: localize('chat.manageModels', "Manage Models..."), + label: manageModelsAction.label, group: { title: '', icon: Codicon.blank }, hideIcon: false, section: otherModels.length ? ModelPickerSection.Other : undefined, @@ -562,9 +568,12 @@ export class ModelPickerWidget extends Disposable { }; const models = this._delegate.getModels(); + const showFilter = models.length >= 10; const isPro = isProUser(this._entitlementService.entitlement); const manifest = this._languageModelsService.getModelsControlManifest(); const controlModelsForTier = isPro ? manifest.paid : manifest.free; + const canShowManageModelsAction = this._delegate.canManageModels() && shouldShowManageModelsAction(this._entitlementService); + const manageModelsAction = canShowManageModelsAction ? createManageModelsAction(this._commandService) : undefined; const items = buildModelPickerItems( models, this._selectedModel?.identifier, @@ -575,13 +584,14 @@ export class ModelPickerWidget extends Disposable { onSelect, this._productService.defaultChatAgent?.manageSettingsUrl, this._delegate.canManageModels(), - this._commandService, + !showFilter ? manageModelsAction : undefined, this._entitlementService, ); const listOptions = { - showFilter: models.length >= 10, + showFilter, filterPlaceholder: localize('chat.modelPicker.search', "Search models"), + filterActions: showFilter && manageModelsAction ? [manageModelsAction] : undefined, focusFilterOnOpen: true, collapsedByDefault: new Set([ModelPickerSection.Other]), minWidth: 200, diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts index d74122c7f61..11be03e3f31 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelPicker.test.ts @@ -10,7 +10,6 @@ import { IStringDictionary } from '../../../../../../../base/common/collections. import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { ActionListItemKind, IActionListItem } from '../../../../../../../platform/actionWidget/browser/actionList.js'; import { IActionWidgetDropdownAction } from '../../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; -import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; import { StateType } from '../../../../../../../platform/update/common/update.js'; import { buildModelPickerItems, getModelPickerAccessibilityProvider } from '../../../../browser/widget/input/chatModelPicker.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, IModelControlEntry } from '../../../../common/languageModels.js'; @@ -48,13 +47,6 @@ function createAutoModel(): ILanguageModelChatMetadataAndIdentifier { return createModel('auto', 'Auto', 'copilot'); } -const stubCommandService: ICommandService = { - _serviceBrand: undefined, - onWillExecuteCommand: () => ({ dispose() { } }), - onDidExecuteCommand: () => ({ dispose() { } }), - executeCommand: () => Promise.resolve(undefined), -}; - function getActionItems(items: IActionListItem[]): IActionListItem[] { return items.filter(i => i.kind === ActionListItemKind.Action); } @@ -67,6 +59,16 @@ function getSeparatorCount(items: IActionListItem[] return items.filter(i => i.kind === ActionListItemKind.Separator).length; } +const stubManageModelsAction: IActionWidgetDropdownAction = { + id: 'manageModels', + enabled: true, + checked: false, + class: undefined, + tooltip: 'Manage Language Models', + label: 'Manage Models...', + run: () => { } +}; + function callBuild( models: ILanguageModelChatMetadataAndIdentifier[], opts: { @@ -95,7 +97,7 @@ function callBuild( onSelect, opts.manageSettingsUrl, true, - stubCommandService, + stubManageModelsAction, entitlementService, ); } @@ -470,7 +472,7 @@ suite('buildModelPickerItems', () => { onSelect, undefined, true, - stubCommandService, + undefined, stubChatEntitlementService, ); const gptItem = getActionItems(items).find(a => a.label === 'GPT-4o'); @@ -552,7 +554,7 @@ suite('buildModelPickerItems', () => { () => { }, 'https://aka.ms/github-copilot-settings', true, - stubCommandService, + undefined, stubChatEntitlementService, ); @@ -635,7 +637,7 @@ suite('buildModelPickerItems', () => { onSelect, undefined, true, - stubCommandService, + undefined, anonymousEntitlementService, ); const gptItem = getActionItems(items).find(a => a.label === 'GPT-4o');