Path: blob/main/src/vs/sessions/contrib/chat/browser/agentHost/agentHostModePicker.ts
13405 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 * as dom from '../../../../../base/browser/dom.js';6import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js';7import { Gesture, EventType as TouchEventType } from '../../../../../base/browser/touch.js';8import { Codicon } from '../../../../../base/common/codicons.js';9import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js';10import { autorun } from '../../../../../base/common/observable.js';11import { ThemeIcon } from '../../../../../base/common/themables.js';12import { localize } from '../../../../../nls.js';13import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js';14import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';15import { SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js';16import { type IAgentHostSessionsProvider, isAgentHostProvider } from '../../../../common/agentHostSessionsProvider.js';17import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';18import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';19import { type ISessionsProvider } from '../../../../services/sessions/common/sessionsProvider.js';20import { isWellKnownModeSchema } from './agentHostPermissionPickerDelegate.js';2122interface IModePickerItem {23readonly value: string;24readonly label: string;25readonly description?: string;26}2728function getModeIcon(value: string | undefined): ThemeIcon | undefined {29switch (value) {30case 'plan': return Codicon.checklist;31case 'autopilot': return Codicon.rocket;32case 'interactive': return Codicon.comment;33default: return undefined;34}35}3637/**38* Self-contained picker widget for the agent-host `mode` session-config39* property (`interactive` / `plan` / `autopilot`).40*41* Mirrors the existing default-Copilot mode picker UX but is backed by the42* active agent-host session's resolved config via43* {@link IAgentHostSessionsProvider}. Renders nothing when the active44* session does not advertise a {@link isWellKnownModeSchema well-known}45* mode schema, so the generic per-property46* {@link AgentHostSessionConfigPicker} can take over for non-conforming47* agents.48*/49export class AgentHostModePicker extends Disposable {5051private readonly _renderDisposables = this._register(new DisposableStore());52private readonly _providerListeners = this._register(new DisposableMap<string>());53private _slotElement: HTMLElement | undefined;54private _triggerElement: HTMLElement | undefined;5556constructor(57@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,58@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,59@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,60) {61super();6263this._register(autorun(reader => {64this._sessionsManagementService.activeSession.read(reader);65this._updateTrigger();66}));6768this._register(this._sessionsProvidersService.onDidChangeProviders(e => {69for (const provider of e.removed) {70this._providerListeners.deleteAndDispose(provider.id);71}72this._watchProviders(e.added);73this._updateTrigger();74}));75this._watchProviders(this._sessionsProvidersService.getProviders());76}7778render(container: HTMLElement): void {79this._renderDisposables.clear();8081const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));82this._renderDisposables.add({ dispose: () => slot.remove() });83this._slotElement = slot;8485const trigger = dom.append(slot, dom.$('a.action-label'));86trigger.tabIndex = 0;87trigger.role = 'button';88this._triggerElement = trigger;8990this._renderDisposables.add(Gesture.addTarget(trigger));91for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {92this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, e => {93dom.EventHelper.stop(e, true);94this._showPicker();95}));96}9798this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, e => {99if (e.key === 'Enter' || e.key === ' ') {100dom.EventHelper.stop(e, true);101this._showPicker();102}103}));104105this._updateTrigger();106}107108private _watchProviders(providers: readonly ISessionsProvider[]): void {109for (const provider of providers) {110if (!isAgentHostProvider(provider) || this._providerListeners.has(provider.id)) {111continue;112}113this._providerListeners.set(provider.id, provider.onDidChangeSessionConfig(() => this._updateTrigger()));114}115}116117private _getActiveContext(): { provider: IAgentHostSessionsProvider; sessionId: string; currentValue: string; items: readonly IModePickerItem[] } | undefined {118const session = this._sessionsManagementService.activeSession.get();119if (!session) {120return undefined;121}122const rawProvider = this._sessionsProvidersService.getProvider(session.providerId);123if (!rawProvider || !isAgentHostProvider(rawProvider)) {124return undefined;125}126const config = rawProvider.getSessionConfig(session.sessionId);127const schema = config?.schema.properties[SessionConfigKey.Mode];128if (!schema || !isWellKnownModeSchema(schema)) {129return undefined;130}131const enumValues = schema.enum ?? [];132const enumLabels = schema.enumLabels ?? [];133const enumDescriptions = schema.enumDescriptions ?? [];134const items: IModePickerItem[] = enumValues.map((value, index) => ({135value,136label: enumLabels[index] ?? value,137description: enumDescriptions[index],138}));139const rawCurrent = config?.values[SessionConfigKey.Mode] ?? schema.default;140const currentValue = typeof rawCurrent === 'string' && enumValues.includes(rawCurrent) ? rawCurrent : enumValues[0] ?? '';141return { provider: rawProvider, sessionId: session.sessionId, currentValue, items };142}143144private _updateTrigger(): void {145if (!this._triggerElement || !this._slotElement) {146return;147}148149const ctx = this._getActiveContext();150if (!ctx) {151this._slotElement.style.display = 'none';152return;153}154this._slotElement.style.display = '';155156dom.clearNode(this._triggerElement);157158const item = ctx.items.find(i => i.value === ctx.currentValue);159const label = item?.label ?? ctx.currentValue;160161const icon = getModeIcon(ctx.currentValue);162if (icon) {163dom.append(this._triggerElement, renderIcon(icon));164}165166const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));167labelSpan.textContent = label;168dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));169170this._triggerElement.ariaLabel = localize('agentHostModePicker.triggerAriaLabel', "Pick Agent Mode, {0}", label);171}172173private _showPicker(): void {174if (!this._triggerElement || this._actionWidgetService.isVisible) {175return;176}177const ctx = this._getActiveContext();178if (!ctx) {179return;180}181182const triggerElement = this._triggerElement;183const actionItems: IActionListItem<IModePickerItem>[] = ctx.items.map(item => ({184kind: ActionListItemKind.Action,185label: item.label,186description: item.description,187group: { title: '', icon: item.value === ctx.currentValue ? Codicon.check : Codicon.blank },188item,189}));190191const delegate: IActionListDelegate<IModePickerItem> = {192onSelect: item => {193this._actionWidgetService.hide();194ctx.provider.setSessionConfigValue(ctx.sessionId, SessionConfigKey.Mode, item.value)195.catch(() => { /* best-effort */ });196},197onHide: () => triggerElement.focus(),198};199200this._actionWidgetService.show<IModePickerItem>(201'agentHostModePicker',202false,203actionItems,204delegate,205this._triggerElement,206undefined,207[],208{209getAriaLabel: i => i.label ?? '',210getWidgetAriaLabel: () => localize('agentHostModePicker.ariaLabel', "Agent Mode Picker"),211},212);213}214}215216217