Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/permissionPicker.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 { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';9import { autorun, IObservable } from '../../../../base/common/observable.js';10import { localize } from '../../../../nls.js';11import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';12import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListOptions } from '../../../../platform/actionWidget/browser/actionList.js';13import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';14import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';15import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';16import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';17import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';18import { ThemeIcon } from '../../../../base/common/themables.js';19import { ChatConfiguration, ChatPermissionLevel, isChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js';20import Severity from '../../../../base/common/severity.js';21import { MarkdownString } from '../../../../base/common/htmlContent.js';22import { IOpenerService } from '../../../../platform/opener/common/opener.js';23import { URI } from '../../../../base/common/uri.js';24import { CopilotChatSessionsProvider } from '../../copilotChatSessions/browser/copilotChatSessionsProvider.js';25import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';2627const PERMISSION_LEVEL_OPTION_ID = 'permissionLevel';2829/**30* Strategy for the per-provider parts of {@link PermissionPicker}: how to read31* back the current level (if at all), whether the picker should be visible32* given the active session, and where to write the user's selection.33*34* Implementations live with the provider they back (e.g.35* {@link CopilotPermissionPickerDelegate} below for the default Copilot36* provider, or `AgentHostPermissionPickerDelegate` in the agent-host folder).37*/38export interface IPermissionPickerDelegate {39/**40* If provided, the picker's trigger label reactively tracks this. If41* omitted, the picker manages its own internal state and starts at42* {@link ChatPermissionLevel.Default}.43*/44readonly currentPermissionLevel?: IObservable<ChatPermissionLevel>;4546/**47* If provided, the picker hides itself when this is `false`. Used by48* delegates whose applicability depends on the active session.49*/50readonly isApplicable?: IObservable<boolean>;5152/**53* Called after the user selects a level (and any required confirmation54* dialog has been accepted).55*/56setPermissionLevel(level: ChatPermissionLevel): void;57}5859interface IPermissionItem {60readonly level?: ChatPermissionLevel;61readonly label: string;62readonly icon: ThemeIcon;63readonly checked: boolean;64}6566// Track whether warnings have been shown this VS Code session67const shownWarnings = new Set<ChatPermissionLevel>();6869export class PermissionPicker extends Disposable {7071private _currentLevel: ChatPermissionLevel = ChatPermissionLevel.Default;72private _triggerElement: HTMLElement | undefined;73private readonly _renderDisposables = this._register(new DisposableStore());7475constructor(76private readonly _delegate: IPermissionPickerDelegate,77@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,78@IConfigurationService private readonly configurationService: IConfigurationService,79@IDialogService private readonly dialogService: IDialogService,80@IOpenerService private readonly openerService: IOpenerService,81) {82super();83}8485render(container: HTMLElement): HTMLElement {86this._renderDisposables.clear();8788// Initialize the picker to reflect the configured default permission level89// (`chat.permissions.default`) whenever it is (re-)rendered. If enterprise90// policy disables global auto-approval, clamp to Default regardless of the91// configured default so we never show an elevated level the user can't pick.92const policyRestricted = this.configurationService.inspect<boolean>(ChatConfiguration.GlobalAutoApprove).policyValue === false;93const configuredDefault = this.configurationService.getValue<string>(ChatConfiguration.DefaultPermissionLevel);94const initialLevel = isChatPermissionLevel(configuredDefault) ? configuredDefault : ChatPermissionLevel.Default;95this._currentLevel = policyRestricted ? ChatPermissionLevel.Default : initialLevel;9697const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));98this._renderDisposables.add({ dispose: () => slot.remove() });99100const trigger = dom.append(slot, dom.$('a.action-label'));101trigger.tabIndex = 0;102trigger.role = 'button';103this._triggerElement = trigger;104105this._updateTriggerLabel(trigger);106107this._renderDisposables.add(Gesture.addTarget(trigger));108for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {109this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {110dom.EventHelper.stop(e, true);111this.showPicker();112}));113}114115this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {116if (e.key === 'Enter' || e.key === ' ') {117dom.EventHelper.stop(e, true);118this.showPicker();119}120}));121122const currentPermissionLevel = this._delegate.currentPermissionLevel;123if (currentPermissionLevel) {124this._renderDisposables.add(autorun(reader => {125this._currentLevel = currentPermissionLevel.read(reader);126this._updateTriggerLabel(trigger);127}));128}129130const isApplicable = this._delegate.isApplicable;131if (isApplicable) {132this._renderDisposables.add(autorun(reader => {133slot.style.display = isApplicable.read(reader) ? '' : 'none';134}));135}136137return slot;138}139140showPicker(): void {141if (!this._triggerElement || this.actionWidgetService.isVisible) {142return;143}144145const policyRestricted = this.configurationService.inspect<boolean>(ChatConfiguration.GlobalAutoApprove).policyValue === false;146const isAutopilotEnabled = this.configurationService.getValue<boolean>(ChatConfiguration.AutopilotEnabled) !== false;147148const items: IActionListItem<IPermissionItem>[] = [149{150kind: ActionListItemKind.Action,151group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.shield },152item: {153level: ChatPermissionLevel.Default,154label: localize('permissions.default', "Default Approvals"),155icon: Codicon.shield,156checked: this._currentLevel === ChatPermissionLevel.Default,157},158label: localize('permissions.default', "Default Approvals"),159detail: localize('permissions.default.subtext', "Copilot uses your configured settings"),160disabled: false,161},162{163kind: ActionListItemKind.Action,164group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.warning },165item: {166level: ChatPermissionLevel.AutoApprove,167label: localize('permissions.autoApprove', "Bypass Approvals"),168icon: Codicon.warning,169checked: this._currentLevel === ChatPermissionLevel.AutoApprove,170},171label: localize('permissions.autoApprove', "Bypass Approvals"),172detail: localize('permissions.autoApprove.subtext', "All tool calls are auto-approved"),173disabled: policyRestricted,174},175];176177if (isAutopilotEnabled) {178items.push({179kind: ActionListItemKind.Action,180group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.rocket },181item: {182level: ChatPermissionLevel.Autopilot,183label: localize('permissions.autopilot', "Autopilot (Preview)"),184icon: Codicon.rocket,185checked: this._currentLevel === ChatPermissionLevel.Autopilot,186},187label: localize('permissions.autopilot', "Autopilot (Preview)"),188detail: localize('permissions.autopilot.subtext', "Autonomously iterates from start to finish"),189disabled: policyRestricted,190});191}192193items.push({194kind: ActionListItemKind.Separator,195label: '',196disabled: false,197});198items.push({199kind: ActionListItemKind.Action,200group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.blank },201item: {202label: localize('permissions.learnMore', "Learn more about permissions"),203icon: Codicon.blank,204checked: false,205},206label: localize('permissions.learnMore', "Learn more about permissions"),207hideIcon: false,208disabled: false,209});210211const triggerElement = this._triggerElement;212const delegate: IActionListDelegate<IPermissionItem> = {213onSelect: async (item) => {214this.actionWidgetService.hide();215if (item.level) {216await this._selectLevel(item.level);217} else {218await this.openerService.open(URI.parse('https://code.visualstudio.com/docs/copilot/agents/agent-tools#_permission-levels'));219}220},221onHide: () => { triggerElement.focus(); },222};223224const listOptions: IActionListOptions = { minWidth: 255 };225this.actionWidgetService.show<IPermissionItem>(226'permissionPicker',227false,228items,229delegate,230this._triggerElement,231undefined,232[],233{234getWidgetAriaLabel: () => localize('permissionPicker.ariaLabel', "Permission Picker"),235},236listOptions,237);238}239240private async _selectLevel(level: ChatPermissionLevel): Promise<void> {241if (level === ChatPermissionLevel.AutoApprove && !shownWarnings.has(ChatPermissionLevel.AutoApprove)) {242const result = await this.dialogService.prompt({243type: Severity.Warning,244message: localize('permissions.autoApprove.warning.title', "Enable Bypass Approvals?"),245buttons: [246{247label: localize('permissions.autoApprove.warning.confirm', "Enable"),248run: () => true249},250{251label: localize('permissions.autoApprove.warning.cancel', "Cancel"),252run: () => false253},254],255custom: {256icon: Codicon.warning,257markdownDetails: [{258markdown: new MarkdownString(259localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.\n\nTo make this the starting permission level for new chat sessions, change the [{0}](command:workbench.action.openSettings?%5B%22{0}%22%5D) setting.", ChatConfiguration.DefaultPermissionLevel),260{ isTrusted: { enabledCommands: ['workbench.action.openSettings'] } },261),262}],263},264});265if (result.result !== true) {266return;267}268shownWarnings.add(ChatPermissionLevel.AutoApprove);269}270271if (level === ChatPermissionLevel.Autopilot && !shownWarnings.has(ChatPermissionLevel.Autopilot)) {272const result = await this.dialogService.prompt({273type: Severity.Warning,274message: localize('permissions.autopilot.warning.title', "Enable Autopilot?"),275buttons: [276{277label: localize('permissions.autopilot.warning.confirm', "Enable"),278run: () => true279},280{281label: localize('permissions.autopilot.warning.cancel', "Cancel"),282run: () => false283},284],285custom: {286icon: Codicon.rocket,287markdownDetails: [{288markdown: new MarkdownString(289localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.\n\nTo make this the starting permission level for new chat sessions, change the [{0}](command:workbench.action.openSettings?%5B%22{0}%22%5D) setting.", ChatConfiguration.DefaultPermissionLevel),290{ isTrusted: { enabledCommands: ['workbench.action.openSettings'] } },291),292}],293},294});295if (result.result !== true) {296return;297}298shownWarnings.add(ChatPermissionLevel.Autopilot);299}300301this._currentLevel = level;302this._updateTriggerLabel(this._triggerElement);303this._delegate.setPermissionLevel(level);304}305306private _updateTriggerLabel(trigger: HTMLElement | undefined): void {307if (!trigger) {308return;309}310311dom.clearNode(trigger);312let icon: ThemeIcon;313let label: string;314switch (this._currentLevel) {315case ChatPermissionLevel.Autopilot:316icon = Codicon.rocket;317label = localize('permissions.autopilot.label', "Autopilot (Preview)");318break;319case ChatPermissionLevel.AutoApprove:320icon = Codicon.warning;321label = localize('permissions.autoApprove.label', "Bypass Approvals");322break;323default:324icon = Codicon.shield;325label = localize('permissions.default.label', "Default Approvals");326break;327}328329dom.append(trigger, renderIcon(icon));330const labelSpan = dom.append(trigger, dom.$('span.sessions-chat-dropdown-label'));331labelSpan.textContent = label;332dom.append(trigger, renderIcon(Codicon.chevronDown));333334trigger.ariaLabel = localize('permissionPicker.triggerAriaLabel', "Pick Permission Level, {0}", label);335336trigger.classList.toggle('warning', this._currentLevel === ChatPermissionLevel.Autopilot);337trigger.classList.toggle('info', this._currentLevel === ChatPermissionLevel.AutoApprove);338}339}340341/**342* Default-Copilot {@link IPermissionPickerDelegate}: writes the user's chosen343* level back to the active {@link CopilotChatSessionsProvider} session.344*345* Does not provide `currentPermissionLevel` or `isApplicable`, so the picker346* manages its own state and is always visible (visibility is gated at the menu347* contribution level via `when` clauses).348*/349export class CopilotPermissionPickerDelegate extends Disposable implements IPermissionPickerDelegate {350constructor(351@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,352@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,353@IChatSessionsService private readonly _chatSessionsService: IChatSessionsService,354) {355super();356}357358setPermissionLevel(level: ChatPermissionLevel): void {359const session = this._sessionsManagementService.activeSession.get();360if (!session) {361return;362}363const provider = this._sessionsProvidersService.getProvider(session.providerId);364if (provider instanceof CopilotChatSessionsProvider) {365const chatSession = provider.getSession(session.sessionId);366if (!chatSession) {367return;368}369if (chatSession.setOption) {370chatSession.setPermissionLevel(level);371chatSession.setOption(PERMISSION_LEVEL_OPTION_ID, level);372} else {373this._chatSessionsService.setSessionOption(chatSession.resource, PERMISSION_LEVEL_OPTION_ID, level);374}375}376}377}378379380