Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/modelPicker.ts
13401 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 * as dom from '../../../../base/browser/dom.js';
7
import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';
8
import { Codicon } from '../../../../base/common/codicons.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
11
import { autorun } from '../../../../base/common/observable.js';
12
import { localize } from '../../../../nls.js';
13
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
14
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';
15
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
16
import { IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
17
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
18
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
19
import { CopilotChatSessionsProvider, RemoteNewSession } from './copilotChatSessionsProvider.js';
20
21
const FILTER_THRESHOLD = 10;
22
23
interface IModelItem {
24
readonly id: string;
25
readonly name: string;
26
readonly description?: string;
27
}
28
29
/**
30
* A self-contained widget for selecting a model in cloud sessions.
31
* Reads the model option group from the {@link RemoteNewSession} and
32
* renders an action list dropdown with the available models.
33
*/
34
export class CloudModelPicker extends Disposable {
35
36
private readonly _onDidChange = this._register(new Emitter<IChatSessionProviderOptionItem>());
37
readonly onDidChange: Event<IChatSessionProviderOptionItem> = this._onDidChange.event;
38
39
private _triggerElement: HTMLElement | undefined;
40
private _slotElement: HTMLElement | undefined;
41
private readonly _renderDisposables = this._register(new DisposableStore());
42
private readonly _sessionDisposables = this._register(new DisposableStore());
43
44
private _session: RemoteNewSession | undefined;
45
private _selectedModel: IModelItem | undefined;
46
private _models: IModelItem[] = [];
47
48
get selectedModel(): IModelItem | undefined {
49
return this._selectedModel;
50
}
51
52
constructor(
53
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
54
@ISessionsManagementService sessionsManagementService: ISessionsManagementService,
55
@ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService,
56
@IChatSessionsService chatSessionsService: IChatSessionsService,
57
) {
58
super();
59
60
this._register(autorun(reader => {
61
const session = sessionsManagementService.activeSession.read(reader);
62
const provider = session ? sessionsProvidersService.getProvider(session.providerId) : undefined;
63
const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;
64
if (providerSession instanceof RemoteNewSession) {
65
this._setSession(providerSession);
66
}
67
}));
68
69
// Also listen directly for option group changes from the extension host,
70
// in case they arrive before the RemoteNewSession relays the event.
71
this._register(chatSessionsService.onDidChangeOptionGroups(() => {
72
if (this._session) {
73
this._loadModels(this._session);
74
}
75
}));
76
}
77
78
private _setSession(session: RemoteNewSession): void {
79
this._session = session;
80
this._sessionDisposables.clear();
81
this._loadModels(session);
82
83
// Sync selected model to the new session
84
if (this._selectedModel) {
85
session.setModelId(this._selectedModel.id);
86
session.setOptionValue('models', { id: this._selectedModel.id, name: this._selectedModel.name });
87
}
88
89
// Re-load models when option groups change
90
this._sessionDisposables.add(session.onDidChangeOptionGroups(() => {
91
this._loadModels(session);
92
}));
93
}
94
95
/**
96
* Renders the model picker trigger button into the given container.
97
*/
98
render(container: HTMLElement): HTMLElement {
99
this._renderDisposables.clear();
100
101
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
102
this._slotElement = slot;
103
this._renderDisposables.add({ dispose: () => slot.remove() });
104
105
const trigger = dom.append(slot, dom.$('a.action-label'));
106
trigger.tabIndex = 0;
107
trigger.role = 'button';
108
this._triggerElement = trigger;
109
110
this._updateTriggerLabel();
111
112
this._renderDisposables.add(Gesture.addTarget(trigger));
113
for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {
114
this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {
115
dom.EventHelper.stop(e, true);
116
this._showPicker();
117
}));
118
}
119
120
this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {
121
if (e.key === 'Enter' || e.key === ' ') {
122
dom.EventHelper.stop(e, true);
123
this._showPicker();
124
}
125
}));
126
127
return slot;
128
}
129
130
private _loadModels(session: RemoteNewSession): void {
131
const modelOption = session.getModelOptionGroup();
132
if (modelOption?.group.items.length) {
133
this._models = modelOption.group.items.map(item => ({
134
id: item.id,
135
name: item.name,
136
description: item.description,
137
}));
138
139
// Select the session's current value, or the default, or the first
140
if (!this._selectedModel || !this._models.some(m => m.id === this._selectedModel!.id)) {
141
const value = modelOption.value;
142
this._selectedModel = value
143
? { id: value.id, name: value.name, description: value.description }
144
: this._models[0];
145
}
146
} else {
147
this._models = [];
148
}
149
this._updateTriggerLabel();
150
}
151
152
private _showPicker(): void {
153
if (!this._triggerElement || this.actionWidgetService.isVisible || this._models.length === 0) {
154
return;
155
}
156
157
const items = this._buildItems();
158
const showFilter = items.filter(i => i.kind === ActionListItemKind.Action).length > FILTER_THRESHOLD;
159
160
const triggerElement = this._triggerElement;
161
const delegate: IActionListDelegate<IModelItem> = {
162
onSelect: (item) => {
163
this.actionWidgetService.hide();
164
this._selectModel(item);
165
},
166
onHide: () => { triggerElement.focus(); },
167
};
168
169
this.actionWidgetService.show<IModelItem>(
170
'remoteModelPicker',
171
false,
172
items,
173
delegate,
174
this._triggerElement,
175
undefined,
176
[],
177
{
178
getAriaLabel: (item) => item.label ?? '',
179
getWidgetAriaLabel: () => localize('modelPicker.ariaLabel', "Model Picker"),
180
},
181
showFilter ? { showFilter: true, filterPlaceholder: localize('modelPicker.filter', "Filter models...") } : undefined,
182
);
183
}
184
185
private _buildItems(): IActionListItem<IModelItem>[] {
186
return this._models.map(model => ({
187
kind: ActionListItemKind.Action,
188
label: model.name,
189
group: { title: '', icon: this._selectedModel?.id === model.id ? Codicon.check : Codicon.blank },
190
item: model,
191
}));
192
}
193
194
private _selectModel(item: IModelItem): void {
195
this._selectedModel = item;
196
this._updateTriggerLabel();
197
198
if (this._session) {
199
this._session.setModelId(item.id);
200
this._session.setOptionValue('models', { id: item.id, name: item.name });
201
}
202
this._onDidChange.fire({ id: item.id, name: item.name, description: item.description });
203
}
204
205
private _updateTriggerLabel(): void {
206
if (!this._triggerElement) {
207
return;
208
}
209
210
dom.clearNode(this._triggerElement);
211
const label = this._selectedModel?.name ?? localize('modelPicker.auto', "Auto");
212
213
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
214
labelSpan.textContent = label;
215
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
216
217
this._triggerElement.ariaLabel = localize('modelPicker.triggerAriaLabel', "Pick Model, {0}", label);
218
219
this._slotElement?.classList.toggle('disabled', this._models.length === 0);
220
this._triggerElement.setAttribute('aria-disabled', String(this._models.length === 0));
221
}
222
}
223
224