Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/branchPicker.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';
8
import { Codicon } from '../../../../base/common/codicons.js';
9
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
10
import { autorun } from '../../../../base/common/observable.js';
11
import { localize } from '../../../../nls.js';
12
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
13
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';
14
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
15
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
16
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
17
import { CopilotChatSessionsProvider, ICopilotChatSession } from './copilotChatSessionsProvider.js';
18
19
const FILTER_THRESHOLD = 10;
20
21
interface IBranchItem {
22
readonly name: string;
23
}
24
25
/**
26
* A widget for selecting a git branch.
27
* Reads branch list and selected branch from the active session,
28
* which is the source of truth for branch state.
29
*/
30
export class BranchPicker extends Disposable {
31
32
private readonly _renderDisposables = this._register(new DisposableStore());
33
private _slotElement: HTMLElement | undefined;
34
private _triggerElement: HTMLElement | undefined;
35
36
constructor(
37
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
38
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
39
@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,
40
) {
41
super();
42
43
this._register(autorun(reader => {
44
const session = this.sessionsManagementService.activeSession.read(reader);
45
const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;
46
const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;
47
if (providerSession) {
48
providerSession.loading.read(reader);
49
providerSession.branches.read(reader);
50
providerSession.branch.read(reader);
51
providerSession.isolationMode.read(reader);
52
}
53
this._updateTriggerLabel();
54
}));
55
}
56
57
private _getSession(): ICopilotChatSession | undefined {
58
const session = this.sessionsManagementService.activeSession.get();
59
if (!session) {
60
return undefined;
61
}
62
const provider = this.sessionsProvidersService.getProvider(session.providerId);
63
return provider instanceof CopilotChatSessionsProvider ? provider.getSession(session.sessionId) : undefined;
64
}
65
66
render(container: HTMLElement): void {
67
this._renderDisposables.clear();
68
69
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
70
this._slotElement = slot;
71
this._renderDisposables.add({ dispose: () => slot.remove() });
72
73
const trigger = dom.append(slot, dom.$('a.action-label'));
74
trigger.tabIndex = 0;
75
trigger.role = 'button';
76
this._triggerElement = trigger;
77
this._updateTriggerLabel();
78
79
this._renderDisposables.add(Gesture.addTarget(trigger));
80
for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {
81
this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {
82
dom.EventHelper.stop(e, true);
83
this.showPicker();
84
}));
85
}
86
87
this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {
88
if (e.key === 'Enter' || e.key === ' ') {
89
dom.EventHelper.stop(e, true);
90
this.showPicker();
91
}
92
}));
93
}
94
95
showPicker(): void {
96
const session = this._getSession();
97
const branches = session?.branches.get() ?? [];
98
if (!this._triggerElement || this.actionWidgetService.isVisible || branches.length === 0 || session?.isolationMode.get() === 'workspace') {
99
return;
100
}
101
102
const selectedBranch = session?.branch.get();
103
const items: IActionListItem<IBranchItem>[] = branches.map(branch => ({
104
kind: ActionListItemKind.Action,
105
label: branch,
106
group: { title: '', icon: Codicon.gitBranch },
107
item: { name: branch, checked: branch === selectedBranch || undefined },
108
}));
109
110
const triggerElement = this._triggerElement;
111
const delegate: IActionListDelegate<IBranchItem> = {
112
onSelect: (item) => {
113
this.actionWidgetService.hide();
114
session?.setBranch(item.name);
115
},
116
onHide: () => { triggerElement.focus(); },
117
};
118
119
const totalActions = items.filter(i => i.kind === ActionListItemKind.Action).length;
120
121
this.actionWidgetService.show<IBranchItem>(
122
'branchPicker',
123
false,
124
items,
125
delegate,
126
this._triggerElement,
127
undefined,
128
[],
129
{
130
getAriaLabel: (item) => item.label ?? '',
131
getWidgetAriaLabel: () => localize('branchPicker.ariaLabel', "Branch Picker"),
132
},
133
totalActions > FILTER_THRESHOLD ? { showFilter: true, filterPlaceholder: localize('branchPicker.filter', "Filter branches...") } : undefined,
134
);
135
}
136
137
private _updateTriggerLabel(): void {
138
if (!this._triggerElement) {
139
return;
140
}
141
dom.clearNode(this._triggerElement);
142
143
const session = this._getSession();
144
const branches = session?.branches.get() ?? [];
145
const isLoading = session?.loading.get() ?? false;
146
const isDisabled = session?.isolationMode.get() === 'workspace' || branches.length === 0;
147
const label = session?.branch.get() ?? localize('branchPicker.select', "Branch");
148
149
dom.append(this._triggerElement, renderIcon(Codicon.gitBranch));
150
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
151
labelSpan.textContent = label;
152
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
153
154
this._triggerElement.ariaLabel = localize('branchPicker.triggerAriaLabel', "Pick Branch, {0}", label);
155
156
this._slotElement?.classList.toggle('disabled', isLoading || isDisabled);
157
this._triggerElement.setAttribute('aria-disabled', String(isLoading || isDisabled));
158
this._triggerElement.tabIndex = (isLoading || isDisabled) ? -1 : 0;
159
}
160
}
161
162