Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSessions.ts
3296 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 './media/chatSessions.css';
7
import * as DOM from '../../../../base/browser/dom.js';
8
import { $, append, getActiveWindow } from '../../../../base/browser/dom.js';
9
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
10
import { IListVirtualDelegate, IListRenderer } from '../../../../base/browser/ui/list/list.js';
11
import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';
12
import { coalesce } from '../../../../base/common/arrays.js';
13
import { CancellationToken } from '../../../../base/common/cancellation.js';
14
import { Codicon } from '../../../../base/common/codicons.js';
15
import { fromNow } from '../../../../base/common/date.js';
16
import { Emitter, Event } from '../../../../base/common/event.js';
17
import { FuzzyScore } from '../../../../base/common/filters.js';
18
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
19
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
20
import { ThemeIcon } from '../../../../base/common/themables.js';
21
import { URI } from '../../../../base/common/uri.js';
22
import { isMarkdownString } from '../../../../base/common/htmlContent.js';
23
import { IChatSessionRecommendation } from '../../../../base/common/product.js';
24
import * as nls from '../../../../nls.js';
25
import { getActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
26
import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
27
import { ICommandService } from '../../../../platform/commands/common/commands.js';
28
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
29
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
30
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
31
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
32
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
33
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
34
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
35
import { WorkbenchAsyncDataTree, WorkbenchList } from '../../../../platform/list/browser/listService.js';
36
import { ILogService } from '../../../../platform/log/common/log.js';
37
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
38
import { IProductService } from '../../../../platform/product/common/productService.js';
39
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
40
import { Registry } from '../../../../platform/registry/common/platform.js';
41
import { IStorageService } from '../../../../platform/storage/common/storage.js';
42
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
43
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
44
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
45
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
46
import { IResourceLabel, ResourceLabels } from '../../../browser/labels.js';
47
import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js';
48
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
49
import { IWorkbenchContribution } from '../../../common/contributions.js';
50
import { EditorInput } from '../../../common/editor/editorInput.js';
51
import { Extensions, IEditableData, IViewContainersRegistry, IViewDescriptor, IViewDescriptorService, IViewsRegistry, ViewContainerLocation } from '../../../common/views.js';
52
import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
53
import { IEditorService } from '../../../services/editor/common/editorService.js';
54
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
55
import { IExtensionGalleryService } from '../../../../platform/extensionManagement/common/extensionManagement.js';
56
import { IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js';
57
import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
58
import { IChatSessionItem, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService, ChatSessionStatus } from '../common/chatSessionsService.js';
59
import { IViewsService } from '../../../services/views/common/viewsService.js';
60
import { ChatContextKeys } from '../common/chatContextKeys.js';
61
import { ChatAgentLocation, ChatConfiguration } from '../common/constants.js';
62
import { IChatWidget, IChatWidgetService, ChatViewId } from './chat.js';
63
import { ChatViewPane } from './chatViewPane.js';
64
import { ChatEditorInput } from './chatEditorInput.js';
65
import { IChatEditorOptions } from './chatEditor.js';
66
import { IChatService } from '../common/chatService.js';
67
import { ChatSessionUri } from '../common/chatUri.js';
68
import { InputBox, MessageType } from '../../../../base/browser/ui/inputbox/inputBox.js';
69
import Severity from '../../../../base/common/severity.js';
70
import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
71
import { createSingleCallFunction } from '../../../../base/common/functional.js';
72
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
73
import { timeout } from '../../../../base/common/async.js';
74
import { KeyCode } from '../../../../base/common/keyCodes.js';
75
import { IProgressService } from '../../../../platform/progress/common/progress.js';
76
import { fillEditorsDragData } from '../../../browser/dnd.js';
77
import { IChatModel } from '../common/chatModel.js';
78
import { IObservable } from '../../../../base/common/observable.js';
79
import { ChatSessionItemWithProvider, getChatSessionType, isChatSession } from './chatSessions/common.js';
80
import { ChatSessionTracker } from './chatSessions/chatSessionTracker.js';
81
import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
82
import { allowedChatMarkdownHtmlTags } from './chatMarkdownRenderer.js';
83
import product from '../../../../platform/product/common/product.js';
84
import { truncate } from '../../../../base/common/strings.js';
85
import { IChatEntitlementService } from '../common/chatEntitlementService.js';
86
87
export const VIEWLET_ID = 'workbench.view.chat.sessions';
88
89
// Helper function to update relative time for chat sessions (similar to timeline)
90
function updateRelativeTime(item: ChatSessionItemWithProvider, lastRelativeTime: string | undefined): string | undefined {
91
if (item.timing?.startTime) {
92
item.relativeTime = fromNow(item.timing.startTime);
93
item.relativeTimeFullWord = fromNow(item.timing.startTime, false, true);
94
if (lastRelativeTime === undefined || item.relativeTime !== lastRelativeTime) {
95
lastRelativeTime = item.relativeTime;
96
item.hideRelativeTime = false;
97
} else {
98
item.hideRelativeTime = true;
99
}
100
} else {
101
// Clear timestamp properties if no timestamp
102
item.relativeTime = undefined;
103
item.relativeTimeFullWord = undefined;
104
item.hideRelativeTime = false;
105
}
106
107
return lastRelativeTime;
108
}
109
110
// Helper function to extract timestamp from session item
111
function extractTimestamp(item: IChatSessionItem): number | undefined {
112
// Use timing.startTime if available from the API
113
if (item.timing?.startTime) {
114
return item.timing.startTime;
115
}
116
117
// For other items, timestamp might already be set
118
if ('timestamp' in item) {
119
return (item as any).timestamp;
120
}
121
122
return undefined;
123
}
124
125
// Helper function to sort sessions by timestamp (newest first)
126
function sortSessionsByTimestamp(sessions: ChatSessionItemWithProvider[]): void {
127
sessions.sort((a, b) => {
128
const aTime = a.timing?.startTime ?? 0;
129
const bTime = b.timing?.startTime ?? 0;
130
return bTime - aTime; // newest first
131
});
132
}
133
134
// Helper function to apply time grouping to a list of sessions
135
function applyTimeGrouping(sessions: ChatSessionItemWithProvider[]): void {
136
let lastRelativeTime: string | undefined;
137
sessions.forEach(session => {
138
lastRelativeTime = updateRelativeTime(session, lastRelativeTime);
139
});
140
}
141
142
// Helper function to process session items with timestamps, sorting, and grouping
143
function processSessionsWithTimeGrouping(sessions: ChatSessionItemWithProvider[]): void {
144
// Only process if we have sessions with timestamps
145
if (sessions.some(session => session.timing?.startTime !== undefined)) {
146
sortSessionsByTimestamp(sessions);
147
applyTimeGrouping(sessions);
148
}
149
}
150
151
// Helper function to create context overlay for session items
152
function getSessionItemContextOverlay(
153
session: IChatSessionItem,
154
provider?: IChatSessionItemProvider,
155
chatWidgetService?: IChatWidgetService,
156
chatService?: IChatService,
157
editorGroupsService?: IEditorGroupsService
158
): [string, any][] {
159
const overlay: [string, any][] = [];
160
// Do not create an overaly for the show-history node
161
if (session.id === 'show-history') {
162
return overlay;
163
}
164
if (provider) {
165
overlay.push([ChatContextKeys.sessionType.key, provider.chatSessionType]);
166
}
167
168
// Mark history items
169
const isHistoryItem = session.id.startsWith('history-');
170
overlay.push([ChatContextKeys.isHistoryItem.key, isHistoryItem]);
171
172
// Mark active sessions - check if session is currently open in editor or widget
173
let isActiveSession = false;
174
175
if (!isHistoryItem && provider?.chatSessionType === 'local') {
176
// Local non-history sessions are always active
177
isActiveSession = true;
178
} else if (isHistoryItem && chatWidgetService && chatService && editorGroupsService) {
179
// For history sessions, check if they're currently opened somewhere
180
const sessionId = session.id.substring('history-'.length); // Remove 'history-' prefix
181
182
// Check if session is open in a chat widget
183
const widget = chatWidgetService.getWidgetBySessionId(sessionId);
184
if (widget) {
185
isActiveSession = true;
186
} else {
187
// Check if session is open in any editor
188
for (const group of editorGroupsService.groups) {
189
for (const editor of group.editors) {
190
if (editor instanceof ChatEditorInput && editor.sessionId === sessionId) {
191
isActiveSession = true;
192
break;
193
}
194
}
195
if (isActiveSession) {
196
break;
197
}
198
}
199
}
200
}
201
202
overlay.push([ChatContextKeys.isActiveSession.key, isActiveSession]);
203
204
return overlay;
205
}
206
207
// Extended interface for local chat session items that includes editor information or widget information
208
export interface ILocalChatSessionItem extends IChatSessionItem {
209
editor?: EditorInput;
210
group?: IEditorGroup;
211
widget?: IChatWidget;
212
sessionType: 'editor' | 'widget';
213
description?: string;
214
status?: ChatSessionStatus;
215
}
216
217
interface IGettingStartedItem {
218
id: string;
219
label: string;
220
commandId: string;
221
icon?: ThemeIcon;
222
args?: any[];
223
}
224
225
class GettingStartedDelegate implements IListVirtualDelegate<IGettingStartedItem> {
226
getHeight(): number {
227
return 22;
228
}
229
230
getTemplateId(): string {
231
return 'gettingStartedItem';
232
}
233
}
234
235
interface IGettingStartedTemplateData {
236
resourceLabel: IResourceLabel;
237
}
238
239
class GettingStartedRenderer implements IListRenderer<IGettingStartedItem, IGettingStartedTemplateData> {
240
readonly templateId = 'gettingStartedItem';
241
242
constructor(private readonly labels: ResourceLabels) { }
243
244
renderTemplate(container: HTMLElement): IGettingStartedTemplateData {
245
const resourceLabel = this.labels.create(container, { supportHighlights: true });
246
return { resourceLabel };
247
}
248
249
renderElement(element: IGettingStartedItem, index: number, templateData: IGettingStartedTemplateData): void {
250
templateData.resourceLabel.setResource({
251
name: element.label,
252
resource: undefined
253
}, {
254
icon: element.icon,
255
hideIcon: false
256
});
257
templateData.resourceLabel.element.setAttribute('data-command', element.commandId);
258
}
259
260
disposeTemplate(templateData: IGettingStartedTemplateData): void {
261
templateData.resourceLabel.dispose();
262
}
263
}
264
265
export class ChatSessionsView extends Disposable implements IWorkbenchContribution {
266
static readonly ID = 'workbench.contrib.chatSessions';
267
268
private isViewContainerRegistered = false;
269
private localProvider: LocalChatSessionsProvider | undefined;
270
private readonly sessionTracker: ChatSessionTracker;
271
272
constructor(
273
@IConfigurationService private readonly configurationService: IConfigurationService,
274
@IInstantiationService private readonly instantiationService: IInstantiationService,
275
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
276
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService
277
) {
278
super();
279
280
this.sessionTracker = this._register(this.instantiationService.createInstance(ChatSessionTracker));
281
this.setupEditorTracking();
282
283
// Create and register the local chat sessions provider immediately
284
// This ensures it's available even when the view container is not initialized
285
this.localProvider = this._register(this.instantiationService.createInstance(LocalChatSessionsProvider));
286
this._register(this.chatSessionsService.registerChatSessionItemProvider(this.localProvider));
287
288
// Initial check
289
this.updateViewContainerRegistration();
290
291
// Listen for configuration changes
292
this._register(this.configurationService.onDidChangeConfiguration(e => {
293
if (e.affectsConfiguration(ChatConfiguration.AgentSessionsViewLocation)) {
294
this.updateViewContainerRegistration();
295
}
296
}));
297
}
298
299
private setupEditorTracking(): void {
300
this._register(this.sessionTracker.onDidChangeEditors(e => {
301
this.chatSessionsService.notifySessionItemsChanged(e.sessionType);
302
}));
303
}
304
305
private updateViewContainerRegistration(): void {
306
const location = this.configurationService.getValue<string>(ChatConfiguration.AgentSessionsViewLocation);
307
308
if (location === 'view' && !this.isViewContainerRegistered) {
309
this.registerViewContainer();
310
} else if (location !== 'view' && this.isViewContainerRegistered) {
311
// Note: VS Code doesn't support unregistering view containers
312
// Once registered, they remain registered for the session
313
// but you could hide them or make them conditional through 'when' clauses
314
}
315
}
316
317
private registerViewContainer(): void {
318
if (this.isViewContainerRegistered) {
319
return;
320
}
321
322
323
if (this.chatEntitlementService.sentiment.hidden || this.chatEntitlementService.sentiment.disabled) {
324
return; // do not register container as AI features are hidden or disabled
325
}
326
327
Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).registerViewContainer(
328
{
329
id: VIEWLET_ID,
330
title: nls.localize2('chat.sessions', "Chat Sessions"),
331
ctorDescriptor: new SyncDescriptor(ChatSessionsViewPaneContainer, [this.sessionTracker]),
332
hideIfEmpty: false,
333
icon: registerIcon('chat-sessions-icon', Codicon.commentDiscussionSparkle, 'Icon for Chat Sessions View'),
334
order: 6
335
}, ViewContainerLocation.Sidebar);
336
}
337
}
338
339
// Local Chat Sessions Provider - tracks open editors as chat sessions
340
class LocalChatSessionsProvider extends Disposable implements IChatSessionItemProvider {
341
static readonly CHAT_WIDGET_VIEW_ID = 'workbench.panel.chat.view.copilot';
342
readonly chatSessionType = 'local';
343
344
private readonly _onDidChange = this._register(new Emitter<void>());
345
readonly onDidChange: Event<void> = this._onDidChange.event;
346
347
readonly _onDidChangeChatSessionItems = this._register(new Emitter<void>());
348
public get onDidChangeChatSessionItems() { return this._onDidChangeChatSessionItems.event; }
349
350
// Track the current editor set to detect actual new additions
351
private currentEditorSet = new Set<string>();
352
353
// Maintain ordered list of editor keys to preserve consistent ordering
354
private editorOrder: string[] = [];
355
356
constructor(
357
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
358
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
359
@IChatService private readonly chatService: IChatService,
360
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
361
) {
362
super();
363
364
this.initializeCurrentEditorSet();
365
this.registerWidgetListeners();
366
367
this._register(this.chatService.onDidDisposeSession(() => {
368
this._onDidChange.fire();
369
}));
370
371
// Listen for global session items changes for our session type
372
this._register(this.chatSessionsService.onDidChangeSessionItems((sessionType) => {
373
if (sessionType === this.chatSessionType) {
374
this.initializeCurrentEditorSet();
375
this._onDidChange.fire();
376
}
377
}));
378
}
379
380
private registerWidgetListeners(): void {
381
// Listen for new chat widgets being added/removed
382
this._register(this.chatWidgetService.onDidAddWidget(widget => {
383
// Only fire for chat view instance
384
if (widget.location === ChatAgentLocation.Panel &&
385
typeof widget.viewContext === 'object' &&
386
'viewId' in widget.viewContext &&
387
widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID) {
388
this._onDidChange.fire();
389
390
// Listen for view model changes on this widget
391
this._register(widget.onDidChangeViewModel(() => {
392
this._onDidChange.fire();
393
if (widget.viewModel) {
394
this.registerProgressListener(widget.viewModel.model.requestInProgressObs);
395
}
396
}));
397
398
// Listen for title changes on the current model
399
this.registerModelTitleListener(widget);
400
if (widget.viewModel) {
401
this.registerProgressListener(widget.viewModel.model.requestInProgressObs);
402
}
403
}
404
}));
405
406
// Check for existing chat widgets and register listeners
407
const existingWidgets = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel)
408
.filter(widget => typeof widget.viewContext === 'object' && 'viewId' in widget.viewContext && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID);
409
410
existingWidgets.forEach(widget => {
411
this._register(widget.onDidChangeViewModel(() => {
412
this._onDidChange.fire();
413
this.registerModelTitleListener(widget);
414
}));
415
416
// Register title listener for existing widget
417
this.registerModelTitleListener(widget);
418
if (widget.viewModel) {
419
this.registerProgressListener(widget.viewModel.model.requestInProgressObs);
420
}
421
});
422
}
423
424
private registerProgressListener(observable: IObservable<boolean>) {
425
const progressEvent = Event.fromObservableLight(observable);
426
this._register(progressEvent(() => {
427
this._onDidChangeChatSessionItems.fire();
428
}));
429
}
430
431
private registerModelTitleListener(widget: IChatWidget): void {
432
const model = widget.viewModel?.model;
433
if (model) {
434
// Listen for model changes, specifically for title changes via setCustomTitle
435
this._register(model.onDidChange((e) => {
436
// Fire change events for all title-related changes to refresh the tree
437
if (!e || e.kind === 'setCustomTitle') {
438
this._onDidChange.fire();
439
}
440
}));
441
}
442
}
443
444
private initializeCurrentEditorSet(): void {
445
this.currentEditorSet.clear();
446
this.editorOrder = []; // Reset the order
447
448
this.editorGroupService.groups.forEach(group => {
449
group.editors.forEach(editor => {
450
if (this.isLocalChatSession(editor)) {
451
const key = this.getEditorKey(editor, group);
452
this.currentEditorSet.add(key);
453
this.editorOrder.push(key);
454
}
455
});
456
});
457
}
458
459
private getEditorKey(editor: EditorInput, group: IEditorGroup): string {
460
return `${group.id}-${editor.typeId}-${editor.resource?.toString() || editor.getName()}`;
461
}
462
463
private isLocalChatSession(editor?: EditorInput): boolean {
464
// For the LocalChatSessionsProvider, we only want to track sessions that are actually 'local' type
465
if (!isChatSession(editor)) {
466
return false;
467
}
468
469
if (!(editor instanceof ChatEditorInput)) {
470
return false;
471
}
472
473
const sessionType = getChatSessionType(editor);
474
return sessionType === 'local';
475
}
476
477
private modelToStatus(model: IChatModel): ChatSessionStatus | undefined {
478
if (model.requestInProgress) {
479
return ChatSessionStatus.InProgress;
480
} else {
481
const requests = model.getRequests();
482
if (requests.length > 0) {
483
// Check if the last request was completed successfully or failed
484
const lastRequest = requests[requests.length - 1];
485
if (lastRequest && lastRequest.response) {
486
if (lastRequest.response.isCanceled || lastRequest.response.result?.errorDetails) {
487
return ChatSessionStatus.Failed;
488
} else if (lastRequest.response.isComplete) {
489
return ChatSessionStatus.Completed;
490
} else {
491
return ChatSessionStatus.InProgress;
492
}
493
}
494
}
495
}
496
return;
497
}
498
499
async provideChatSessionItems(token: CancellationToken): Promise<IChatSessionItem[]> {
500
const sessions: ChatSessionItemWithProvider[] = [];
501
// Create a map to quickly find editors by their key
502
const editorMap = new Map<string, { editor: EditorInput; group: IEditorGroup }>();
503
504
this.editorGroupService.groups.forEach(group => {
505
group.editors.forEach(editor => {
506
if (editor instanceof ChatEditorInput) {
507
const key = this.getEditorKey(editor, group);
508
editorMap.set(key, { editor, group });
509
}
510
});
511
});
512
513
// Add chat view instance
514
const chatWidget = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel)
515
.find(widget => typeof widget.viewContext === 'object' && 'viewId' in widget.viewContext && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID);
516
const status = chatWidget?.viewModel?.model ? this.modelToStatus(chatWidget.viewModel.model) : undefined;
517
const widgetSession: ILocalChatSessionItem & ChatSessionItemWithProvider = {
518
id: LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID,
519
label: chatWidget?.viewModel?.model.title || nls.localize2('chat.sessions.chatView', "Chat").value,
520
description: nls.localize('chat.sessions.chatView.description', "Chat View"),
521
iconPath: Codicon.chatSparkle,
522
widget: chatWidget,
523
sessionType: 'widget',
524
status,
525
provider: this
526
};
527
sessions.push(widgetSession);
528
529
// Build editor-based sessions in the order specified by editorOrder
530
this.editorOrder.forEach((editorKey, index) => {
531
const editorInfo = editorMap.get(editorKey);
532
if (editorInfo) {
533
const sessionId = `local-${editorInfo.group.id}-${index}`;
534
535
// Determine status and timestamp for editor-based session
536
let status: ChatSessionStatus | undefined;
537
let timestamp: number | undefined;
538
if (editorInfo.editor instanceof ChatEditorInput && editorInfo.editor.sessionId) {
539
const model = this.chatService.getSession(editorInfo.editor.sessionId);
540
if (model) {
541
status = this.modelToStatus(model);
542
// Get the last interaction timestamp from the model
543
const requests = model.getRequests();
544
if (requests.length > 0) {
545
const lastRequest = requests[requests.length - 1];
546
timestamp = lastRequest.timestamp;
547
} else {
548
// Fallback to current time if no requests yet
549
timestamp = Date.now();
550
}
551
}
552
}
553
554
const editorSession: ILocalChatSessionItem & ChatSessionItemWithProvider = {
555
id: sessionId,
556
label: editorInfo.editor.getName(),
557
iconPath: Codicon.chatSparkle,
558
editor: editorInfo.editor,
559
group: editorInfo.group,
560
sessionType: 'editor',
561
status,
562
provider: this,
563
timing: {
564
startTime: timestamp ?? 0
565
}
566
};
567
sessions.push(editorSession);
568
}
569
});
570
571
// Add "Show history..." node at the end
572
return [...sessions, historyNode];
573
}
574
}
575
576
const historyNode: IChatSessionItem = {
577
id: 'show-history',
578
label: nls.localize('chat.sessions.showHistory', "History"),
579
};
580
581
// Chat sessions container
582
class ChatSessionsViewPaneContainer extends ViewPaneContainer {
583
private registeredViewDescriptors: Map<string, IViewDescriptor> = new Map();
584
585
constructor(
586
private readonly sessionTracker: ChatSessionTracker,
587
@IInstantiationService instantiationService: IInstantiationService,
588
@IConfigurationService configurationService: IConfigurationService,
589
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
590
@IContextMenuService contextMenuService: IContextMenuService,
591
@ITelemetryService telemetryService: ITelemetryService,
592
@IExtensionService extensionService: IExtensionService,
593
@IThemeService themeService: IThemeService,
594
@IStorageService storageService: IStorageService,
595
@IWorkspaceContextService contextService: IWorkspaceContextService,
596
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
597
@ILogService logService: ILogService,
598
@IProductService private readonly productService: IProductService,
599
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
600
) {
601
super(
602
VIEWLET_ID,
603
{
604
mergeViewWithContainerWhenSingleView: false,
605
},
606
instantiationService,
607
configurationService,
608
layoutService,
609
contextMenuService,
610
telemetryService,
611
extensionService,
612
themeService,
613
storageService,
614
contextService,
615
viewDescriptorService,
616
logService
617
);
618
619
this.updateViewRegistration();
620
621
// Listen for provider changes and register/unregister views accordingly
622
this._register(this.chatSessionsService.onDidChangeItemsProviders(() => {
623
this.updateViewRegistration();
624
}));
625
626
// Listen for session items changes and refresh the appropriate provider tree
627
this._register(this.chatSessionsService.onDidChangeSessionItems((chatSessionType) => {
628
this.refreshProviderTree(chatSessionType);
629
}));
630
631
// Listen for contribution availability changes and update view registration
632
this._register(this.chatSessionsService.onDidChangeAvailability(() => {
633
this.updateViewRegistration();
634
}));
635
}
636
637
override getTitle(): string {
638
const title = nls.localize('chat.sessions.title', "Chat Sessions");
639
return title;
640
}
641
642
private getAllChatSessionItemProviders(): IChatSessionItemProvider[] {
643
return Array.from(this.chatSessionsService.getAllChatSessionItemProviders());
644
}
645
646
private refreshProviderTree(chatSessionType: string): void {
647
// Find the provider with the matching chatSessionType
648
const providers = this.getAllChatSessionItemProviders();
649
const targetProvider = providers.find(provider => provider.chatSessionType === chatSessionType);
650
651
if (targetProvider) {
652
// Find the corresponding view and refresh its tree
653
const viewId = `${VIEWLET_ID}.${chatSessionType}`;
654
const view = this.getView(viewId) as SessionsViewPane | undefined;
655
if (view) {
656
view.refreshTree();
657
}
658
}
659
}
660
661
private async updateViewRegistration(): Promise<void> {
662
// prepare all chat session providers
663
const contributions = this.chatSessionsService.getAllChatSessionContributions();
664
await Promise.all(contributions.map(contrib => this.chatSessionsService.canResolveItemProvider(contrib.type)));
665
const currentProviders = this.getAllChatSessionItemProviders();
666
const currentProviderIds = new Set(currentProviders.map(p => p.chatSessionType));
667
668
// Find views that need to be unregistered (providers that are no longer available)
669
const viewsToUnregister: IViewDescriptor[] = [];
670
for (const [providerId, viewDescriptor] of this.registeredViewDescriptors.entries()) {
671
if (!currentProviderIds.has(providerId)) {
672
viewsToUnregister.push(viewDescriptor);
673
this.registeredViewDescriptors.delete(providerId);
674
}
675
}
676
677
// Unregister removed views
678
if (viewsToUnregister.length > 0) {
679
const container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(VIEWLET_ID);
680
if (container) {
681
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).deregisterViews(viewsToUnregister, container);
682
}
683
}
684
685
// Register new views
686
this.registerViews(contributions);
687
}
688
689
private async registerViews(extensionPointContributions: IChatSessionsExtensionPoint[]) {
690
const container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(VIEWLET_ID);
691
const providers = this.getAllChatSessionItemProviders();
692
693
if (container && providers.length > 0) {
694
const viewDescriptorsToRegister: IViewDescriptor[] = [];
695
696
// Separate providers by type and prepare display names
697
const localProvider = providers.find(p => p.chatSessionType === 'local');
698
const historyProvider = providers.find(p => p.chatSessionType === 'history');
699
const otherProviders = providers.filter(p => p.chatSessionType !== 'local' && p.chatSessionType !== 'history');
700
701
// Sort other providers alphabetically by display name
702
const providersWithDisplayNames = otherProviders.map(provider => {
703
const extContribution = extensionPointContributions.find(c => c.type === provider.chatSessionType);
704
if (!extContribution) {
705
this.logService.warn(`No extension contribution found for chat session type: ${provider.chatSessionType}`);
706
return null;
707
}
708
return {
709
provider,
710
displayName: extContribution.displayName
711
};
712
}).filter(item => item !== null) as Array<{ provider: IChatSessionItemProvider; displayName: string }>;
713
714
// Sort alphabetically by display name
715
providersWithDisplayNames.sort((a, b) => a.displayName.localeCompare(b.displayName));
716
717
// Register views in priority order: local, history, then alphabetically sorted others
718
const orderedProviders = [
719
...(localProvider ? [{ provider: localProvider, displayName: 'Local Chat Sessions', baseOrder: 0 }] : []),
720
...(historyProvider ? [{ provider: historyProvider, displayName: 'History', baseOrder: 1, when: undefined }] : []),
721
...providersWithDisplayNames.map((item, index) => ({
722
...item,
723
baseOrder: 2 + index, // Start from 2 for other providers
724
when: undefined,
725
}))
726
];
727
728
orderedProviders.forEach(({ provider, displayName, baseOrder, when }) => {
729
// Only register if not already registered
730
if (!this.registeredViewDescriptors.has(provider.chatSessionType)) {
731
const viewDescriptor: IViewDescriptor = {
732
id: `${VIEWLET_ID}.${provider.chatSessionType}`,
733
name: {
734
value: displayName,
735
original: displayName,
736
},
737
ctorDescriptor: new SyncDescriptor(SessionsViewPane, [provider, this.sessionTracker]),
738
canToggleVisibility: true,
739
canMoveView: true,
740
order: baseOrder, // Use computed order based on priority and alphabetical sorting
741
when,
742
};
743
744
viewDescriptorsToRegister.push(viewDescriptor);
745
this.registeredViewDescriptors.set(provider.chatSessionType, viewDescriptor);
746
747
if (provider.chatSessionType === 'local') {
748
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
749
this._register(viewsRegistry.registerViewWelcomeContent(viewDescriptor.id, {
750
content: nls.localize('chatSessions.noResults', "No local chat sessions\n[Start a Chat](command:workbench.action.openChat)"),
751
}));
752
}
753
}
754
});
755
756
const gettingStartedViewId = `${VIEWLET_ID}.gettingStarted`;
757
if (!this.registeredViewDescriptors.has('gettingStarted')
758
&& this.productService.chatSessionRecommendations
759
&& this.productService.chatSessionRecommendations.length) {
760
const gettingStartedDescriptor: IViewDescriptor = {
761
id: gettingStartedViewId,
762
name: {
763
value: nls.localize('chat.sessions.gettingStarted', "Getting Started"),
764
original: 'Getting Started',
765
},
766
ctorDescriptor: new SyncDescriptor(SessionsViewPane, [null, this.sessionTracker]),
767
canToggleVisibility: true,
768
canMoveView: true,
769
order: 1000,
770
collapsed: !!otherProviders.length,
771
};
772
viewDescriptorsToRegister.push(gettingStartedDescriptor);
773
this.registeredViewDescriptors.set('gettingStarted', gettingStartedDescriptor);
774
}
775
776
if (viewDescriptorsToRegister.length > 0) {
777
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).registerViews(viewDescriptorsToRegister, container);
778
}
779
}
780
}
781
782
override dispose(): void {
783
// Unregister all views before disposal
784
if (this.registeredViewDescriptors.size > 0) {
785
const container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(VIEWLET_ID);
786
if (container) {
787
const allRegisteredViews = Array.from(this.registeredViewDescriptors.values());
788
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).deregisterViews(allRegisteredViews, container);
789
}
790
this.registeredViewDescriptors.clear();
791
}
792
793
super.dispose();
794
}
795
}
796
797
798
// Chat sessions item data source for the tree
799
class SessionsDataSource implements IAsyncDataSource<IChatSessionItemProvider, ChatSessionItemWithProvider> {
800
801
constructor(
802
private readonly provider: IChatSessionItemProvider,
803
private readonly chatService: IChatService,
804
private readonly sessionTracker: ChatSessionTracker,
805
) {
806
}
807
808
hasChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider): boolean {
809
const isProvider = element === this.provider;
810
if (isProvider) {
811
// Root provider always has children
812
return true;
813
}
814
815
// Check if this is the "Show history..." node
816
if ('id' in element && element.id === historyNode.id) {
817
return true;
818
}
819
820
return false;
821
}
822
823
async getChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider): Promise<ChatSessionItemWithProvider[]> {
824
if (element === this.provider) {
825
try {
826
const items = await this.provider.provideChatSessionItems(CancellationToken.None);
827
const itemsWithProvider = items.map(item => {
828
const itemWithProvider: ChatSessionItemWithProvider = { ...item, provider: this.provider };
829
830
// Extract timestamp using the helper function
831
itemWithProvider.timing = { startTime: extractTimestamp(item) ?? 0 };
832
833
return itemWithProvider;
834
});
835
836
// Add hybrid local editor sessions for this provider using the centralized service
837
if (this.provider.chatSessionType !== 'local') {
838
const hybridSessions = await this.sessionTracker.getHybridSessionsForProvider(this.provider);
839
itemsWithProvider.push(...(hybridSessions as ChatSessionItemWithProvider[]));
840
}
841
842
// For non-local providers, apply time-based sorting and grouping
843
if (this.provider.chatSessionType !== 'local') {
844
processSessionsWithTimeGrouping(itemsWithProvider);
845
}
846
847
return itemsWithProvider;
848
} catch (error) {
849
return [];
850
}
851
}
852
853
// Check if this is the "Show history..." node
854
if ('id' in element && element.id === historyNode.id) {
855
return this.getHistoryItems();
856
}
857
858
// Individual session items don't have children
859
return [];
860
}
861
862
private async getHistoryItems(): Promise<ChatSessionItemWithProvider[]> {
863
try {
864
// Get all chat history
865
const allHistory = await this.chatService.getHistory();
866
867
// Create history items with provider reference and timestamps
868
const historyItems = allHistory.map((historyDetail: any): ChatSessionItemWithProvider => ({
869
id: `history-${historyDetail.sessionId}`,
870
label: historyDetail.title,
871
iconPath: Codicon.chatSparkle,
872
provider: this.provider,
873
timing: {
874
startTime: historyDetail.lastMessageDate ?? Date.now()
875
}
876
}));
877
878
// Apply sorting and time grouping
879
processSessionsWithTimeGrouping(historyItems);
880
881
return historyItems;
882
883
} catch (error) {
884
return [];
885
}
886
}
887
}
888
889
// Tree delegate for session items
890
class SessionsDelegate implements IListVirtualDelegate<ChatSessionItemWithProvider> {
891
static readonly ITEM_HEIGHT = 22;
892
static readonly ITEM_HEIGHT_WITH_DESCRIPTION = 44; // Slightly smaller for cleaner look
893
894
constructor(private readonly configurationService: IConfigurationService) { }
895
896
getHeight(element: ChatSessionItemWithProvider): number {
897
// Return consistent height for all items (single-line layout)
898
if (element.description && this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription) && element.provider.chatSessionType !== 'local') {
899
return SessionsDelegate.ITEM_HEIGHT_WITH_DESCRIPTION;
900
} else {
901
return SessionsDelegate.ITEM_HEIGHT;
902
}
903
}
904
905
getTemplateId(element: ChatSessionItemWithProvider): string {
906
return SessionsRenderer.TEMPLATE_ID;
907
}
908
}
909
910
// Template data for session items
911
interface ISessionTemplateData {
912
readonly container: HTMLElement;
913
readonly resourceLabel: IResourceLabel;
914
readonly actionBar: ActionBar;
915
readonly elementDisposable: DisposableStore;
916
readonly timestamp: HTMLElement;
917
readonly descriptionRow: HTMLElement;
918
readonly descriptionLabel: HTMLElement;
919
readonly statisticsLabel: HTMLElement;
920
}
921
922
// Renderer for session items in the tree
923
class SessionsRenderer extends Disposable implements ITreeRenderer<IChatSessionItem, FuzzyScore, ISessionTemplateData> {
924
static readonly TEMPLATE_ID = 'session';
925
private appliedIconColorStyles = new Set<string>();
926
private markdownRenderer: MarkdownRenderer;
927
928
constructor(
929
private readonly labels: ResourceLabels,
930
@IThemeService private readonly themeService: IThemeService,
931
@ILogService private readonly logService: ILogService,
932
@IContextViewService private readonly contextViewService: IContextViewService,
933
@IConfigurationService private readonly configurationService: IConfigurationService,
934
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
935
@IMenuService private readonly menuService: IMenuService,
936
@IContextKeyService private readonly contextKeyService: IContextKeyService,
937
@IHoverService private readonly hoverService: IHoverService,
938
@IInstantiationService instantiationService: IInstantiationService,
939
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
940
@IChatService private readonly chatService: IChatService,
941
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
942
) {
943
super();
944
945
// Listen for theme changes to clear applied styles
946
this._register(this.themeService.onDidColorThemeChange(() => {
947
this.appliedIconColorStyles.clear();
948
}));
949
950
this.markdownRenderer = instantiationService.createInstance(MarkdownRenderer, {});
951
}
952
953
private applyIconColorStyle(iconId: string, colorId: string): void {
954
const styleKey = `${iconId}-${colorId}`;
955
if (this.appliedIconColorStyles.has(styleKey)) {
956
return; // Already applied
957
}
958
959
const colorTheme = this.themeService.getColorTheme();
960
const color = colorTheme.getColor(colorId);
961
962
if (color) {
963
// Target the ::before pseudo-element where the actual icon is rendered
964
const css = `.monaco-workbench .chat-session-item .monaco-icon-label.codicon-${iconId}::before { color: ${color} !important; }`;
965
const activeWindow = getActiveWindow();
966
967
const styleId = `chat-sessions-icon-${styleKey}`;
968
const existingStyle = activeWindow.document.getElementById(styleId);
969
if (existingStyle) {
970
existingStyle.textContent = css;
971
} else {
972
const styleElement = activeWindow.document.createElement('style');
973
styleElement.id = styleId;
974
styleElement.textContent = css;
975
activeWindow.document.head.appendChild(styleElement);
976
977
// Clean up on dispose
978
this._register({
979
dispose: () => {
980
const activeWin = getActiveWindow();
981
const style = activeWin.document.getElementById(styleId);
982
if (style) {
983
style.remove();
984
}
985
}
986
});
987
}
988
989
this.appliedIconColorStyles.add(styleKey);
990
} else {
991
this.logService.debug('No color found for colorId:', colorId);
992
}
993
}
994
995
private isLocalChatSessionItem(item: IChatSessionItem): item is ILocalChatSessionItem {
996
return ('editor' in item && 'group' in item) || ('widget' in item && 'sessionType' in item);
997
}
998
999
get templateId(): string {
1000
return SessionsRenderer.TEMPLATE_ID;
1001
}
1002
1003
renderTemplate(container: HTMLElement): ISessionTemplateData {
1004
const element = append(container, $('.chat-session-item'));
1005
1006
// Create a container that holds the label, timestamp, and actions
1007
const contentContainer = append(element, $('.session-content'));
1008
const resourceLabel = this.labels.create(contentContainer, { supportHighlights: true });
1009
const descriptionRow = append(element, $('.description-row'));
1010
const descriptionLabel = append(descriptionRow, $('span.description'));
1011
const statisticsLabel = append(descriptionRow, $('span.statistics'));
1012
1013
// Create timestamp container and element
1014
const timestampContainer = append(contentContainer, $('.timestamp-container'));
1015
const timestamp = append(timestampContainer, $('.timestamp'));
1016
1017
const actionsContainer = append(contentContainer, $('.actions'));
1018
const actionBar = new ActionBar(actionsContainer);
1019
const elementDisposable = new DisposableStore();
1020
1021
return {
1022
container: element,
1023
resourceLabel,
1024
actionBar,
1025
elementDisposable,
1026
timestamp,
1027
descriptionRow,
1028
descriptionLabel,
1029
statisticsLabel,
1030
};
1031
}
1032
1033
statusToIcon(status?: ChatSessionStatus) {
1034
switch (status) {
1035
case ChatSessionStatus.InProgress:
1036
return Codicon.loading;
1037
case ChatSessionStatus.Completed:
1038
return Codicon.pass;
1039
case ChatSessionStatus.Failed:
1040
return Codicon.error;
1041
default:
1042
return Codicon.circleOutline;
1043
}
1044
1045
}
1046
1047
renderElement(element: ITreeNode<IChatSessionItem, FuzzyScore>, index: number, templateData: ISessionTemplateData): void {
1048
const session = element.element;
1049
const sessionWithProvider = session as ChatSessionItemWithProvider;
1050
1051
// Add CSS class for local sessions
1052
if (sessionWithProvider.provider.chatSessionType === 'local') {
1053
templateData.container.classList.add('local-session');
1054
} else {
1055
templateData.container.classList.remove('local-session');
1056
}
1057
1058
// Get the actual session ID for editable data lookup
1059
let actualSessionId: string | undefined;
1060
if (this.isLocalChatSessionItem(session)) {
1061
if (session.sessionType === 'editor' && session.editor instanceof ChatEditorInput) {
1062
actualSessionId = session.editor.sessionId;
1063
} else if (session.sessionType === 'widget' && session.widget) {
1064
actualSessionId = session.widget.viewModel?.model.sessionId;
1065
}
1066
} else if (session.id.startsWith('history-')) {
1067
// For history items, extract the actual session ID by removing the 'history-' prefix
1068
actualSessionId = session.id.substring('history-'.length);
1069
}
1070
1071
// Check if this session is being edited using the actual session ID
1072
const editableData = actualSessionId ? this.chatSessionsService.getEditableData(actualSessionId) : undefined;
1073
if (editableData) {
1074
// Render input box for editing
1075
templateData.actionBar.clear();
1076
const editDisposable = this.renderInputBox(templateData.container, session, editableData);
1077
templateData.elementDisposable.add(editDisposable);
1078
return;
1079
}
1080
1081
// Normal rendering - clear the action bar in case it was used for editing
1082
templateData.actionBar.clear();
1083
1084
// Handle different icon types
1085
let iconResource: URI | undefined;
1086
let iconTheme: ThemeIcon | undefined;
1087
if (!session.iconPath && session.id !== historyNode.id) {
1088
iconTheme = this.statusToIcon(session.status);
1089
} else {
1090
iconTheme = session.iconPath;
1091
}
1092
1093
if (iconTheme?.color?.id) {
1094
this.applyIconColorStyle(iconTheme.id, iconTheme.color.id);
1095
}
1096
1097
const renderDescriptionOnSecondRow = this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription) && sessionWithProvider.provider.chatSessionType !== 'local';
1098
1099
if (renderDescriptionOnSecondRow && session.description) {
1100
templateData.container.classList.toggle('multiline', true);
1101
templateData.descriptionRow.style.display = 'flex';
1102
if (typeof session.description === 'string') {
1103
templateData.descriptionLabel.textContent = session.description;
1104
} else {
1105
templateData.elementDisposable.add(this.markdownRenderer.render(session.description, {
1106
sanitizerConfig: {
1107
replaceWithPlaintext: true,
1108
allowedTags: {
1109
override: allowedChatMarkdownHtmlTags,
1110
},
1111
allowedLinkSchemes: { augment: [product.urlProtocol] }
1112
},
1113
}, templateData.descriptionLabel));
1114
templateData.elementDisposable.add(DOM.addDisposableListener(templateData.descriptionLabel, 'mousedown', e => e.stopPropagation()));
1115
templateData.elementDisposable.add(DOM.addDisposableListener(templateData.descriptionLabel, 'click', e => e.stopPropagation()));
1116
templateData.elementDisposable.add(DOM.addDisposableListener(templateData.descriptionLabel, 'auxclick', e => e.stopPropagation()));
1117
}
1118
1119
DOM.clearNode(templateData.statisticsLabel);
1120
const insertionNode = append(templateData.statisticsLabel, $('span.insertions'));
1121
insertionNode.textContent = session.statistics ? `+${session.statistics.insertions}` : '';
1122
const deletionNode = append(templateData.statisticsLabel, $('span.deletions'));
1123
deletionNode.textContent = session.statistics ? `-${session.statistics.deletions}` : '';
1124
} else {
1125
templateData.container.classList.toggle('multiline', false);
1126
}
1127
1128
// Prepare tooltip content
1129
const tooltipContent = 'tooltip' in session && session.tooltip ?
1130
(typeof session.tooltip === 'string' ? session.tooltip :
1131
isMarkdownString(session.tooltip) ? {
1132
markdown: session.tooltip,
1133
markdownNotSupportedFallback: session.tooltip.value
1134
} : undefined) :
1135
undefined;
1136
1137
// Set the resource label
1138
templateData.resourceLabel.setResource({
1139
name: session.label,
1140
description: !renderDescriptionOnSecondRow && 'description' in session && typeof session.description === 'string' ? session.description : '',
1141
resource: iconResource
1142
}, {
1143
fileKind: undefined,
1144
icon: iconTheme,
1145
// Set tooltip on resourceLabel only for single-row items
1146
title: !renderDescriptionOnSecondRow || !session.description ? tooltipContent : undefined
1147
});
1148
1149
// For two-row items, set tooltip on the container instead
1150
if (renderDescriptionOnSecondRow && session.description && tooltipContent) {
1151
if (typeof tooltipContent === 'string') {
1152
templateData.elementDisposable.add(
1153
this.hoverService.setupDelayedHover(templateData.container, { content: tooltipContent })
1154
);
1155
} else if (tooltipContent && typeof tooltipContent === 'object' && 'markdown' in tooltipContent) {
1156
templateData.elementDisposable.add(
1157
this.hoverService.setupDelayedHover(templateData.container, { content: tooltipContent.markdown })
1158
);
1159
}
1160
}
1161
1162
// Handle timestamp display and grouping
1163
const hasTimestamp = sessionWithProvider.timing?.startTime !== undefined;
1164
if (hasTimestamp) {
1165
templateData.timestamp.textContent = sessionWithProvider.relativeTime ?? '';
1166
templateData.timestamp.ariaLabel = sessionWithProvider.relativeTimeFullWord ?? '';
1167
templateData.timestamp.parentElement!.classList.toggle('timestamp-duplicate', sessionWithProvider.hideRelativeTime === true);
1168
templateData.timestamp.parentElement!.style.display = '';
1169
} else {
1170
// Hide timestamp container if no timestamp available
1171
templateData.timestamp.parentElement!.style.display = 'none';
1172
}
1173
1174
// Create context overlay for this specific session item
1175
const contextOverlay = getSessionItemContextOverlay(
1176
session,
1177
sessionWithProvider.provider,
1178
this.chatWidgetService,
1179
this.chatService,
1180
this.editorGroupsService
1181
);
1182
1183
const contextKeyService = this.contextKeyService.createOverlay(contextOverlay);
1184
1185
// Create menu for this session item
1186
const menu = templateData.elementDisposable.add(
1187
this.menuService.createMenu(MenuId.ChatSessionsMenu, contextKeyService)
1188
);
1189
1190
// Setup action bar with contributed actions
1191
const setupActionBar = () => {
1192
templateData.actionBar.clear();
1193
1194
// Create marshalled context for command execution
1195
const marshalledSession = {
1196
session: session,
1197
$mid: MarshalledId.ChatSessionContext
1198
};
1199
1200
const actions = menu.getActions({ arg: marshalledSession, shouldForwardArgs: true });
1201
1202
const { primary } = getActionBarActions(
1203
actions,
1204
'inline',
1205
);
1206
1207
templateData.actionBar.push(primary, { icon: true, label: false });
1208
1209
// Set context for the action bar
1210
templateData.actionBar.context = session;
1211
};
1212
1213
// Setup initial action bar and listen for menu changes
1214
templateData.elementDisposable.add(menu.onDidChange(() => setupActionBar()));
1215
setupActionBar();
1216
}
1217
1218
disposeElement(_element: ITreeNode<IChatSessionItem, FuzzyScore>, _index: number, templateData: ISessionTemplateData): void {
1219
templateData.elementDisposable.clear();
1220
templateData.resourceLabel.clear();
1221
templateData.actionBar.clear();
1222
}
1223
1224
private renderInputBox(container: HTMLElement, session: IChatSessionItem, editableData: IEditableData): DisposableStore {
1225
// Hide the existing resource label element and session content
1226
const existingResourceLabelElement = container.querySelector('.monaco-icon-label') as HTMLElement;
1227
if (existingResourceLabelElement) {
1228
existingResourceLabelElement.style.display = 'none';
1229
}
1230
1231
// Hide the session content container to avoid layout conflicts
1232
const sessionContentElement = container.querySelector('.session-content') as HTMLElement;
1233
if (sessionContentElement) {
1234
sessionContentElement.style.display = 'none';
1235
}
1236
1237
// Create a simple container that mimics the file explorer's structure
1238
const editContainer = DOM.append(container, DOM.$('.explorer-item.explorer-item-edited'));
1239
1240
// Add the icon
1241
const iconElement = DOM.append(editContainer, DOM.$('.codicon'));
1242
if (session.iconPath && ThemeIcon.isThemeIcon(session.iconPath)) {
1243
iconElement.classList.add(`codicon-${session.iconPath.id}`);
1244
} else {
1245
iconElement.classList.add('codicon-file'); // Default file icon
1246
}
1247
1248
// Create the input box directly
1249
const inputBox = new InputBox(editContainer, this.contextViewService, {
1250
validationOptions: {
1251
validation: (value) => {
1252
const message = editableData.validationMessage(value);
1253
if (!message || message.severity !== Severity.Error) {
1254
return null;
1255
}
1256
return {
1257
content: message.content,
1258
formatContent: true,
1259
type: MessageType.ERROR
1260
};
1261
}
1262
},
1263
ariaLabel: nls.localize('chatSessionInputAriaLabel', "Type session name. Press Enter to confirm or Escape to cancel."),
1264
inputBoxStyles: defaultInputBoxStyles,
1265
});
1266
1267
inputBox.value = session.label;
1268
inputBox.focus();
1269
inputBox.select({ start: 0, end: session.label.length });
1270
1271
const done = createSingleCallFunction((success: boolean, finishEditing: boolean) => {
1272
const value = inputBox.value;
1273
1274
// Clean up the edit container
1275
editContainer.style.display = 'none';
1276
editContainer.remove();
1277
1278
// Restore the original resource label
1279
if (existingResourceLabelElement) {
1280
existingResourceLabelElement.style.display = '';
1281
}
1282
1283
// Restore the session content container
1284
const sessionContentElement = container.querySelector('.session-content') as HTMLElement;
1285
if (sessionContentElement) {
1286
sessionContentElement.style.display = '';
1287
}
1288
1289
if (finishEditing) {
1290
editableData.onFinish(value, success);
1291
}
1292
});
1293
1294
const showInputBoxNotification = () => {
1295
if (inputBox.isInputValid()) {
1296
const message = editableData.validationMessage(inputBox.value);
1297
if (message) {
1298
inputBox.showMessage({
1299
content: message.content,
1300
formatContent: true,
1301
type: message.severity === Severity.Info ? MessageType.INFO : message.severity === Severity.Warning ? MessageType.WARNING : MessageType.ERROR
1302
});
1303
} else {
1304
inputBox.hideMessage();
1305
}
1306
}
1307
};
1308
showInputBoxNotification();
1309
1310
const disposables: IDisposable[] = [
1311
inputBox,
1312
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
1313
if (e.equals(KeyCode.Enter)) {
1314
if (!inputBox.validate()) {
1315
done(true, true);
1316
}
1317
} else if (e.equals(KeyCode.Escape)) {
1318
done(false, true);
1319
}
1320
}),
1321
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_UP, () => {
1322
showInputBoxNotification();
1323
}),
1324
DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, async () => {
1325
while (true) {
1326
await timeout(0);
1327
1328
const ownerDocument = inputBox.inputElement.ownerDocument;
1329
if (!ownerDocument.hasFocus()) {
1330
break;
1331
}
1332
if (DOM.isActiveElement(inputBox.inputElement)) {
1333
return;
1334
} else if (DOM.isHTMLElement(ownerDocument.activeElement) && DOM.hasParentWithClass(ownerDocument.activeElement, 'context-view')) {
1335
// Do nothing - context menu is open
1336
} else {
1337
break;
1338
}
1339
}
1340
1341
done(inputBox.isInputValid(), true);
1342
})
1343
];
1344
1345
const disposableStore = new DisposableStore();
1346
disposables.forEach(d => disposableStore.add(d));
1347
disposableStore.add(toDisposable(() => done(false, false)));
1348
return disposableStore;
1349
}
1350
1351
disposeTemplate(templateData: ISessionTemplateData): void {
1352
templateData.elementDisposable.dispose();
1353
templateData.resourceLabel.dispose();
1354
templateData.actionBar.dispose();
1355
}
1356
}
1357
1358
// Identity provider for session items
1359
class SessionsIdentityProvider {
1360
getId(element: ChatSessionItemWithProvider): string {
1361
return element.id;
1362
}
1363
}
1364
1365
// Accessibility provider for session items
1366
class SessionsAccessibilityProvider {
1367
getWidgetAriaLabel(): string {
1368
return nls.localize('chatSessions', 'Chat Sessions');
1369
}
1370
1371
getAriaLabel(element: ChatSessionItemWithProvider): string | null {
1372
return element.label || element.id;
1373
}
1374
}
1375
1376
class SessionsViewPane extends ViewPane {
1377
private tree: WorkbenchAsyncDataTree<IChatSessionItemProvider, ChatSessionItemWithProvider, FuzzyScore> | undefined;
1378
private list: WorkbenchList<IGettingStartedItem> | undefined;
1379
private treeContainer: HTMLElement | undefined;
1380
private messageElement?: HTMLElement;
1381
private _isEmpty: boolean = true;
1382
1383
constructor(
1384
private readonly provider: IChatSessionItemProvider,
1385
private readonly sessionTracker: ChatSessionTracker,
1386
options: IViewPaneOptions,
1387
@IKeybindingService keybindingService: IKeybindingService,
1388
@IContextMenuService contextMenuService: IContextMenuService,
1389
@IConfigurationService configurationService: IConfigurationService,
1390
@IContextKeyService contextKeyService: IContextKeyService,
1391
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
1392
@IInstantiationService instantiationService: IInstantiationService,
1393
@IOpenerService openerService: IOpenerService,
1394
@IThemeService themeService: IThemeService,
1395
@IHoverService hoverService: IHoverService,
1396
@IChatService private readonly chatService: IChatService,
1397
@IEditorService private readonly editorService: IEditorService,
1398
@IViewsService private readonly viewsService: IViewsService,
1399
@ILogService private readonly logService: ILogService,
1400
@IProgressService private readonly progressService: IProgressService,
1401
@IMenuService private readonly menuService: IMenuService,
1402
@ICommandService private readonly commandService: ICommandService,
1403
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
1404
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
1405
) {
1406
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
1407
1408
// Listen for changes in the provider if it's a LocalChatSessionsProvider
1409
if (provider instanceof LocalChatSessionsProvider) {
1410
this._register(provider.onDidChange(() => {
1411
if (this.tree && this.isBodyVisible()) {
1412
this.refreshTreeWithProgress();
1413
}
1414
}));
1415
}
1416
1417
// Listen for configuration changes to refresh view when description display changes
1418
this._register(this.configurationService.onDidChangeConfiguration(e => {
1419
if (e.affectsConfiguration(ChatConfiguration.ShowAgentSessionsViewDescription)) {
1420
if (this.tree && this.isBodyVisible()) {
1421
this.refreshTreeWithProgress();
1422
}
1423
}
1424
}));
1425
}
1426
1427
override shouldShowWelcome(): boolean {
1428
return this._isEmpty;
1429
}
1430
1431
private isLocalChatSessionItem(item: IChatSessionItem): item is ILocalChatSessionItem {
1432
return ('editor' in item && 'group' in item) || ('widget' in item && 'sessionType' in item);
1433
}
1434
1435
public refreshTree(): void {
1436
if (this.tree && this.isBodyVisible()) {
1437
this.refreshTreeWithProgress();
1438
}
1439
}
1440
1441
private isEmpty() {
1442
// Check if the tree has the provider node and get its children count
1443
if (!this.tree?.hasNode(this.provider)) {
1444
return true;
1445
}
1446
const providerNode = this.tree.getNode(this.provider);
1447
const childCount = providerNode.children?.length || 0;
1448
1449
return childCount === 0;
1450
}
1451
1452
/**
1453
* Updates the empty state message based on current tree data.
1454
* Uses the tree's existing data to avoid redundant provider calls.
1455
*/
1456
private updateEmptyState(): void {
1457
try {
1458
const newEmptyState = this.isEmpty();
1459
if (newEmptyState !== this._isEmpty) {
1460
this._isEmpty = newEmptyState;
1461
this._onDidChangeViewWelcomeState.fire();
1462
}
1463
} catch (error) {
1464
this.logService.error('Error checking tree data for empty state:', error);
1465
}
1466
}
1467
1468
/**
1469
* Refreshes the tree data with progress indication.
1470
* Shows a progress indicator while the tree updates its children from the provider.
1471
*/
1472
private async refreshTreeWithProgress(): Promise<void> {
1473
if (!this.tree) {
1474
return;
1475
}
1476
1477
try {
1478
await this.progressService.withProgress(
1479
{
1480
location: this.id, // Use the view ID as the progress location
1481
title: nls.localize('chatSessions.refreshing', 'Refreshing chat sessions...'),
1482
},
1483
async () => {
1484
await this.tree!.updateChildren(this.provider);
1485
}
1486
);
1487
1488
// Check for empty state after refresh using tree data
1489
this.updateEmptyState();
1490
} catch (error) {
1491
// Log error but don't throw to avoid breaking the UI
1492
this.logService.error('Error refreshing chat sessions tree:', error);
1493
}
1494
}
1495
1496
/**
1497
* Loads initial tree data with progress indication.
1498
* Shows a progress indicator while the tree loads data from the provider.
1499
*/
1500
private async loadDataWithProgress(): Promise<void> {
1501
if (!this.tree) {
1502
return;
1503
}
1504
1505
try {
1506
await this.progressService.withProgress(
1507
{
1508
location: this.id, // Use the view ID as the progress location
1509
title: nls.localize('chatSessions.loading', 'Loading chat sessions...'),
1510
},
1511
async () => {
1512
await this.tree!.setInput(this.provider);
1513
}
1514
);
1515
1516
// Check for empty state after loading using tree data
1517
this.updateEmptyState();
1518
} catch (error) {
1519
// Log error but don't throw to avoid breaking the UI
1520
this.logService.error('Error loading chat sessions data:', error);
1521
}
1522
}
1523
1524
protected override renderBody(container: HTMLElement): void {
1525
super.renderBody(container);
1526
1527
// For Getting Started view (null provider), show simple list
1528
if (this.provider === null) {
1529
this.renderGettingStartedList(container);
1530
return;
1531
}
1532
1533
this.treeContainer = DOM.append(container, DOM.$('.chat-sessions-tree-container'));
1534
// Create message element for empty state
1535
this.messageElement = append(container, $('.chat-sessions-message'));
1536
this.messageElement.style.display = 'none';
1537
// Create the tree components
1538
const dataSource = new SessionsDataSource(this.provider, this.chatService, this.sessionTracker);
1539
const delegate = new SessionsDelegate(this.configurationService);
1540
const identityProvider = new SessionsIdentityProvider();
1541
const accessibilityProvider = new SessionsAccessibilityProvider();
1542
1543
// Use the existing ResourceLabels service for consistent styling
1544
const labels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
1545
const renderer = this.instantiationService.createInstance(SessionsRenderer, labels);
1546
this._register(renderer);
1547
1548
const getResourceForElement = (element: ChatSessionItemWithProvider): URI | null => {
1549
if (this.isLocalChatSessionItem(element)) {
1550
return null;
1551
}
1552
1553
if (element.provider.chatSessionType === 'local') {
1554
const actualSessionId = element.id.startsWith('history-') ? element.id.substring('history-'.length) : element.id;
1555
return ChatSessionUri.forSession(element.provider.chatSessionType, actualSessionId);
1556
}
1557
1558
return ChatSessionUri.forSession(element.provider.chatSessionType, element.id);
1559
};
1560
1561
this.tree = this.instantiationService.createInstance(
1562
WorkbenchAsyncDataTree,
1563
'ChatSessions',
1564
this.treeContainer,
1565
delegate,
1566
[renderer],
1567
dataSource,
1568
{
1569
dnd: {
1570
onDragStart: (data, originalEvent) => {
1571
try {
1572
const elements = data.getData() as ChatSessionItemWithProvider[];
1573
const uris = coalesce(elements.map(getResourceForElement));
1574
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, uris, originalEvent));
1575
} catch {
1576
// noop
1577
}
1578
},
1579
getDragURI: (element: ChatSessionItemWithProvider) => {
1580
if (element.id === historyNode.id) {
1581
return null;
1582
}
1583
return getResourceForElement(element)?.toString() ?? null;
1584
},
1585
getDragLabel: (elements: ChatSessionItemWithProvider[]) => {
1586
if (elements.length === 1) {
1587
return elements[0].label;
1588
}
1589
return nls.localize('chatSessions.dragLabel', "{0} chat sessions", elements.length);
1590
},
1591
drop: () => { },
1592
onDragOver: () => false,
1593
dispose: () => { },
1594
},
1595
accessibilityProvider,
1596
identityProvider,
1597
multipleSelectionSupport: false,
1598
overrideStyles: {
1599
listBackground: undefined
1600
},
1601
setRowLineHeight: false
1602
1603
}
1604
) as WorkbenchAsyncDataTree<IChatSessionItemProvider, ChatSessionItemWithProvider, FuzzyScore>;
1605
1606
// Set the input
1607
this.tree.setInput(this.provider);
1608
1609
// Register tree events
1610
this._register(this.tree.onDidOpen((e) => {
1611
if (e.element) {
1612
this.openChatSession(e.element);
1613
}
1614
}));
1615
1616
// Register context menu event for right-click actions
1617
this._register(this.tree.onContextMenu((e) => {
1618
if (e.element && e.element.id !== historyNode.id) {
1619
this.showContextMenu(e);
1620
}
1621
}));
1622
1623
// Handle visibility changes to load data
1624
this._register(this.onDidChangeBodyVisibility(async visible => {
1625
if (visible && this.tree) {
1626
await this.loadDataWithProgress();
1627
}
1628
}));
1629
1630
// Initially load data if visible
1631
if (this.isBodyVisible() && this.tree) {
1632
this.loadDataWithProgress();
1633
}
1634
1635
this._register(this.tree);
1636
}
1637
1638
private renderGettingStartedList(container: HTMLElement): void {
1639
const listContainer = DOM.append(container, DOM.$('.getting-started-list-container'));
1640
const items: IGettingStartedItem[] = [
1641
{
1642
id: 'install-extensions',
1643
label: nls.localize('chatSessions.installExtensions', "Install Chat Extensions"),
1644
icon: Codicon.extensions,
1645
commandId: 'chat.sessions.gettingStarted'
1646
},
1647
{
1648
id: 'learn-more',
1649
label: nls.localize('chatSessions.learnMoreGHCodingAgent', "Learn More About GitHub Copilot coding agent"),
1650
commandId: 'vscode.open',
1651
icon: Codicon.book,
1652
args: [URI.parse('https://aka.ms/coding-agent-docs')]
1653
}
1654
];
1655
const delegate = new GettingStartedDelegate();
1656
1657
// Create ResourceLabels instance for the renderer
1658
const labels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
1659
this._register(labels);
1660
1661
const renderer = new GettingStartedRenderer(labels);
1662
this.list = this.instantiationService.createInstance(
1663
WorkbenchList<IGettingStartedItem>,
1664
'GettingStarted',
1665
listContainer,
1666
delegate,
1667
[renderer],
1668
{
1669
horizontalScrolling: false,
1670
}
1671
);
1672
this.list.splice(0, 0, items);
1673
this._register(this.list.onDidOpen(e => {
1674
if (e.element) {
1675
this.commandService.executeCommand(e.element.commandId, ...e.element.args ?? []);
1676
}
1677
}));
1678
1679
this._register(this.list);
1680
}
1681
1682
protected override layoutBody(height: number, width: number): void {
1683
super.layoutBody(height, width);
1684
if (this.tree) {
1685
this.tree.layout(height, width);
1686
}
1687
if (this.list) {
1688
this.list.layout(height, width);
1689
}
1690
}
1691
1692
private async openChatSession(element: ChatSessionItemWithProvider) {
1693
if (!element || !element.id) {
1694
return;
1695
}
1696
1697
try {
1698
if (element.id === historyNode.id) {
1699
// Don't try to open the "Show history..." node itself
1700
return;
1701
}
1702
1703
// Handle history items first
1704
if (element.id.startsWith('history-')) {
1705
const sessionId = element.id.substring('history-'.length);
1706
const sessionWithProvider = element as ChatSessionItemWithProvider;
1707
1708
// For local history sessions, use ChatEditorInput approach
1709
if (sessionWithProvider.provider.chatSessionType === 'local') {
1710
const options: IChatEditorOptions = {
1711
target: { sessionId },
1712
pinned: true,
1713
// Add a marker to indicate this session was opened from history
1714
ignoreInView: true,
1715
preserveFocus: true,
1716
};
1717
await this.editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options });
1718
} else {
1719
// For external provider sessions, use ChatSessionUri approach
1720
const providerType = sessionWithProvider.provider.chatSessionType;
1721
const options: IChatEditorOptions = {
1722
pinned: true,
1723
preferredTitle: truncate(element.label, 30),
1724
preserveFocus: true,
1725
};
1726
await this.editorService.openEditor({
1727
resource: ChatSessionUri.forSession(providerType, sessionId),
1728
options,
1729
});
1730
}
1731
return;
1732
}
1733
1734
// Handle local session items (active editors/widgets)
1735
if (this.isLocalChatSessionItem(element)) {
1736
if (element.sessionType === 'editor' && element.editor && element.group) {
1737
// Focus the existing editor
1738
await element.group.openEditor(element.editor, { pinned: true });
1739
return;
1740
} else if (element.sessionType === 'widget') {
1741
// Focus the chat widget
1742
const chatViewPane = await this.viewsService.openView(ChatViewId) as ChatViewPane;
1743
if (chatViewPane && element?.widget?.viewModel?.model) {
1744
await chatViewPane.loadSession(element.widget.viewModel.model.sessionId);
1745
}
1746
return;
1747
}
1748
}
1749
1750
// For other session types, open as a new chat editor
1751
const sessionWithProvider = element as ChatSessionItemWithProvider;
1752
const sessionId = element.id;
1753
const providerType = sessionWithProvider.provider.chatSessionType;
1754
1755
const options: IChatEditorOptions = {
1756
pinned: true,
1757
ignoreInView: true,
1758
preferredTitle: truncate(element.label, 30),
1759
preserveFocus: true,
1760
};
1761
await this.editorService.openEditor({
1762
resource: ChatSessionUri.forSession(providerType, sessionId),
1763
options,
1764
});
1765
1766
} catch (error) {
1767
this.logService.error('[SessionsViewPane] Failed to open chat session:', error);
1768
}
1769
}
1770
1771
private showContextMenu(e: ITreeContextMenuEvent<ChatSessionItemWithProvider>) {
1772
if (!e.element) {
1773
return;
1774
}
1775
1776
const session = e.element;
1777
const sessionWithProvider = session as ChatSessionItemWithProvider;
1778
1779
// Create context overlay for this specific session item
1780
const contextOverlay = getSessionItemContextOverlay(
1781
session,
1782
sessionWithProvider.provider,
1783
this.chatWidgetService,
1784
this.chatService,
1785
this.editorGroupsService
1786
);
1787
const contextKeyService = this.contextKeyService.createOverlay(contextOverlay);
1788
1789
// Create marshalled context for command execution
1790
const marshalledSession = {
1791
session: session,
1792
$mid: MarshalledId.ChatSessionContext
1793
};
1794
1795
// Create menu for this session item to get actions
1796
const menu = this.menuService.createMenu(MenuId.ChatSessionsMenu, contextKeyService);
1797
1798
// Get actions and filter for context menu (all actions that are NOT inline)
1799
const actions = menu.getActions({ arg: marshalledSession, shouldForwardArgs: true });
1800
1801
const { secondary } = getActionBarActions(actions, 'inline'); this.contextMenuService.showContextMenu({
1802
getActions: () => secondary,
1803
getAnchor: () => e.anchor,
1804
getActionsContext: () => marshalledSession,
1805
});
1806
1807
menu.dispose();
1808
}
1809
}
1810
1811
class ChatSessionsGettingStartedAction extends Action2 {
1812
static readonly ID = 'chat.sessions.gettingStarted';
1813
1814
constructor() {
1815
super({
1816
id: ChatSessionsGettingStartedAction.ID,
1817
title: nls.localize2('chat.sessions.gettingStarted.action', "Getting Started with Chat Sessions"),
1818
icon: Codicon.sendToRemoteAgent,
1819
f1: false,
1820
});
1821
}
1822
1823
override async run(accessor: ServicesAccessor): Promise<void> {
1824
const productService = accessor.get(IProductService);
1825
const quickInputService = accessor.get(IQuickInputService);
1826
const extensionManagementService = accessor.get(IWorkbenchExtensionManagementService);
1827
const extensionGalleryService = accessor.get(IExtensionGalleryService);
1828
1829
const recommendations = productService.chatSessionRecommendations;
1830
if (!recommendations || recommendations.length === 0) {
1831
return;
1832
}
1833
1834
const installedExtensions = await extensionManagementService.getInstalled();
1835
const isExtensionAlreadyInstalled = (extensionId: string) => {
1836
return installedExtensions.find(installed => installed.identifier.id === extensionId);
1837
};
1838
1839
const quickPickItems = recommendations.map((recommendation: IChatSessionRecommendation) => {
1840
const extensionInstalled = !!isExtensionAlreadyInstalled(recommendation.extensionId);
1841
return {
1842
label: recommendation.displayName,
1843
description: recommendation.description,
1844
detail: extensionInstalled
1845
? nls.localize('chatSessions.extensionAlreadyInstalled', "'{0}' is already installed", recommendation.extensionName)
1846
: nls.localize('chatSessions.installExtension', "Installs '{0}'", recommendation.extensionName),
1847
extensionId: recommendation.extensionId,
1848
disabled: extensionInstalled,
1849
};
1850
});
1851
1852
const selected = await quickInputService.pick(quickPickItems, {
1853
title: nls.localize('chatSessions.selectExtension', "Install Chat Extensions"),
1854
placeHolder: nls.localize('chatSessions.pickPlaceholder', "Choose extensions to enhance your chat experience"),
1855
canPickMany: true,
1856
});
1857
1858
if (!selected) {
1859
return;
1860
}
1861
1862
const galleryExtensions = await extensionGalleryService.getExtensions(selected.map(item => ({ id: item.extensionId })), CancellationToken.None);
1863
if (!galleryExtensions) {
1864
return;
1865
}
1866
await extensionManagementService.installGalleryExtensions(galleryExtensions.map(extension => ({ extension, options: { preRelease: productService.quality !== 'stable' } })));
1867
}
1868
}
1869
1870
registerAction2(ChatSessionsGettingStartedAction);
1871
1872
MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
1873
command: {
1874
id: 'workbench.action.openChat',
1875
title: nls.localize2('interactiveSession.open', "New Chat Editor"),
1876
icon: Codicon.plus
1877
},
1878
group: 'navigation',
1879
order: 1,
1880
when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),
1881
});
1882
1883