Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/isolationPicker.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 } from '../../../../base/common/observable.js';10import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';11import { localize } from '../../../../nls.js';12import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';13import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';14import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';16import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';17import { CopilotChatSessionsProvider } from './copilotChatSessionsProvider.js';1819export type IsolationMode = 'worktree' | 'workspace';2021interface IIsolationPickerItem {22readonly mode: IsolationMode;23readonly checked?: boolean;24}2526/**27* A self-contained widget for selecting the isolation mode.28*29* Options:30* - **Worktree** (`worktree`) — run in a git worktree31* - **Folder** (`workspace`) — run directly in the folder32*33* Only visible when isolation option is enabled, project has a git repo,34* and the target is CLI.35*36* Emits `onDidChange` with the selected `IsolationMode` when the user picks an option.37*/38export class IsolationPicker extends Disposable {3940private _hasGitRepo = false;41private _isolationOptionEnabled: boolean;4243private readonly _renderDisposables = this._register(new DisposableStore());44private _slotElement: HTMLElement | undefined;45private _triggerElement: HTMLElement | undefined;4647constructor(48@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,49@IConfigurationService private readonly configurationService: IConfigurationService,50@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,51@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,52) {53super();54this._isolationOptionEnabled = this.configurationService.getValue<boolean>('github.copilot.chat.cli.isolationOption.enabled') !== false;5556this._register(this.configurationService.onDidChangeConfiguration(e => {57if (e.affectsConfiguration('github.copilot.chat.cli.isolationOption.enabled')) {58this._isolationOptionEnabled = this.configurationService.getValue<boolean>('github.copilot.chat.cli.isolationOption.enabled') !== false;59if (!this._isolationOptionEnabled) {60this._setModeOnSession('worktree');61}62this._updateTriggerLabel();63}64}));6566this._register(autorun(reader => {67const session = this.sessionsManagementService.activeSession.read(reader);68const isLoading = session?.loading.read(reader);69const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;70const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;71if (providerSession) {72const gitRepo = providerSession.gitRepository;73const repoState = gitRepo?.state?.read?.(reader);74const hasHeadCommit = repoState ? !!repoState.HEAD?.commit : true;75// Enable only when git repo exists and HEAD has a valid commit (not an empty repo)76this._hasGitRepo = !isLoading && !!gitRepo && hasHeadCommit;77// Read isolation mode from session — session is the source of truth78providerSession.isolationMode.read(reader);79} else {80this._hasGitRepo = false;81}82this._updateTriggerLabel();83}));84}8586private _getSessionIsolationMode(): IsolationMode {87const session = this.sessionsManagementService.activeSession.get();88const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;89const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;90return providerSession?.isolationMode.get() ?? 'worktree';91}9293render(container: HTMLElement): void {94this._renderDisposables.clear();9596const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));97this._renderDisposables.add({ dispose: () => slot.remove() });98this._slotElement = slot;99100const trigger = dom.append(slot, dom.$('a.action-label'));101trigger.tabIndex = 0;102trigger.role = 'button';103this._triggerElement = trigger;104this._updateTriggerLabel();105106this._renderDisposables.add(Gesture.addTarget(trigger));107for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {108this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {109dom.EventHelper.stop(e, true);110this._showPicker();111}));112}113114this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {115if (e.key === 'Enter' || e.key === ' ') {116dom.EventHelper.stop(e, true);117this._showPicker();118}119}));120}121122private _showPicker(): void {123if (!this._triggerElement || this.actionWidgetService.isVisible) {124return;125}126127if (!this._hasGitRepo || !this._isolationOptionEnabled) {128return;129}130131const currentIsolationMode = this._getSessionIsolationMode();132const items: IActionListItem<IIsolationPickerItem>[] = [133{134kind: ActionListItemKind.Action,135label: localize('isolationMode.worktree', "Worktree"),136group: { title: '', icon: Codicon.worktree },137item: { mode: 'worktree', checked: currentIsolationMode === 'worktree' || undefined },138},139{140kind: ActionListItemKind.Action,141label: localize('isolationMode.folder', "Folder"),142group: { title: '', icon: Codicon.folder },143item: { mode: 'workspace', checked: currentIsolationMode === 'workspace' || undefined },144},145];146147const triggerElement = this._triggerElement;148const delegate: IActionListDelegate<IIsolationPickerItem> = {149onSelect: ({ mode }) => {150this.actionWidgetService.hide();151this._setModeOnSession(mode);152},153onHide: () => { triggerElement.focus(); },154};155156this.actionWidgetService.show<IIsolationPickerItem>(157'isolationPicker',158false,159items,160delegate,161this._triggerElement,162undefined,163[],164{165getAriaLabel: (item) => item.label ?? '',166getWidgetAriaLabel: () => localize('isolationPicker.ariaLabel', "Isolation Mode"),167},168);169}170171private _setModeOnSession(mode: IsolationMode): void {172const session = this.sessionsManagementService.activeSession.get();173const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;174const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;175providerSession?.setIsolationMode(mode);176}177178private _updateTriggerLabel(): void {179if (!this._triggerElement) {180return;181}182183dom.clearNode(this._triggerElement);184185const isolationMode = this._getSessionIsolationMode();186let modeIcon;187let modeLabel: string;188189switch (isolationMode) {190case 'workspace':191modeIcon = Codicon.folder;192modeLabel = localize('isolationMode.folder', "Folder");193break;194case 'worktree':195default:196modeIcon = Codicon.worktree;197modeLabel = localize('isolationMode.worktree', "Worktree");198break;199}200201dom.append(this._triggerElement, renderIcon(modeIcon));202const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));203labelSpan.textContent = modeLabel;204dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));205206this._triggerElement.ariaLabel = localize('isolationPicker.triggerAriaLabel', "Pick Isolation Mode, {0}", modeLabel);207208const isDisabled = !this._hasGitRepo;209this._slotElement?.classList.toggle('disabled', isDisabled);210this._triggerElement.setAttribute('aria-disabled', String(isDisabled));211this._triggerElement.tabIndex = isDisabled ? -1 : 0;212}213}214215216