Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/modePicker.ts
13401 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 { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';7import { Codicon } from '../../../../base/common/codicons.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';10import { localize } from '../../../../nls.js';11import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';12import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';13import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';14import { ChatMode, IChatMode, IChatModeService } from '../../../../workbench/contrib/chat/common/chatModes.js';15import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';16import { ICommandService } from '../../../../platform/commands/common/commands.js';17import { Target } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';18import { AICustomizationManagementCommands } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';19import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';20import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';21import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';22import { CopilotChatSessionsProvider } from './copilotChatSessionsProvider.js';23import { CopilotCLISessionType } from '../../../services/sessions/common/session.js';2425interface IModePickerItem {26readonly kind: 'mode';27readonly mode: IChatMode;28}2930interface IConfigurePickerItem {31readonly kind: 'configure';32}3334type ModePickerItem = IModePickerItem | IConfigurePickerItem;3536/**37* A self-contained widget for selecting a chat mode (Agent, custom agents)38* for local/Background sessions. Shows only modes whose target matches39* the Background session type's customAgentTarget.40*/41export class ModePicker extends Disposable {4243private readonly _onDidChange = this._register(new Emitter<IChatMode>());44readonly onDidChange: Event<IChatMode> = this._onDidChange.event;4546private _triggerElement: HTMLElement | undefined;47private readonly _renderDisposables = this._register(new DisposableStore());4849private _selectedMode: IChatMode = ChatMode.Agent;5051get selectedMode(): IChatMode {52return this._selectedMode;53}5455constructor(56@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,57@IChatModeService private readonly chatModeService: IChatModeService,58@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,59@ICommandService private readonly commandService: ICommandService,60@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,61@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,62) {63super();6465this._register(this.chatModeService.onDidChangeChatModes(() => {66// Refresh the trigger label when available chat modes change67if (this._triggerElement) {68this._updateTriggerLabel();69}70}));71}7273/**74* Resets the selected mode back to the default Agent mode.75*/76reset(): void {77this._selectedMode = ChatMode.Agent;78this._updateTriggerLabel();79}8081/**82* Renders the mode picker trigger button into the given container.83*/84render(container: HTMLElement): HTMLElement {85this._renderDisposables.clear();8687const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));88this._renderDisposables.add({ dispose: () => slot.remove() });8990const trigger = dom.append(slot, dom.$('a.action-label'));91trigger.tabIndex = 0;92trigger.role = 'button';93this._triggerElement = trigger;9495this._updateTriggerLabel();9697this._renderDisposables.add(Gesture.addTarget(trigger));98for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {99this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {100dom.EventHelper.stop(e, true);101this._showPicker();102}));103}104105this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {106if (e.key === 'Enter' || e.key === ' ') {107dom.EventHelper.stop(e, true);108this._showPicker();109}110}));111112return slot;113}114115private _getAvailableModes(): IChatMode[] {116const customAgentTarget = this.chatSessionsService.getCustomAgentTargetForSessionType(CopilotCLISessionType.id);117const effectiveTarget = customAgentTarget && customAgentTarget !== Target.Undefined ? customAgentTarget : Target.GitHubCopilot;118const modes = this.chatModeService.getModes();119120// Always include the default Agent mode121const result: IChatMode[] = [ChatMode.Agent];122123// Add custom modes matching the target and visible to users124for (const mode of modes.custom) {125const target = mode.target.get();126if (target === effectiveTarget || target === Target.Undefined) {127const visibility = mode.visibility?.get();128if (visibility && !visibility.userInvocable) {129continue;130}131result.push(mode);132}133}134135return result;136}137138private _showPicker(): void {139if (!this._triggerElement || this.actionWidgetService.isVisible) {140return;141}142143const modes = this._getAvailableModes();144145const items = this._buildItems(modes);146147const triggerElement = this._triggerElement;148const delegate: IActionListDelegate<ModePickerItem> = {149onSelect: (item) => {150this.actionWidgetService.hide();151if (item.kind === 'mode') {152this._selectMode(item.mode);153} else {154this.commandService.executeCommand(AICustomizationManagementCommands.OpenEditor, AICustomizationManagementSection.Agents);155}156},157onHide: () => { triggerElement.focus(); },158};159160this.actionWidgetService.show<ModePickerItem>(161'localModePicker',162false,163items,164delegate,165this._triggerElement,166undefined,167[],168{169getAriaLabel: (item) => item.label ?? '',170getWidgetAriaLabel: () => localize('modePicker.ariaLabel', "Mode Picker"),171},172);173}174175private _buildItems(modes: IChatMode[]): IActionListItem<ModePickerItem>[] {176const items: IActionListItem<ModePickerItem>[] = [];177178// Default Agent mode179const agentMode = modes[0];180items.push({181kind: ActionListItemKind.Action,182label: agentMode.label.get(),183group: { title: '', icon: this._selectedMode.id === agentMode.id ? Codicon.check : Codicon.blank },184item: { kind: 'mode', mode: agentMode },185});186187// Custom modes (with separator if any exist)188const customModes = modes.slice(1);189if (customModes.length > 0) {190items.push({ kind: ActionListItemKind.Separator, label: '' });191for (const mode of customModes) {192items.push({193kind: ActionListItemKind.Action,194label: mode.label.get(),195group: { title: '', icon: this._selectedMode.id === mode.id ? Codicon.check : Codicon.blank },196item: { kind: 'mode', mode },197});198}199}200201// Configure Custom Agents action202items.push({ kind: ActionListItemKind.Separator, label: '' });203items.push({204kind: ActionListItemKind.Action,205label: localize('configureCustomAgents', "Configure Custom Agents..."),206group: { title: '', icon: Codicon.blank },207item: { kind: 'configure' },208});209210return items;211}212213private _selectMode(mode: IChatMode): void {214this._selectedMode = mode;215this._updateTriggerLabel();216this._onDidChange.fire(mode);217218const session = this.sessionsManagementService.activeSession.get();219if (!session) {220return;221}222223const provider = this.sessionsProvidersService.getProvider(session.providerId);224if (provider instanceof CopilotChatSessionsProvider) {225provider.getSession(session.sessionId)?.setMode(mode);226}227}228229private _updateTriggerLabel(): void {230if (!this._triggerElement) {231return;232}233234dom.clearNode(this._triggerElement);235236const icon = this._selectedMode.icon.get();237if (icon) {238dom.append(this._triggerElement, renderIcon(icon));239}240241const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));242labelSpan.textContent = this._selectedMode.label.get();243dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));244245this._triggerElement.ariaLabel = localize('modePicker.triggerAriaLabel', "Pick Mode, {0}", this._selectedMode.label.get());246}247}248249250