Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/branchPicker.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 { 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 { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';15import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';16import { CopilotChatSessionsProvider, ICopilotChatSession } from './copilotChatSessionsProvider.js';1718const FILTER_THRESHOLD = 10;1920interface IBranchItem {21readonly name: string;22}2324/**25* A widget for selecting a git branch.26* Reads branch list and selected branch from the active session,27* which is the source of truth for branch state.28*/29export class BranchPicker extends Disposable {3031private readonly _renderDisposables = this._register(new DisposableStore());32private _slotElement: HTMLElement | undefined;33private _triggerElement: HTMLElement | undefined;3435constructor(36@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,37@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,38@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,39) {40super();4142this._register(autorun(reader => {43const session = this.sessionsManagementService.activeSession.read(reader);44const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;45const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;46if (providerSession) {47providerSession.loading.read(reader);48providerSession.branches.read(reader);49providerSession.branch.read(reader);50providerSession.isolationMode.read(reader);51}52this._updateTriggerLabel();53}));54}5556private _getSession(): ICopilotChatSession | undefined {57const session = this.sessionsManagementService.activeSession.get();58if (!session) {59return undefined;60}61const provider = this.sessionsProvidersService.getProvider(session.providerId);62return provider instanceof CopilotChatSessionsProvider ? provider.getSession(session.sessionId) : undefined;63}6465render(container: HTMLElement): void {66this._renderDisposables.clear();6768const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));69this._slotElement = slot;70this._renderDisposables.add({ dispose: () => slot.remove() });7172const trigger = dom.append(slot, dom.$('a.action-label'));73trigger.tabIndex = 0;74trigger.role = 'button';75this._triggerElement = trigger;76this._updateTriggerLabel();7778this._renderDisposables.add(Gesture.addTarget(trigger));79for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {80this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {81dom.EventHelper.stop(e, true);82this.showPicker();83}));84}8586this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {87if (e.key === 'Enter' || e.key === ' ') {88dom.EventHelper.stop(e, true);89this.showPicker();90}91}));92}9394showPicker(): void {95const session = this._getSession();96const branches = session?.branches.get() ?? [];97if (!this._triggerElement || this.actionWidgetService.isVisible || branches.length === 0 || session?.isolationMode.get() === 'workspace') {98return;99}100101const selectedBranch = session?.branch.get();102const items: IActionListItem<IBranchItem>[] = branches.map(branch => ({103kind: ActionListItemKind.Action,104label: branch,105group: { title: '', icon: Codicon.gitBranch },106item: { name: branch, checked: branch === selectedBranch || undefined },107}));108109const triggerElement = this._triggerElement;110const delegate: IActionListDelegate<IBranchItem> = {111onSelect: (item) => {112this.actionWidgetService.hide();113session?.setBranch(item.name);114},115onHide: () => { triggerElement.focus(); },116};117118const totalActions = items.filter(i => i.kind === ActionListItemKind.Action).length;119120this.actionWidgetService.show<IBranchItem>(121'branchPicker',122false,123items,124delegate,125this._triggerElement,126undefined,127[],128{129getAriaLabel: (item) => item.label ?? '',130getWidgetAriaLabel: () => localize('branchPicker.ariaLabel', "Branch Picker"),131},132totalActions > FILTER_THRESHOLD ? { showFilter: true, filterPlaceholder: localize('branchPicker.filter', "Filter branches...") } : undefined,133);134}135136private _updateTriggerLabel(): void {137if (!this._triggerElement) {138return;139}140dom.clearNode(this._triggerElement);141142const session = this._getSession();143const branches = session?.branches.get() ?? [];144const isLoading = session?.loading.get() ?? false;145const isDisabled = session?.isolationMode.get() === 'workspace' || branches.length === 0;146const label = session?.branch.get() ?? localize('branchPicker.select', "Branch");147148dom.append(this._triggerElement, renderIcon(Codicon.gitBranch));149const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));150labelSpan.textContent = label;151dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));152153this._triggerElement.ariaLabel = localize('branchPicker.triggerAriaLabel', "Pick Branch, {0}", label);154155this._slotElement?.classList.toggle('disabled', isLoading || isDisabled);156this._triggerElement.setAttribute('aria-disabled', String(isLoading || isDisabled));157this._triggerElement.tabIndex = (isLoading || isDisabled) ? -1 : 0;158}159}160161162