-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Mode picker in Sessions app #298199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Mode picker in Sessions app #298199
Changes from all commits
a1f1da5
c850537
5ff8465
f111e7a
7871ff8
d72a55a
cb8b60e
cb1ad72
bc86a2e
4a23c42
9ae8e51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,232 @@ | ||||||||
| /*--------------------------------------------------------------------------------------------- | ||||||||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||||||||
| *--------------------------------------------------------------------------------------------*/ | ||||||||
|
|
||||||||
| import * as dom from '../../../../base/browser/dom.js'; | ||||||||
| import { Codicon } from '../../../../base/common/codicons.js'; | ||||||||
| import { Emitter, Event } from '../../../../base/common/event.js'; | ||||||||
| import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; | ||||||||
| import { localize } from '../../../../nls.js'; | ||||||||
| import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; | ||||||||
| import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; | ||||||||
| import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; | ||||||||
| import { ChatMode, IChatMode, IChatModeService } from '../../../../workbench/contrib/chat/common/chatModes.js'; | ||||||||
| import { IGitRepository } from '../../../../workbench/contrib/git/common/gitService.js'; | ||||||||
| import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; | ||||||||
| import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; | ||||||||
| import { Target } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; | ||||||||
| import { ICommandService } from '../../../../platform/commands/common/commands.js'; | ||||||||
|
|
||||||||
| const CONFIGURE_AGENTS_ACTION_ID = 'workbench.action.chat.picker.customagents'; | ||||||||
|
|
||||||||
| interface IModePickerItem { | ||||||||
| readonly kind: 'mode'; | ||||||||
| readonly mode: IChatMode; | ||||||||
| } | ||||||||
|
|
||||||||
| interface IConfigurePickerItem { | ||||||||
| readonly kind: 'configure'; | ||||||||
| } | ||||||||
|
|
||||||||
| type ModePickerItem = IModePickerItem | IConfigurePickerItem; | ||||||||
|
|
||||||||
| /** | ||||||||
| * A self-contained widget for selecting a chat mode (Agent, custom agents) | ||||||||
| * for local/Background sessions. Shows only modes whose target matches | ||||||||
| * the Background session type's customAgentTarget. | ||||||||
| */ | ||||||||
| export class ModePicker extends Disposable { | ||||||||
|
|
||||||||
| private readonly _onDidChange = this._register(new Emitter<IChatMode>()); | ||||||||
| readonly onDidChange: Event<IChatMode> = this._onDidChange.event; | ||||||||
|
|
||||||||
| private _triggerElement: HTMLElement | undefined; | ||||||||
| private _slotElement: HTMLElement | undefined; | ||||||||
| private readonly _renderDisposables = this._register(new DisposableStore()); | ||||||||
|
|
||||||||
| private _selectedMode: IChatMode = ChatMode.Agent; | ||||||||
|
|
||||||||
| get selectedMode(): IChatMode { | ||||||||
| return this._selectedMode; | ||||||||
| } | ||||||||
|
|
||||||||
| constructor( | ||||||||
| @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, | ||||||||
| @IChatModeService private readonly chatModeService: IChatModeService, | ||||||||
| @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, | ||||||||
| @ICommandService private readonly commandService: ICommandService, | ||||||||
| ) { | ||||||||
| super(); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Sets the git repository. When the repository changes, resets the selected mode | ||||||||
| * back to the default Agent mode. | ||||||||
| */ | ||||||||
| setRepository(repository: IGitRepository | undefined): void { | ||||||||
| this._selectedMode = ChatMode.Agent; | ||||||||
| this._updateTriggerLabel(); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Renders the mode picker trigger button into the given container. | ||||||||
| */ | ||||||||
| render(container: HTMLElement): HTMLElement { | ||||||||
| this._renderDisposables.clear(); | ||||||||
|
|
||||||||
| const slot = dom.append(container, dom.$('.sessions-chat-picker-slot')); | ||||||||
| this._slotElement = slot; | ||||||||
| this._renderDisposables.add({ dispose: () => slot.remove() }); | ||||||||
|
|
||||||||
| const trigger = dom.append(slot, dom.$('a.action-label')); | ||||||||
| trigger.tabIndex = 0; | ||||||||
| trigger.role = 'button'; | ||||||||
|
||||||||
| trigger.role = 'button'; | |
| trigger.role = 'button'; | |
| trigger.setAttribute('aria-label', localize('sessions.modePicker.ariaLabel', "Select chat mode")); |
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mode filtering logic does not check the visibility.userInvocable property of custom modes. According to the codebase conventions (see chatModes.ts line 116 and line 162), modes with userInvocable set to false should not be shown in user-facing UI. The picker should filter out modes where visibility.userInvocable is false to prevent users from selecting modes that are only intended for agent invocation.
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The early return when modes.length is 0 prevents the picker from being shown, but this condition should never be true since _getAvailableModes always includes at least ChatMode.Agent (line 109). This check is defensive but may hide bugs where the default Agent mode is unexpectedly missing. Consider removing this check or adding logging to detect if this condition ever occurs.
| if (modes.length === 0) { | |
| return; | |
| } |
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ModePicker does not visually indicate when no custom modes are available, unlike the CloudModelPicker which adds a 'disabled' class (see modelPicker.ts line 202). While the picker will show only the default Agent mode, it would be better UX to add visual feedback when the picker is effectively showing only one option. Consider adding disabled styling when modes.length is 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ModePicker does not listen to changes in available modes. If custom agents are added, removed, or modified after the picker is created, the picker will not update its list of available modes until the user reopens the picker. Consider adding a listener to chatModeService.onDidChangeChatModes to refresh the available modes dynamically.