Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/isolationPicker.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 { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
12
import { localize } from '../../../../nls.js';
13
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
14
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
17
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
18
import { CopilotChatSessionsProvider } from './copilotChatSessionsProvider.js';
19
20
export type IsolationMode = 'worktree' | 'workspace';
21
22
interface IIsolationPickerItem {
23
readonly mode: IsolationMode;
24
readonly checked?: boolean;
25
}
26
27
/**
28
* A self-contained widget for selecting the isolation mode.
29
*
30
* Options:
31
* - **Worktree** (`worktree`) — run in a git worktree
32
* - **Folder** (`workspace`) — run directly in the folder
33
*
34
* Only visible when isolation option is enabled, project has a git repo,
35
* and the target is CLI.
36
*
37
* Emits `onDidChange` with the selected `IsolationMode` when the user picks an option.
38
*/
39
export class IsolationPicker extends Disposable {
40
41
private _hasGitRepo = false;
42
private _isolationOptionEnabled: boolean;
43
44
private readonly _renderDisposables = this._register(new DisposableStore());
45
private _slotElement: HTMLElement | undefined;
46
private _triggerElement: HTMLElement | undefined;
47
48
constructor(
49
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
50
@IConfigurationService private readonly configurationService: IConfigurationService,
51
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
52
@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,
53
) {
54
super();
55
this._isolationOptionEnabled = this.configurationService.getValue<boolean>('github.copilot.chat.cli.isolationOption.enabled') !== false;
56
57
this._register(this.configurationService.onDidChangeConfiguration(e => {
58
if (e.affectsConfiguration('github.copilot.chat.cli.isolationOption.enabled')) {
59
this._isolationOptionEnabled = this.configurationService.getValue<boolean>('github.copilot.chat.cli.isolationOption.enabled') !== false;
60
if (!this._isolationOptionEnabled) {
61
this._setModeOnSession('worktree');
62
}
63
this._updateTriggerLabel();
64
}
65
}));
66
67
this._register(autorun(reader => {
68
const session = this.sessionsManagementService.activeSession.read(reader);
69
const isLoading = session?.loading.read(reader);
70
const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;
71
const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;
72
if (providerSession) {
73
const gitRepo = providerSession.gitRepository;
74
const repoState = gitRepo?.state?.read?.(reader);
75
const hasHeadCommit = repoState ? !!repoState.HEAD?.commit : true;
76
// Enable only when git repo exists and HEAD has a valid commit (not an empty repo)
77
this._hasGitRepo = !isLoading && !!gitRepo && hasHeadCommit;
78
// Read isolation mode from session — session is the source of truth
79
providerSession.isolationMode.read(reader);
80
} else {
81
this._hasGitRepo = false;
82
}
83
this._updateTriggerLabel();
84
}));
85
}
86
87
private _getSessionIsolationMode(): IsolationMode {
88
const session = this.sessionsManagementService.activeSession.get();
89
const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;
90
const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;
91
return providerSession?.isolationMode.get() ?? 'worktree';
92
}
93
94
render(container: HTMLElement): void {
95
this._renderDisposables.clear();
96
97
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
98
this._renderDisposables.add({ dispose: () => slot.remove() });
99
this._slotElement = slot;
100
101
const trigger = dom.append(slot, dom.$('a.action-label'));
102
trigger.tabIndex = 0;
103
trigger.role = 'button';
104
this._triggerElement = trigger;
105
this._updateTriggerLabel();
106
107
this._renderDisposables.add(Gesture.addTarget(trigger));
108
for (const eventType of [dom.EventType.CLICK, TouchEventType.Tap]) {
109
this._renderDisposables.add(dom.addDisposableListener(trigger, eventType, (e) => {
110
dom.EventHelper.stop(e, true);
111
this._showPicker();
112
}));
113
}
114
115
this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.KEY_DOWN, (e) => {
116
if (e.key === 'Enter' || e.key === ' ') {
117
dom.EventHelper.stop(e, true);
118
this._showPicker();
119
}
120
}));
121
}
122
123
private _showPicker(): void {
124
if (!this._triggerElement || this.actionWidgetService.isVisible) {
125
return;
126
}
127
128
if (!this._hasGitRepo || !this._isolationOptionEnabled) {
129
return;
130
}
131
132
const currentIsolationMode = this._getSessionIsolationMode();
133
const items: IActionListItem<IIsolationPickerItem>[] = [
134
{
135
kind: ActionListItemKind.Action,
136
label: localize('isolationMode.worktree', "Worktree"),
137
group: { title: '', icon: Codicon.worktree },
138
item: { mode: 'worktree', checked: currentIsolationMode === 'worktree' || undefined },
139
},
140
{
141
kind: ActionListItemKind.Action,
142
label: localize('isolationMode.folder', "Folder"),
143
group: { title: '', icon: Codicon.folder },
144
item: { mode: 'workspace', checked: currentIsolationMode === 'workspace' || undefined },
145
},
146
];
147
148
const triggerElement = this._triggerElement;
149
const delegate: IActionListDelegate<IIsolationPickerItem> = {
150
onSelect: ({ mode }) => {
151
this.actionWidgetService.hide();
152
this._setModeOnSession(mode);
153
},
154
onHide: () => { triggerElement.focus(); },
155
};
156
157
this.actionWidgetService.show<IIsolationPickerItem>(
158
'isolationPicker',
159
false,
160
items,
161
delegate,
162
this._triggerElement,
163
undefined,
164
[],
165
{
166
getAriaLabel: (item) => item.label ?? '',
167
getWidgetAriaLabel: () => localize('isolationPicker.ariaLabel', "Isolation Mode"),
168
},
169
);
170
}
171
172
private _setModeOnSession(mode: IsolationMode): void {
173
const session = this.sessionsManagementService.activeSession.get();
174
const provider = session ? this.sessionsProvidersService.getProvider(session.providerId) : undefined;
175
const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session!.sessionId) : undefined;
176
providerSession?.setIsolationMode(mode);
177
}
178
179
private _updateTriggerLabel(): void {
180
if (!this._triggerElement) {
181
return;
182
}
183
184
dom.clearNode(this._triggerElement);
185
186
const isolationMode = this._getSessionIsolationMode();
187
let modeIcon;
188
let modeLabel: string;
189
190
switch (isolationMode) {
191
case 'workspace':
192
modeIcon = Codicon.folder;
193
modeLabel = localize('isolationMode.folder', "Folder");
194
break;
195
case 'worktree':
196
default:
197
modeIcon = Codicon.worktree;
198
modeLabel = localize('isolationMode.worktree', "Worktree");
199
break;
200
}
201
202
dom.append(this._triggerElement, renderIcon(modeIcon));
203
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
204
labelSpan.textContent = modeLabel;
205
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
206
207
this._triggerElement.ariaLabel = localize('isolationPicker.triggerAriaLabel', "Pick Isolation Mode, {0}", modeLabel);
208
209
const isDisabled = !this._hasGitRepo;
210
this._slotElement?.classList.toggle('disabled', isDisabled);
211
this._triggerElement.setAttribute('aria-disabled', String(isDisabled));
212
this._triggerElement.tabIndex = isDisabled ? -1 : 0;
213
}
214
}
215
216