Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/modelPicker.ts
13401 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 * as dom from '../../../../base/browser/dom.js';6import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';7import { Codicon } from '../../../../base/common/codicons.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';10import { autorun } from '../../../../base/common/observable.js';11import { localize } from '../../../../nls.js';12import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';13import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';14import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';15import { IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';16import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';17import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';18import { CopilotChatSessionsProvider, RemoteNewSession } from './copilotChatSessionsProvider.js';1920const FILTER_THRESHOLD = 10;2122interface IModelItem {23readonly id: string;24readonly name: string;25readonly description?: string;26}2728/**29* A self-contained widget for selecting a model in cloud sessions.30* Reads the model option group from the {@link RemoteNewSession} and31* renders an action list dropdown with the available models.32*/33export class CloudModelPicker extends Disposable {3435private readonly _onDidChange = this._register(new Emitter<IChatSessionProviderOptionItem>());36readonly onDidChange: Event<IChatSessionProviderOptionItem> = this._onDidChange.event;3738private _triggerElement: HTMLElement | undefined;39private _slotElement: HTMLElement | undefined;40private readonly _renderDisposables = this._register(new DisposableStore());41private readonly _sessionDisposables = this._register(new DisposableStore());4243private _session: RemoteNewSession | undefined;44private _selectedModel: IModelItem | undefined;45private _models: IModelItem[] = [];4647get selectedModel(): IModelItem | undefined {48return this._selectedModel;49}5051constructor(52@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,53@ISessionsManagementService sessionsManagementService: ISessionsManagementService,54@ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService,55@IChatSessionsService chatSessionsService: IChatSessionsService,56) {57super();5859this._register(autorun(reader => {60const session = sessionsManagementService.activeSession.read(reader);61const provider = session ? sessionsProvidersService.getProvider(session.providerId) : undefined;62const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;63if (providerSession instanceof RemoteNewSession) {64this._setSession(providerSession);65}66}));6768// Also listen directly for option group changes from the extension host,69// in case they arrive before the RemoteNewSession relays the event.70this._register(chatSessionsService.onDidChangeOptionGroups(() => {71if (this._session) {72this._loadModels(this._session);73}74}));75}7677private _setSession(session: RemoteNewSession): void {78this._session = session;79this._sessionDisposables.clear();80this._loadModels(session);8182// Sync selected model to the new session83if (this._selectedModel) {84session.setModelId(this._selectedModel.id);85session.setOptionValue('models', { id: this._selectedModel.id, name: this._selectedModel.name });86}8788// Re-load models when option groups change89this._sessionDisposables.add(session.onDidChangeOptionGroups(() => {90this._loadModels(session);91}));92}9394/**95* Renders the model picker trigger button into the given container.96*/97render(container: HTMLElement): HTMLElement {98this._renderDisposables.clear();99100const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));101this._slotElement = slot;102this._renderDisposables.add({ dispose: () => slot.remove() });103104const trigger = dom.append(slot, dom.$('a.action-label'));105trigger.tabIndex = 0;106trigger.role = 'button';107this._triggerElement = trigger;108109this._updateTriggerLabel();110111this._renderDisposables.add(Gesture.addTarget(trigger));112for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {113this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {114dom.EventHelper.stop(e, true);115this._showPicker();116}));117}118119this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {120if (e.key === 'Enter' || e.key === ' ') {121dom.EventHelper.stop(e, true);122this._showPicker();123}124}));125126return slot;127}128129private _loadModels(session: RemoteNewSession): void {130const modelOption = session.getModelOptionGroup();131if (modelOption?.group.items.length) {132this._models = modelOption.group.items.map(item => ({133id: item.id,134name: item.name,135description: item.description,136}));137138// Select the session's current value, or the default, or the first139if (!this._selectedModel || !this._models.some(m => m.id === this._selectedModel!.id)) {140const value = modelOption.value;141this._selectedModel = value142? { id: value.id, name: value.name, description: value.description }143: this._models[0];144}145} else {146this._models = [];147}148this._updateTriggerLabel();149}150151private _showPicker(): void {152if (!this._triggerElement || this.actionWidgetService.isVisible || this._models.length === 0) {153return;154}155156const items = this._buildItems();157const showFilter = items.filter(i => i.kind === ActionListItemKind.Action).length > FILTER_THRESHOLD;158159const triggerElement = this._triggerElement;160const delegate: IActionListDelegate<IModelItem> = {161onSelect: (item) => {162this.actionWidgetService.hide();163this._selectModel(item);164},165onHide: () => { triggerElement.focus(); },166};167168this.actionWidgetService.show<IModelItem>(169'remoteModelPicker',170false,171items,172delegate,173this._triggerElement,174undefined,175[],176{177getAriaLabel: (item) => item.label ?? '',178getWidgetAriaLabel: () => localize('modelPicker.ariaLabel', "Model Picker"),179},180showFilter ? { showFilter: true, filterPlaceholder: localize('modelPicker.filter', "Filter models...") } : undefined,181);182}183184private _buildItems(): IActionListItem<IModelItem>[] {185return this._models.map(model => ({186kind: ActionListItemKind.Action,187label: model.name,188group: { title: '', icon: this._selectedModel?.id === model.id ? Codicon.check : Codicon.blank },189item: model,190}));191}192193private _selectModel(item: IModelItem): void {194this._selectedModel = item;195this._updateTriggerLabel();196197if (this._session) {198this._session.setModelId(item.id);199this._session.setOptionValue('models', { id: item.id, name: item.name });200}201this._onDidChange.fire({ id: item.id, name: item.name, description: item.description });202}203204private _updateTriggerLabel(): void {205if (!this._triggerElement) {206return;207}208209dom.clearNode(this._triggerElement);210const label = this._selectedModel?.name ?? localize('modelPicker.auto', "Auto");211212const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));213labelSpan.textContent = label;214dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));215216this._triggerElement.ariaLabel = localize('modelPicker.triggerAriaLabel', "Pick Model, {0}", label);217218this._slotElement?.classList.toggle('disabled', this._models.length === 0);219this._triggerElement.setAttribute('aria-disabled', String(this._models.length === 0));220}221}222223224