Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/chat/browser/agentHost/agentHostModePicker.ts
13405 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 { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';
8
import { Gesture, EventType as TouchEventType } from '../../../../../base/browser/touch.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js';
11
import { autorun } from '../../../../../base/common/observable.js';
12
import { ThemeIcon } from '../../../../../base/common/themables.js';
13
import { localize } from '../../../../../nls.js';
14
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js';
15
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
16
import { SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js';
17
import { type IAgentHostSessionsProvider, isAgentHostProvider } from '../../../../common/agentHostSessionsProvider.js';
18
import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';
19
import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';
20
import { type ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js';
21
import { isWellKnownModeSchema } from './agentHostPermissionPickerDelegate.js';
22
23
interface IModePickerItem {
24
readonly value: string;
25
readonly label: string;
26
readonly description?: string;
27
}
28
29
function getModeIcon(value: string | undefined): ThemeIcon | undefined {
30
switch (value) {
31
case 'plan': return Codicon.checklist;
32
case 'autopilot': return Codicon.rocket;
33
case 'interactive': return Codicon.comment;
34
default: return undefined;
35
}
36
}
37
38
/**
39
* Self-contained picker widget for the agent-host `mode` session-config
40
* property (`interactive` / `plan` / `autopilot`).
41
*
42
* Mirrors the existing default-Copilot mode picker UX but is backed by the
43
* active agent-host session's resolved config via
44
* {@link IAgentHostSessionsProvider}. Renders nothing when the active
45
* session does not advertise a {@link isWellKnownModeSchema well-known}
46
* mode schema, so the generic per-property
47
* {@link AgentHostSessionConfigPicker} can take over for non-conforming
48
* agents.
49
*/
50
export class AgentHostModePicker extends Disposable {
51
52
private readonly _renderDisposables = this._register(new DisposableStore());
53
private readonly _providerListeners = this._register(new DisposableMap<string>());
54
private _slotElement: HTMLElement | undefined;
55
private _triggerElement: HTMLElement | undefined;
56
57
constructor(
58
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,
59
@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,
60
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
61
) {
62
super();
63
64
this._register(autorun(reader => {
65
this._sessionsManagementService.activeSession.read(reader);
66
this._updateTrigger();
67
}));
68
69
this._register(this._sessionsProvidersService.onDidChangeProviders(e => {
70
for (const provider of e.removed) {
71
this._providerListeners.deleteAndDispose(provider.id);
72
}
73
this._watchProviders(e.added);
74
this._updateTrigger();
75
}));
76
this._watchProviders(this._sessionsProvidersService.getProviders());
77
}
78
79
render(container: HTMLElement): void {
80
this._renderDisposables.clear();
81
82
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
83
this._renderDisposables.add({ dispose: () => slot.remove() });
84
this._slotElement = slot;
85
86
const trigger = dom.append(slot, dom.$('a.action-label'));
87
trigger.tabIndex = 0;
88
trigger.role = 'button';
89
this._triggerElement = trigger;
90
91
this._renderDisposables.add(Gesture.addTarget(trigger));
92
for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {
93
this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, e => {
94
dom.EventHelper.stop(e, true);
95
this._showPicker();
96
}));
97
}
98
99
this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, e => {
100
if (e.key === 'Enter' || e.key === ' ') {
101
dom.EventHelper.stop(e, true);
102
this._showPicker();
103
}
104
}));
105
106
this._updateTrigger();
107
}
108
109
private _watchProviders(providers: readonly ISessionsProvider[]): void {
110
for (const provider of providers) {
111
if (!isAgentHostProvider(provider) || this._providerListeners.has(provider.id)) {
112
continue;
113
}
114
this._providerListeners.set(provider.id, provider.onDidChangeSessionConfig(() => this._updateTrigger()));
115
}
116
}
117
118
private _getActiveContext(): { provider: IAgentHostSessionsProvider; sessionId: string; currentValue: string; items: readonly IModePickerItem[] } | undefined {
119
const session = this._sessionsManagementService.activeSession.get();
120
if (!session) {
121
return undefined;
122
}
123
const rawProvider = this._sessionsProvidersService.getProvider(session.providerId);
124
if (!rawProvider || !isAgentHostProvider(rawProvider)) {
125
return undefined;
126
}
127
const config = rawProvider.getSessionConfig(session.sessionId);
128
const schema = config?.schema.properties[SessionConfigKey.Mode];
129
if (!schema || !isWellKnownModeSchema(schema)) {
130
return undefined;
131
}
132
const enumValues = schema.enum ?? [];
133
const enumLabels = schema.enumLabels ?? [];
134
const enumDescriptions = schema.enumDescriptions ?? [];
135
const items: IModePickerItem[] = enumValues.map((value, index) => ({
136
value,
137
label: enumLabels[index] ?? value,
138
description: enumDescriptions[index],
139
}));
140
const rawCurrent = config?.values[SessionConfigKey.Mode] ?? schema.default;
141
const currentValue = typeof rawCurrent === 'string' && enumValues.includes(rawCurrent) ? rawCurrent : enumValues[0] ?? '';
142
return { provider: rawProvider, sessionId: session.sessionId, currentValue, items };
143
}
144
145
private _updateTrigger(): void {
146
if (!this._triggerElement || !this._slotElement) {
147
return;
148
}
149
150
const ctx = this._getActiveContext();
151
if (!ctx) {
152
this._slotElement.style.display = 'none';
153
return;
154
}
155
this._slotElement.style.display = '';
156
157
dom.clearNode(this._triggerElement);
158
159
const item = ctx.items.find(i => i.value === ctx.currentValue);
160
const label = item?.label ?? ctx.currentValue;
161
162
const icon = getModeIcon(ctx.currentValue);
163
if (icon) {
164
dom.append(this._triggerElement, renderIcon(icon));
165
}
166
167
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
168
labelSpan.textContent = label;
169
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
170
171
this._triggerElement.ariaLabel = localize('agentHostModePicker.triggerAriaLabel', "Pick Agent Mode, {0}", label);
172
}
173
174
private _showPicker(): void {
175
if (!this._triggerElement || this._actionWidgetService.isVisible) {
176
return;
177
}
178
const ctx = this._getActiveContext();
179
if (!ctx) {
180
return;
181
}
182
183
const triggerElement = this._triggerElement;
184
const actionItems: IActionListItem<IModePickerItem>[] = ctx.items.map(item => ({
185
kind: ActionListItemKind.Action,
186
label: item.label,
187
description: item.description,
188
group: { title: '', icon: item.value === ctx.currentValue ? Codicon.check : Codicon.blank },
189
item,
190
}));
191
192
const delegate: IActionListDelegate<IModePickerItem> = {
193
onSelect: item => {
194
this._actionWidgetService.hide();
195
ctx.provider.setSessionConfigValue(ctx.sessionId, SessionConfigKey.Mode, item.value)
196
.catch(() => { /* best-effort */ });
197
},
198
onHide: () => triggerElement.focus(),
199
};
200
201
this._actionWidgetService.show<IModePickerItem>(
202
'agentHostModePicker',
203
false,
204
actionItems,
205
delegate,
206
this._triggerElement,
207
undefined,
208
[],
209
{
210
getAriaLabel: i => i.label ?? '',
211
getWidgetAriaLabel: () => localize('agentHostModePicker.ariaLabel', "Agent Mode Picker"),
212
},
213
);
214
}
215
}
216
217