Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsActions.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 { coalesce } from '../../../../base/common/arrays.js';
7
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
8
import { IReader, autorun, observableValue } from '../../../../base/common/observable.js';
9
import { localize2 } from '../../../../nls.js';
10
import { Action2, registerAction2, MenuId, MenuRegistry, isIMenuItem } from '../../../../platform/actions/common/actions.js';
11
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
12
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
13
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
14
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
15
import { BaseActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
16
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
17
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
18
import { ModelPickerActionItem, IModelPickerDelegate } from '../../../../workbench/contrib/chat/browser/widget/input/modelPickerActionItem.js';
19
import { IChatInputPickerOptions } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js';
20
import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
21
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
22
import { Menus } from '../../../browser/menus.js';
23
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
24
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
25
import { SessionItemContextMenuId } from '../../sessions/browser/views/sessionsList.js';
26
import { CLAUDE_CODE_SESSION_TYPE, COPILOT_CLI_SESSION_TYPE, COPILOT_CLOUD_SESSION_TYPE, ISession } from '../../../services/sessions/common/session.js';
27
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
28
import { COPILOT_PROVIDER_ID, CopilotChatSessionsProvider } from './copilotChatSessionsProvider.js';
29
import { ActiveSessionHasGitRepositoryContext, ActiveSessionProviderIdContext, ActiveSessionTypeContext, ChatSessionProviderIdContext, IsNewChatSessionContext } from '../../../common/contextkeys.js';
30
import { IsolationPicker } from './isolationPicker.js';
31
import { BranchPicker } from './branchPicker.js';
32
import { ModePicker } from './modePicker.js';
33
import { CloudModelPicker } from './modelPicker.js';
34
import { CopilotPermissionPickerDelegate, PermissionPicker } from './permissionPicker.js';
35
import { ClaudePermissionModePicker } from './claudePermissionModePicker.js';
36
37
const IsActiveSessionCopilotCLI = ContextKeyExpr.equals(ActiveSessionTypeContext.key, COPILOT_CLI_SESSION_TYPE);
38
const IsActiveSessionCopilotCloud = ContextKeyExpr.equals(ActiveSessionTypeContext.key, COPILOT_CLOUD_SESSION_TYPE);
39
const IsActiveCopilotChatSessionProvider = ContextKeyExpr.equals(ActiveSessionProviderIdContext.key, COPILOT_PROVIDER_ID);
40
const IsActiveSessionCopilotChatCLI = ContextKeyExpr.and(IsActiveSessionCopilotCLI, IsActiveCopilotChatSessionProvider);
41
const IsActiveSessionCopilotChatCloud = ContextKeyExpr.and(IsActiveSessionCopilotCloud, IsActiveCopilotChatSessionProvider);
42
const IsActiveSessionClaudeCode = ContextKeyExpr.equals(ActiveSessionTypeContext.key, CLAUDE_CODE_SESSION_TYPE);
43
const IsActiveSessionCopilotChatClaudeCode = ContextKeyExpr.and(IsActiveSessionClaudeCode, IsActiveCopilotChatSessionProvider);
44
45
// -- Actions --
46
47
registerAction2(class extends Action2 {
48
constructor() {
49
super({
50
id: 'sessions.defaultCopilot.isolationPicker',
51
title: localize2('isolationPicker', "Isolation Mode"),
52
f1: false,
53
menu: [{
54
id: Menus.NewSessionRepositoryConfig,
55
group: 'navigation',
56
order: 1,
57
when: ContextKeyExpr.and(
58
IsNewChatSessionContext,
59
IsActiveSessionCopilotChatCLI,
60
ContextKeyExpr.equals('config.github.copilot.chat.cli.isolationOption.enabled', true),
61
),
62
}],
63
});
64
}
65
override async run(): Promise<void> { /* handled by action view item */ }
66
});
67
68
registerAction2(class extends Action2 {
69
constructor() {
70
super({
71
id: 'sessions.defaultCopilot.branchPicker',
72
title: localize2('branchPicker', "Branch"),
73
f1: false,
74
precondition: ActiveSessionHasGitRepositoryContext,
75
menu: [{
76
id: Menus.NewSessionRepositoryConfig,
77
group: 'navigation',
78
order: 2,
79
when: ContextKeyExpr.and(IsNewChatSessionContext, IsActiveSessionCopilotChatCLI),
80
}],
81
});
82
}
83
override async run(): Promise<void> { /* handled by action view item */ }
84
});
85
86
registerAction2(class extends Action2 {
87
constructor() {
88
super({
89
id: 'sessions.defaultCopilot.modePicker',
90
title: localize2('modePicker', "Mode"),
91
f1: false,
92
menu: [{
93
id: Menus.NewSessionConfig,
94
group: 'navigation',
95
order: 0,
96
when: IsActiveSessionCopilotChatCLI,
97
}],
98
});
99
}
100
override async run(): Promise<void> { /* handled by action view item */ }
101
});
102
103
registerAction2(class extends Action2 {
104
constructor() {
105
super({
106
id: 'sessions.defaultCopilot.localModelPicker',
107
title: localize2('localModelPicker', "Model"),
108
f1: false,
109
menu: [{
110
id: Menus.NewSessionConfig,
111
group: 'navigation',
112
order: 1,
113
when: ContextKeyExpr.or(IsActiveSessionCopilotChatCLI, IsActiveSessionCopilotChatClaudeCode),
114
}],
115
});
116
}
117
override async run(): Promise<void> { /* handled by action view item */ }
118
});
119
120
registerAction2(class extends Action2 {
121
constructor() {
122
super({
123
id: 'sessions.defaultCopilot.cloudModelPicker',
124
title: localize2('cloudModelPicker', "Model"),
125
f1: false,
126
menu: [{
127
id: Menus.NewSessionConfig,
128
group: 'navigation',
129
order: 1,
130
when: IsActiveSessionCopilotChatCloud,
131
}],
132
});
133
}
134
override async run(): Promise<void> { /* handled by action view item */ }
135
});
136
137
registerAction2(class extends Action2 {
138
constructor() {
139
super({
140
id: 'sessions.defaultCopilot.permissionPicker',
141
title: localize2('permissionPicker', "Permissions"),
142
f1: false,
143
menu: [{
144
id: Menus.NewSessionControl,
145
group: 'navigation',
146
order: 1,
147
when: IsActiveSessionCopilotChatCLI,
148
}],
149
});
150
}
151
override async run(): Promise<void> { /* handled by action view item */ }
152
});
153
154
registerAction2(class extends Action2 {
155
constructor() {
156
super({
157
id: 'sessions.defaultCopilot.claudePermissionModePicker',
158
title: localize2('claudePermissionModePicker', "Permission Mode"),
159
f1: false,
160
menu: [{
161
id: Menus.NewSessionControl,
162
group: 'navigation',
163
order: 1,
164
when: IsActiveSessionCopilotChatClaudeCode,
165
}],
166
});
167
}
168
override async run(): Promise<void> { /* handled by action view item */ }
169
});
170
171
// -- Helper --
172
173
/**
174
* Wraps a standalone picker widget as a {@link BaseActionViewItem}
175
* so it can be rendered by a {@link MenuWorkbenchToolBar}.
176
*/
177
class PickerActionViewItem extends BaseActionViewItem {
178
constructor(private readonly picker: { render(container: HTMLElement): void; dispose(): void }, disposable?: IDisposable) {
179
super(undefined, { id: '', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } });
180
if (disposable) {
181
this._register(disposable);
182
}
183
}
184
185
override render(container: HTMLElement): void {
186
this.picker.render(container);
187
}
188
189
override dispose(): void {
190
this.picker.dispose();
191
super.dispose();
192
}
193
}
194
195
// -- Action View Item Registrations --
196
197
class CopilotPickerActionViewItemContribution extends Disposable implements IWorkbenchContribution {
198
199
static readonly ID = 'workbench.contrib.copilotPickerActionViewItems';
200
201
constructor(
202
@IActionViewItemService actionViewItemService: IActionViewItemService,
203
@IInstantiationService instantiationService: IInstantiationService,
204
) {
205
super();
206
207
this._register(actionViewItemService.register(
208
Menus.NewSessionRepositoryConfig, 'sessions.defaultCopilot.isolationPicker',
209
() => {
210
const picker = instantiationService.createInstance(IsolationPicker);
211
return new PickerActionViewItem(picker);
212
},
213
));
214
this._register(actionViewItemService.register(
215
Menus.NewSessionRepositoryConfig, 'sessions.defaultCopilot.branchPicker',
216
() => {
217
const picker = instantiationService.createInstance(BranchPicker);
218
return new PickerActionViewItem(picker);
219
},
220
));
221
this._register(actionViewItemService.register(
222
Menus.NewSessionConfig, 'sessions.defaultCopilot.modePicker',
223
() => {
224
const picker = instantiationService.createInstance(ModePicker);
225
return new PickerActionViewItem(picker);
226
},
227
));
228
this._register(actionViewItemService.register(
229
Menus.NewSessionConfig, 'sessions.defaultCopilot.localModelPicker',
230
() => {
231
const picker = instantiationService.createInstance(SessionModelPicker);
232
return new PickerActionViewItem(picker);
233
},
234
));
235
this._register(actionViewItemService.register(
236
Menus.NewSessionConfig, 'sessions.defaultCopilot.cloudModelPicker',
237
() => {
238
const picker = instantiationService.createInstance(CloudModelPicker);
239
return new PickerActionViewItem(picker);
240
},
241
));
242
this._register(actionViewItemService.register(
243
Menus.NewSessionControl, 'sessions.defaultCopilot.permissionPicker',
244
() => {
245
const delegate = instantiationService.createInstance(CopilotPermissionPickerDelegate);
246
const picker = instantiationService.createInstance(PermissionPicker, delegate);
247
return new PickerActionViewItem(picker, delegate);
248
},
249
));
250
this._register(actionViewItemService.register(
251
Menus.NewSessionControl, 'sessions.defaultCopilot.claudePermissionModePicker',
252
() => {
253
const picker = instantiationService.createInstance(ClaudePermissionModePicker);
254
return new PickerActionViewItem(picker);
255
},
256
));
257
}
258
}
259
260
// -- Model Picker Helpers --
261
262
/**
263
* Returns a storage key scoped to the given session type.
264
*/
265
export function modelPickerStorageKey(sessionType: string): string {
266
return `sessions.modelPicker.${sessionType}.selectedModelId`;
267
}
268
269
/**
270
* A model picker widget that persists the selected model per session type and
271
* syncs the selection to the active session's provider. Instantiated via DI,
272
* consistent with the other picker widgets in this file.
273
*/
274
export class SessionModelPicker extends Disposable {
275
276
private readonly _currentModel = observableValue<ILanguageModelChatMetadataAndIdentifier | undefined>('currentModel', undefined);
277
private readonly _delegate: IModelPickerDelegate;
278
private readonly _modelPicker: ModelPickerActionItem;
279
private _lastSessionType: string | undefined;
280
private _lastPushedSessionId: string | undefined;
281
282
constructor(
283
@IInstantiationService instantiationService: IInstantiationService,
284
@ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService,
285
@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,
286
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
287
@IStorageService private readonly _storageService: IStorageService,
288
) {
289
super();
290
291
this._delegate = {
292
currentModel: this._currentModel,
293
setModel: (model: ILanguageModelChatMetadataAndIdentifier) => {
294
this._currentModel.set(model, undefined);
295
const session = this._sessionsManagementService.activeSession.get();
296
if (session) {
297
this._storageService.store(modelPickerStorageKey(session.sessionType), model.identifier, StorageScope.PROFILE, StorageTarget.MACHINE);
298
const provider = this._sessionsProvidersService.getProviders().find(p => p.id === session.providerId);
299
provider?.setModel(session.sessionId, model.identifier);
300
}
301
},
302
getModels: () => getAvailableModels(this._languageModelsService, this._sessionsManagementService),
303
useGroupedModelPicker: () => true,
304
showManageModelsAction: () => false,
305
showUnavailableFeatured: () => false,
306
showFeatured: () => true,
307
};
308
309
const pickerOptions: IChatInputPickerOptions = {
310
hideChevrons: observableValue('hideChevrons', false),
311
};
312
const action = { id: 'sessions.modelPicker', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } };
313
this._modelPicker = instantiationService.createInstance(ModelPickerActionItem, action, this._delegate, pickerOptions);
314
315
this._initModel();
316
this._register(this._languageModelsService.onDidChangeLanguageModels(() => this._initModel()));
317
318
// When the active session changes, re-init (may switch session type).
319
// _initModel() calls _delegate.setModel() which already forwards to
320
// the provider, so no additional provider.setModel() call is needed.
321
this._register(autorun(reader => {
322
const session = this._sessionsManagementService.activeSession.read(reader);
323
if (session) {
324
this._initModel();
325
}
326
}));
327
}
328
329
private _initModel(): void {
330
const session = this._sessionsManagementService.activeSession.get();
331
const sessionType = session?.sessionType;
332
333
// Reset the current model when switching session types so we load the
334
// remembered model for the new type instead of carrying over the old one.
335
if (sessionType !== this._lastSessionType) {
336
this._currentModel.set(undefined, undefined);
337
this._lastSessionType = sessionType;
338
}
339
340
const models = getAvailableModels(this._languageModelsService, this._sessionsManagementService);
341
this._modelPicker.setEnabled(models.length > 0);
342
if (models.length === 0) {
343
return;
344
}
345
346
const current = this._currentModel.get();
347
if (!current) {
348
const rememberedModelId = sessionType ? this._storageService.get(modelPickerStorageKey(sessionType), StorageScope.PROFILE) : undefined;
349
const remembered = rememberedModelId ? models.find(m => m.identifier === rememberedModelId) : undefined;
350
this._delegate.setModel(remembered ?? models[0]);
351
this._lastPushedSessionId = session?.sessionId;
352
} else if (session && session.sessionId !== this._lastPushedSessionId && models.some(m => m.identifier === current.identifier)) {
353
// Active session changed (e.g. user switched repository) but the
354
// previously selected model is still available. Re-push it so the
355
// new session's provider receives setModel — otherwise the request
356
// would be sent with the default model even though the picker UI
357
// still shows the user's selection. See #313385.
358
//
359
// Gated on sessionId so unrelated re-invocations of _initModel
360
// (e.g. from onDidChangeLanguageModels) don't redundantly write
361
// storage and dispatch provider.setModel for the same session.
362
this._delegate.setModel(current);
363
this._lastPushedSessionId = session.sessionId;
364
}
365
}
366
367
render(container: HTMLElement): void {
368
this._modelPicker.render(container);
369
}
370
371
override dispose(): void {
372
this._modelPicker.dispose();
373
super.dispose();
374
}
375
}
376
377
export function getAvailableModels(
378
languageModelsService: ILanguageModelsService,
379
sessionsManagementService: ISessionsManagementService,
380
): ILanguageModelChatMetadataAndIdentifier[] {
381
const session = sessionsManagementService.activeSession.get();
382
if (!session) {
383
return [];
384
}
385
return languageModelsService.getLanguageModelIds()
386
.map(id => {
387
const metadata = languageModelsService.lookupLanguageModel(id);
388
return metadata ? { metadata, identifier: id } : undefined;
389
})
390
.filter((m): m is ILanguageModelChatMetadataAndIdentifier => !!m && m.metadata.targetChatSessionType === session.sessionType);
391
}
392
393
// -- Context Key Contribution --
394
395
class CopilotActiveSessionContribution extends Disposable implements IWorkbenchContribution {
396
397
static readonly ID = 'workbench.contrib.copilotActiveSession';
398
399
constructor(
400
@ISessionsManagementService sessionsManagementService: ISessionsManagementService,
401
@ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService,
402
@IContextKeyService contextKeyService: IContextKeyService,
403
) {
404
super();
405
406
const hasRepositoryKey = ActiveSessionHasGitRepositoryContext.bindTo(contextKeyService);
407
408
this._register(autorun((reader: IReader) => {
409
const session = sessionsManagementService.activeSession.read(reader);
410
if (session?.providerId === COPILOT_PROVIDER_ID) {
411
const provider = sessionsProvidersService.getProvider(session.providerId);
412
const providerSession = provider instanceof CopilotChatSessionsProvider ? provider.getSession(session.sessionId) : undefined;
413
const isLoading = providerSession?.loading.read(reader);
414
hasRepositoryKey.set(!isLoading && !!providerSession?.gitRepository);
415
} else {
416
hasRepositoryKey.set(false);
417
}
418
}));
419
}
420
}
421
422
registerWorkbenchContribution2(CopilotPickerActionViewItemContribution.ID, CopilotPickerActionViewItemContribution, WorkbenchPhase.AfterRestored);
423
registerWorkbenchContribution2(CopilotActiveSessionContribution.ID, CopilotActiveSessionContribution, WorkbenchPhase.AfterRestored);
424
425
/**
426
* Bridges extension-contributed context menu actions from {@link MenuId.AgentSessionsContext}
427
* to {@link SessionItemContextMenuId} for the new sessions view.
428
* Registers wrapper commands that resolve {@link ISession} → {@link IAgentSession}
429
* and forward to the original command with marshalled context.
430
*/
431
class CopilotSessionContextMenuBridge extends Disposable implements IWorkbenchContribution {
432
static readonly ID = 'copilotChatSessions.contextMenuBridge';
433
434
private readonly _bridgedIds = new Set<string>();
435
436
constructor(
437
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
438
@ICommandService private readonly commandService: ICommandService,
439
) {
440
super();
441
this._bridgeItems();
442
this._register(MenuRegistry.onDidChangeMenu(menuIds => {
443
if (menuIds.has(MenuId.AgentSessionsContext)) {
444
this._bridgeItems();
445
}
446
}));
447
}
448
449
private _bridgeItems(): void {
450
const items = MenuRegistry.getMenuItems(MenuId.AgentSessionsContext).filter(isIMenuItem);
451
for (const item of items) {
452
const commandId = item.command.id;
453
if (!commandId.startsWith('github.copilot.')) {
454
continue;
455
}
456
if (commandId === 'github.copilot.cli.sessions.delete') {
457
continue; // Delete is handled natively via sessionsManagementService
458
}
459
if (this._bridgedIds.has(commandId)) {
460
continue;
461
}
462
this._bridgedIds.add(commandId);
463
464
const wrapperId = `sessionsViewPane.bridge.${commandId}`;
465
this._register(CommandsRegistry.registerCommand(wrapperId, (accessor, context?: ISession | ISession[]) => {
466
if (!context) {
467
return;
468
}
469
const sessions = Array.isArray(context) ? context : [context];
470
const agentSessions = coalesce(sessions.map(s => this.agentSessionsService.getSession(s.resource)));
471
if (agentSessions.length === 0) {
472
return;
473
}
474
return this.commandService.executeCommand(commandId, {
475
session: agentSessions[0],
476
sessions: agentSessions,
477
$mid: MarshalledId.AgentSessionContext,
478
});
479
}));
480
481
const providerWhen = ContextKeyExpr.equals(ChatSessionProviderIdContext.key, COPILOT_PROVIDER_ID);
482
this._register(MenuRegistry.appendMenuItem(SessionItemContextMenuId, {
483
command: { ...item.command, id: wrapperId },
484
group: item.group,
485
order: item.order,
486
when: item.when ? ContextKeyExpr.and(providerWhen, item.when) : providerWhen,
487
}));
488
}
489
}
490
}
491
492
registerWorkbenchContribution2(CopilotSessionContextMenuBridge.ID, CopilotSessionContextMenuBridge, WorkbenchPhase.AfterRestored);
493
494
registerAction2(class DeleteSessionAction extends Action2 {
495
constructor() {
496
super({
497
id: 'sessionsViewPane.copilot.deleteSession',
498
title: localize2('deleteSession', "Delete..."),
499
menu: [{
500
id: SessionItemContextMenuId,
501
group: '1_edit',
502
order: 4,
503
when: ContextKeyExpr.equals(ChatSessionProviderIdContext.key, COPILOT_PROVIDER_ID),
504
}]
505
});
506
}
507
async run(accessor: ServicesAccessor, context?: ISession | ISession[]): Promise<void> {
508
if (!context) {
509
return;
510
}
511
const sessions = Array.isArray(context) ? context : [context];
512
const sessionsManagementService = accessor.get(ISessionsManagementService);
513
for (const session of sessions) {
514
await sessionsManagementService.deleteSession(session);
515
}
516
}
517
});
518
519