Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/modePicker.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 { localize } from '../../../../nls.js';
12
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
13
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';
14
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
15
import { ChatMode, IChatMode, IChatModeService } from '../../../../workbench/contrib/chat/common/chatModes.js';
16
import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
17
import { ICommandService } from '../../../../platform/commands/common/commands.js';
18
import { Target } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
19
import { AICustomizationManagementCommands } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';
20
import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
21
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
22
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
23
import { CopilotChatSessionsProvider } from './copilotChatSessionsProvider.js';
24
import { CopilotCLISessionType } from '../../../services/sessions/common/session.js';
25
26
interface IModePickerItem {
27
readonly kind: 'mode';
28
readonly mode: IChatMode;
29
}
30
31
interface IConfigurePickerItem {
32
readonly kind: 'configure';
33
}
34
35
type ModePickerItem = IModePickerItem | IConfigurePickerItem;
36
37
/**
38
* A self-contained widget for selecting a chat mode (Agent, custom agents)
39
* for local/Background sessions. Shows only modes whose target matches
40
* the Background session type's customAgentTarget.
41
*/
42
export class ModePicker extends Disposable {
43
44
private readonly _onDidChange = this._register(new Emitter<IChatMode>());
45
readonly onDidChange: Event<IChatMode> = this._onDidChange.event;
46
47
private _triggerElement: HTMLElement | undefined;
48
private readonly _renderDisposables = this._register(new DisposableStore());
49
50
private _selectedMode: IChatMode = ChatMode.Agent;
51
52
get selectedMode(): IChatMode {
53
return this._selectedMode;
54
}
55
56
constructor(
57
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
58
@IChatModeService private readonly chatModeService: IChatModeService,
59
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
60
@ICommandService private readonly commandService: ICommandService,
61
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
62
@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,
63
) {
64
super();
65
66
this._register(this.chatModeService.onDidChangeChatModes(() => {
67
// Refresh the trigger label when available chat modes change
68
if (this._triggerElement) {
69
this._updateTriggerLabel();
70
}
71
}));
72
}
73
74
/**
75
* Resets the selected mode back to the default Agent mode.
76
*/
77
reset(): void {
78
this._selectedMode = ChatMode.Agent;
79
this._updateTriggerLabel();
80
}
81
82
/**
83
* Renders the mode picker trigger button into the given container.
84
*/
85
render(container: HTMLElement): HTMLElement {
86
this._renderDisposables.clear();
87
88
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
89
this._renderDisposables.add({ dispose: () => slot.remove() });
90
91
const trigger = dom.append(slot, dom.$('a.action-label'));
92
trigger.tabIndex = 0;
93
trigger.role = 'button';
94
this._triggerElement = trigger;
95
96
this._updateTriggerLabel();
97
98
this._renderDisposables.add(Gesture.addTarget(trigger));
99
for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {
100
this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {
101
dom.EventHelper.stop(e, true);
102
this._showPicker();
103
}));
104
}
105
106
this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {
107
if (e.key === 'Enter' || e.key === ' ') {
108
dom.EventHelper.stop(e, true);
109
this._showPicker();
110
}
111
}));
112
113
return slot;
114
}
115
116
private _getAvailableModes(): IChatMode[] {
117
const customAgentTarget = this.chatSessionsService.getCustomAgentTargetForSessionType(CopilotCLISessionType.id);
118
const effectiveTarget = customAgentTarget && customAgentTarget !== Target.Undefined ? customAgentTarget : Target.GitHubCopilot;
119
const modes = this.chatModeService.getModes();
120
121
// Always include the default Agent mode
122
const result: IChatMode[] = [ChatMode.Agent];
123
124
// Add custom modes matching the target and visible to users
125
for (const mode of modes.custom) {
126
const target = mode.target.get();
127
if (target === effectiveTarget || target === Target.Undefined) {
128
const visibility = mode.visibility?.get();
129
if (visibility && !visibility.userInvocable) {
130
continue;
131
}
132
result.push(mode);
133
}
134
}
135
136
return result;
137
}
138
139
private _showPicker(): void {
140
if (!this._triggerElement || this.actionWidgetService.isVisible) {
141
return;
142
}
143
144
const modes = this._getAvailableModes();
145
146
const items = this._buildItems(modes);
147
148
const triggerElement = this._triggerElement;
149
const delegate: IActionListDelegate<ModePickerItem> = {
150
onSelect: (item) => {
151
this.actionWidgetService.hide();
152
if (item.kind === 'mode') {
153
this._selectMode(item.mode);
154
} else {
155
this.commandService.executeCommand(AICustomizationManagementCommands.OpenEditor, AICustomizationManagementSection.Agents);
156
}
157
},
158
onHide: () => { triggerElement.focus(); },
159
};
160
161
this.actionWidgetService.show<ModePickerItem>(
162
'localModePicker',
163
false,
164
items,
165
delegate,
166
this._triggerElement,
167
undefined,
168
[],
169
{
170
getAriaLabel: (item) => item.label ?? '',
171
getWidgetAriaLabel: () => localize('modePicker.ariaLabel', "Mode Picker"),
172
},
173
);
174
}
175
176
private _buildItems(modes: IChatMode[]): IActionListItem<ModePickerItem>[] {
177
const items: IActionListItem<ModePickerItem>[] = [];
178
179
// Default Agent mode
180
const agentMode = modes[0];
181
items.push({
182
kind: ActionListItemKind.Action,
183
label: agentMode.label.get(),
184
group: { title: '', icon: this._selectedMode.id === agentMode.id ? Codicon.check : Codicon.blank },
185
item: { kind: 'mode', mode: agentMode },
186
});
187
188
// Custom modes (with separator if any exist)
189
const customModes = modes.slice(1);
190
if (customModes.length > 0) {
191
items.push({ kind: ActionListItemKind.Separator, label: '' });
192
for (const mode of customModes) {
193
items.push({
194
kind: ActionListItemKind.Action,
195
label: mode.label.get(),
196
group: { title: '', icon: this._selectedMode.id === mode.id ? Codicon.check : Codicon.blank },
197
item: { kind: 'mode', mode },
198
});
199
}
200
}
201
202
// Configure Custom Agents action
203
items.push({ kind: ActionListItemKind.Separator, label: '' });
204
items.push({
205
kind: ActionListItemKind.Action,
206
label: localize('configureCustomAgents', "Configure Custom Agents..."),
207
group: { title: '', icon: Codicon.blank },
208
item: { kind: 'configure' },
209
});
210
211
return items;
212
}
213
214
private _selectMode(mode: IChatMode): void {
215
this._selectedMode = mode;
216
this._updateTriggerLabel();
217
this._onDidChange.fire(mode);
218
219
const session = this.sessionsManagementService.activeSession.get();
220
if (!session) {
221
return;
222
}
223
224
const provider = this.sessionsProvidersService.getProvider(session.providerId);
225
if (provider instanceof CopilotChatSessionsProvider) {
226
provider.getSession(session.sessionId)?.setMode(mode);
227
}
228
}
229
230
private _updateTriggerLabel(): void {
231
if (!this._triggerElement) {
232
return;
233
}
234
235
dom.clearNode(this._triggerElement);
236
237
const icon = this._selectedMode.icon.get();
238
if (icon) {
239
dom.append(this._triggerElement, renderIcon(icon));
240
}
241
242
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
243
labelSpan.textContent = this._selectedMode.label.get();
244
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
245
246
this._triggerElement.ariaLabel = localize('modePicker.triggerAriaLabel', "Pick Mode, {0}", this._selectedMode.label.get());
247
}
248
}
249
250