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
This commit is contained in:
Sandeep Somavarapu
2026-03-09 16:06:15 +01:00
committed by GitHub
parent 812058f3c3
commit 4a4e6624c3
4 changed files with 77 additions and 36 deletions

View File

@@ -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<T> 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;

View File

@@ -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);
}

View File

@@ -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<IActionWidgetDropdownAction>[] {
const items: IActionListItem<IActionWidgetDropdownAction>[] = [];
@@ -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,

View File

@@ -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<IActionWidgetDropdownAction>[]): IActionListItem<IActionWidgetDropdownAction>[] {
return items.filter(i => i.kind === ActionListItemKind.Action);
}
@@ -67,6 +59,16 @@ function getSeparatorCount(items: IActionListItem<IActionWidgetDropdownAction>[]
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');