Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.ts
5289 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/chatSessionPickerActionItem.css';
7
import { IAction } from '../../../../../base/common/actions.js';
8
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
9
import { Delayer } from '../../../../../base/common/async.js';
10
import * as dom from '../../../../../base/browser/dom.js';
11
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
12
import { IActionWidgetDropdownAction } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
13
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
14
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
15
import { IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../common/chatSessionsService.js';
16
import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
17
import { renderLabelWithIcons, renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';
18
import { localize } from '../../../../../nls.js';
19
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
20
import { ThemeIcon } from '../../../../../base/common/themables.js';
21
import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from './chatSessionPickerActionItem.js';
22
import { ILogService } from '../../../../../platform/log/common/log.js';
23
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
24
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
25
26
interface ISearchableOptionQuickPickItem extends IQuickPickItem {
27
readonly optionItem: IChatSessionProviderOptionItem;
28
}
29
30
function isSearchableOptionQuickPickItem(item: IQuickPickItem | undefined): item is ISearchableOptionQuickPickItem {
31
return !!item && typeof (item as ISearchableOptionQuickPickItem).optionItem === 'object';
32
}
33
34
/**
35
* Action view item for searchable option groups with QuickPick.
36
* Used when an option group has `searchable: true` (e.g., repository selection).
37
* Shows an inline dropdown with items + "See more..." option that opens a searchable QuickPick.
38
*/
39
export class SearchableOptionPickerActionItem extends ChatSessionPickerActionItem {
40
private static readonly SEE_MORE_ID = '__see_more__';
41
42
constructor(
43
action: IAction,
44
initialState: { group: IChatSessionProviderOptionGroup; item: IChatSessionProviderOptionItem | undefined },
45
delegate: IChatSessionPickerDelegate,
46
@IActionWidgetService actionWidgetService: IActionWidgetService,
47
@IContextKeyService contextKeyService: IContextKeyService,
48
@IKeybindingService keybindingService: IKeybindingService,
49
@IQuickInputService private readonly quickInputService: IQuickInputService,
50
@ILogService private readonly logService: ILogService,
51
@ICommandService commandService: ICommandService,
52
@ITelemetryService telemetryService: ITelemetryService,
53
) {
54
super(action, initialState, delegate, actionWidgetService, contextKeyService, keybindingService, commandService, telemetryService);
55
}
56
57
protected override getDropdownActions(): IActionWidgetDropdownAction[] {
58
// If locked, show the current option only
59
const currentOption = this.delegate.getCurrentOption();
60
if (currentOption?.locked) {
61
return [this.createLockedOptionAction(currentOption)];
62
}
63
64
const optionGroup = this.delegate.getOptionGroup();
65
if (!optionGroup) {
66
return [];
67
}
68
69
// Build actions from items
70
const actions: IActionWidgetDropdownAction[] = optionGroup.items.map(optionItem => {
71
const isCurrent = optionItem.id === currentOption?.id;
72
return {
73
id: optionItem.id,
74
enabled: !optionItem.locked,
75
icon: optionItem.icon,
76
checked: isCurrent,
77
class: undefined,
78
description: optionItem.description,
79
tooltip: optionItem.description ?? optionItem.name,
80
label: optionItem.name,
81
run: () => {
82
this.delegate.setOption(optionItem);
83
}
84
};
85
});
86
87
// Add "See more..." action if onSearch is available
88
if (optionGroup.onSearch) {
89
actions.push({
90
id: SearchableOptionPickerActionItem.SEE_MORE_ID,
91
enabled: true,
92
checked: false,
93
class: 'searchable-picker-see-more',
94
description: undefined,
95
tooltip: localize('seeMore.tooltip', "Search for more options"),
96
label: localize('seeMore', "See more..."),
97
run: () => {
98
this.showSearchableQuickPick(optionGroup);
99
}
100
} satisfies IActionWidgetDropdownAction);
101
}
102
103
return actions;
104
}
105
106
protected override renderLabel(element: HTMLElement): IDisposable | null {
107
const domChildren = [];
108
const optionGroup = this.delegate.getOptionGroup();
109
110
element.classList.add('chat-session-option-picker');
111
112
if (optionGroup?.icon) {
113
domChildren.push(renderIcon(optionGroup.icon));
114
}
115
116
// Label
117
const label = this.currentOption?.name ?? optionGroup?.name ?? localize('selectOption', "Select...");
118
domChildren.push(dom.$('span.chat-session-option-label', undefined, label));
119
120
domChildren.push(...renderLabelWithIcons(`$(chevron-down)`));
121
122
dom.reset(element, ...domChildren);
123
this.setAriaLabelAttributes(element);
124
return null;
125
}
126
127
protected override getContainerClass(): string {
128
return 'chat-searchable-option-picker-item';
129
}
130
131
/**
132
* Shows the full searchable QuickPick with all items (initial + search results)
133
* Called when user clicks "See more..." from the dropdown
134
*/
135
private async showSearchableQuickPick(optionGroup: IChatSessionProviderOptionGroup): Promise<void> {
136
if (optionGroup.onSearch) {
137
const disposables = new DisposableStore();
138
const quickPick = this.quickInputService.createQuickPick<ISearchableOptionQuickPickItem>();
139
disposables.add(quickPick);
140
quickPick.placeholder = optionGroup.description ?? localize('selectOption.placeholder', "Select {0}", optionGroup.name);
141
quickPick.matchOnDescription = true;
142
quickPick.matchOnDetail = true;
143
quickPick.ignoreFocusOut = true;
144
quickPick.busy = true;
145
quickPick.show();
146
147
// Debounced search state
148
let currentSearchCts: CancellationTokenSource | undefined;
149
const searchDelayer = disposables.add(new Delayer<void>(300));
150
151
const performSearch = async (query: string) => {
152
// Cancel previous search
153
currentSearchCts?.cancel();
154
currentSearchCts?.dispose();
155
currentSearchCts = new CancellationTokenSource();
156
const token = currentSearchCts.token;
157
158
quickPick.busy = true;
159
try {
160
const items = await optionGroup.onSearch!(query, token);
161
if (!token.isCancellationRequested) {
162
quickPick.items = items.map(item => this.createQuickPickItem(item));
163
}
164
} catch (error) {
165
if (!token.isCancellationRequested) {
166
this.logService.error('Error fetching searchable option items:', error);
167
}
168
} finally {
169
if (!token.isCancellationRequested) {
170
quickPick.busy = false;
171
}
172
}
173
};
174
175
// Initial search with empty query
176
await performSearch('');
177
178
// Listen for value changes and perform debounced search
179
disposables.add(quickPick.onDidChangeValue(value => {
180
searchDelayer.trigger(() => performSearch(value));
181
}));
182
183
184
// Handle selection
185
return new Promise<void>((resolve) => {
186
disposables.add(quickPick.onDidAccept(() => {
187
const pick = quickPick.selectedItems[0];
188
if (isSearchableOptionQuickPickItem(pick)) {
189
const selectedItem = pick.optionItem;
190
if (!selectedItem.locked) {
191
this.delegate.setOption(selectedItem);
192
}
193
}
194
quickPick.hide();
195
}));
196
197
disposables.add(quickPick.onDidHide(() => {
198
currentSearchCts?.cancel();
199
currentSearchCts?.dispose();
200
disposables.dispose();
201
resolve();
202
}));
203
});
204
}
205
}
206
207
private createQuickPickItem(
208
item: IChatSessionProviderOptionItem,
209
): ISearchableOptionQuickPickItem {
210
const iconClass = item.icon ? ThemeIcon.asClassName(item.icon) : undefined;
211
212
return {
213
label: item.name,
214
description: item.description,
215
iconClass,
216
disabled: item.locked,
217
optionItem: item,
218
};
219
}
220
221
/**
222
* Opens the picker programmatically.
223
*/
224
override show(): void {
225
const optionGroup = this.delegate.getOptionGroup();
226
if (optionGroup) {
227
this.showSearchableQuickPick(optionGroup);
228
}
229
}
230
}
231
232