Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts
5289 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/chatSessionPickerActionItem.css';6import { IAction } from '../../../../../base/common/actions.js';7import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';8import { Delayer } from '../../../../../base/common/async.js';9import * as dom from '../../../../../base/browser/dom.js';10import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';11import { IActionWidgetDropdownAction } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';12import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';13import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';14import { IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../common/chatSessionsService.js';15import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';16import { renderLabelWithIcons, renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';17import { localize } from '../../../../../nls.js';18import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';19import { ThemeIcon } from '../../../../../base/common/themables.js';20import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from './chatSessionPickerActionItem.js';21import { ILogService } from '../../../../../platform/log/common/log.js';22import { ICommandService } from '../../../../../platform/commands/common/commands.js';23import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';2425interface ISearchableOptionQuickPickItem extends IQuickPickItem {26readonly optionItem: IChatSessionProviderOptionItem;27}2829function isSearchableOptionQuickPickItem(item: IQuickPickItem | undefined): item is ISearchableOptionQuickPickItem {30return !!item && typeof (item as ISearchableOptionQuickPickItem).optionItem === 'object';31}3233/**34* Action view item for searchable option groups with QuickPick.35* Used when an option group has `searchable: true` (e.g., repository selection).36* Shows an inline dropdown with items + "See more..." option that opens a searchable QuickPick.37*/38export class SearchableOptionPickerActionItem extends ChatSessionPickerActionItem {39private static readonly SEE_MORE_ID = '__see_more__';4041constructor(42action: IAction,43initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined },44delegate: IChatSessionPickerDelegate,45@IActionWidgetService actionWidgetService: IActionWidgetService,46@IContextKeyService contextKeyService: IContextKeyService,47@IKeybindingService keybindingService: IKeybindingService,48@IQuickInputService private readonly quickInputService: IQuickInputService,49@ILogService private readonly logService: ILogService,50@ICommandService commandService: ICommandService,51@ITelemetryService telemetryService: ITelemetryService,52) {53super(action, initialState, delegate, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService);54}5556protected override getDropdownActions(): IActionWidgetDropdownAction[] {57// If locked, show the current option only58const currentOption = this.delegate.getCurrentOption();59if (currentOption?.locked) {60return [this.createLockedOptionAction(currentOption)];61}6263const optionGroup = this.delegate.getOptionGroup();64if (!optionGroup) {65return [];66}6768// Build actions from items69const actions: IActionWidgetDropdownAction[] = optionGroup.items.map(optionItem => {70const isCurrent = optionItem.id === currentOption?.id;71return {72id: optionItem.id,73enabled: !optionItem.locked,74icon: optionItem.icon,75checked: isCurrent,76class: undefined,77description: optionItem.description,78tooltip: optionItem.description ?? optionItem.name,79label: optionItem.name,80run: () => {81this.delegate.setOption(optionItem);82}83};84});8586// Add "See more..." action if onSearch is available87if (optionGroup.onSearch) {88actions.push({89id: SearchableOptionPickerActionItem.SEE_MORE_ID,90enabled: true,91checked: false,92class: 'searchable-picker-see-more',93description: undefined,94tooltip: localize('seeMore.tooltip', "Search for more options"),95label: localize('seeMore', "See more..."),96run: () => {97this.showSearchableQuickPick(optionGroup);98}99} satisfies IActionWidgetDropdownAction);100}101102return actions;103}104105protected override renderLabel(element: HTMLElement): IDisposable | null {106const domChildren = [];107const optionGroup = this.delegate.getOptionGroup();108109element.classList.add('chat-session-option-picker');110111if (optionGroup?.icon) {112domChildren.push(renderIcon(optionGroup.icon));113}114115// Label116const label = this.currentOption?.name ?? optionGroup?.name ?? localize('selectOption', "Select...");117domChildren.push(dom.$('span.chat-session-option-label', undefined, label));118119domChildren.push(...renderLabelWithIcons(`$(chevron-down)`));120121dom.reset(element, ...domChildren);122this.setAriaLabelAttributes(element);123return null;124}125126protected override getContainerClass(): string {127return 'chat-searchable-option-picker-item';128}129130/**131* Shows the full searchable QuickPick with all items (initial + search results)132* Called when user clicks "See more..." from the dropdown133*/134private async showSearchableQuickPick(optionGroup: IChatSessionProviderOptionGroup): Promise<void> {135if (optionGroup.onSearch) {136const disposables = new DisposableStore();137const quickPick = this.quickInputService.createQuickPick<ISearchableOptionQuickPickItem>();138disposables.add(quickPick);139quickPick.placeholder = optionGroup.description ?? localize('selectOption.placeholder', "Select {0}", optionGroup.name);140quickPick.matchOnDescription = true;141quickPick.matchOnDetail = true;142quickPick.ignoreFocusOut = true;143quickPick.busy = true;144quickPick.show();145146// Debounced search state147let currentSearchCts: CancellationTokenSource | undefined;148const searchDelayer = disposables.add(new Delayer<void>(300));149150const performSearch = async (query: string) => {151// Cancel previous search152currentSearchCts?.cancel();153currentSearchCts?.dispose();154currentSearchCts = new CancellationTokenSource();155const token = currentSearchCts.token;156157quickPick.busy = true;158try {159const items = await optionGroup.onSearch!(query, token);160if (!token.isCancellationRequested) {161quickPick.items = items.map(item => this.createQuickPickItem(item));162}163} catch (error) {164if (!token.isCancellationRequested) {165this.logService.error('Error fetching searchable option items:', error);166}167} finally {168if (!token.isCancellationRequested) {169quickPick.busy = false;170}171}172};173174// Initial search with empty query175await performSearch('');176177// Listen for value changes and perform debounced search178disposables.add(quickPick.onDidChangeValue(value => {179searchDelayer.trigger(() => performSearch(value));180}));181182183// Handle selection184return new Promise<void>((resolve) => {185disposables.add(quickPick.onDidAccept(() => {186const pick = quickPick.selectedItems[0];187if (isSearchableOptionQuickPickItem(pick)) {188const selectedItem = pick.optionItem;189if (!selectedItem.locked) {190this.delegate.setOption(selectedItem);191}192}193quickPick.hide();194}));195196disposables.add(quickPick.onDidHide(() => {197currentSearchCts?.cancel();198currentSearchCts?.dispose();199disposables.dispose();200resolve();201}));202});203}204}205206private createQuickPickItem(207item: IChatSessionProviderOptionItem,208): ISearchableOptionQuickPickItem {209const iconClass = item.icon ? ThemeIcon.asClassName(item.icon) : undefined;210211return {212label: item.name,213description: item.description,214iconClass,215disabled: item.locked,216optionItem: item,217};218}219220/**221* Opens the picker programmatically.222*/223override show(): void {224const optionGroup = this.delegate.getOptionGroup();225if (optionGroup) {226this.showSearchableQuickPick(optionGroup);227}228}229}230231232