Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts
4780 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/agentsessionsviewer.css';
7
import { h } from '../../../../../base/browser/dom.js';
8
import { localize } from '../../../../../nls.js';
9
import { IIdentityProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';
10
import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js';
11
import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js';
12
import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js';
13
import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js';
14
import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js';
15
import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
16
import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isLocalAgentSessionItem, isSessionInProgressStatus } from './agentSessionsModel.js';
17
import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js';
18
import { ThemeIcon } from '../../../../../base/common/themables.js';
19
import { Codicon } from '../../../../../base/common/codicons.js';
20
import { fromNow, getDurationString } from '../../../../../base/common/date.js';
21
import { FuzzyScore, createMatches } from '../../../../../base/common/filters.js';
22
import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
23
import { allowedChatMarkdownHtmlTags } from '../widget/chatContentMarkdownRenderer.js';
24
import { IProductService } from '../../../../../platform/product/common/productService.js';
25
import { IDragAndDropData } from '../../../../../base/browser/dnd.js';
26
import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listView.js';
27
import { coalesce } from '../../../../../base/common/arrays.js';
28
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
29
import { fillEditorsDragData } from '../../../../browser/dnd.js';
30
import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js';
31
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
32
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
33
import { IntervalTimer } from '../../../../../base/common/async.js';
34
import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
35
import { MenuId } from '../../../../../platform/actions/common/actions.js';
36
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
37
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
38
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
39
import { Event } from '../../../../../base/common/event.js';
40
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
41
import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js';
42
43
export type AgentSessionListItem = IAgentSession | IAgentSessionSection;
44
45
//#region Agent Session Renderer
46
47
interface IAgentSessionItemTemplate {
48
readonly element: HTMLElement;
49
50
// Column 1
51
readonly icon: HTMLElement;
52
53
// Column 2 Row 1
54
readonly title: IconLabel;
55
readonly titleToolbar: MenuWorkbenchToolBar;
56
57
// Column 2 Row 2
58
readonly diffContainer: HTMLElement;
59
readonly diffFilesSpan: HTMLSpanElement;
60
readonly diffAddedSpan: HTMLSpanElement;
61
readonly diffRemovedSpan: HTMLSpanElement;
62
63
readonly badge: HTMLElement;
64
readonly description: HTMLElement;
65
66
readonly statusContainer: HTMLElement;
67
readonly statusProviderIcon: HTMLElement;
68
readonly statusTime: HTMLElement;
69
70
readonly contextKeyService: IContextKeyService;
71
readonly elementDisposable: DisposableStore;
72
readonly disposables: IDisposable;
73
}
74
75
export interface IAgentSessionRendererOptions {
76
getHoverPosition(): HoverPosition;
77
}
78
79
export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSession, FuzzyScore, IAgentSessionItemTemplate> {
80
81
static readonly TEMPLATE_ID = 'agent-session';
82
83
readonly templateId = AgentSessionRenderer.TEMPLATE_ID;
84
85
constructor(
86
private readonly options: IAgentSessionRendererOptions,
87
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
88
@IProductService private readonly productService: IProductService,
89
@IHoverService private readonly hoverService: IHoverService,
90
@IInstantiationService private readonly instantiationService: IInstantiationService,
91
@IContextKeyService private readonly contextKeyService: IContextKeyService,
92
) { }
93
94
renderTemplate(container: HTMLElement): IAgentSessionItemTemplate {
95
const disposables = new DisposableStore();
96
const elementDisposable = disposables.add(new DisposableStore());
97
98
const elements = h(
99
'div.agent-session-item@item',
100
[
101
h('div.agent-session-icon-col', [
102
h('div.agent-session-icon@icon')
103
]),
104
h('div.agent-session-main-col', [
105
h('div.agent-session-title-row', [
106
h('div.agent-session-title@title'),
107
h('div.agent-session-title-toolbar@titleToolbar'),
108
]),
109
h('div.agent-session-details-row', [
110
h('div.agent-session-diff-container@diffContainer',
111
[
112
h('span.agent-session-diff-files@filesSpan'),
113
h('span.agent-session-diff-added@addedSpan'),
114
h('span.agent-session-diff-removed@removedSpan')
115
]),
116
h('div.agent-session-badge@badge'),
117
h('div.agent-session-description@description'),
118
h('div.agent-session-status@statusContainer', [
119
h('span.agent-session-status-provider-icon@statusProviderIcon'),
120
h('span.agent-session-status-time@statusTime')
121
])
122
])
123
])
124
]
125
);
126
127
const contextKeyService = disposables.add(this.contextKeyService.createScoped(elements.item));
128
const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
129
const titleToolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, elements.titleToolbar, MenuId.AgentSessionItemToolbar, {
130
menuOptions: { shouldForwardArgs: true },
131
}));
132
133
container.appendChild(elements.item);
134
135
return {
136
element: elements.item,
137
icon: elements.icon,
138
title: disposables.add(new IconLabel(elements.title, { supportHighlights: true, supportIcons: true })),
139
titleToolbar,
140
diffContainer: elements.diffContainer,
141
diffFilesSpan: elements.filesSpan,
142
diffAddedSpan: elements.addedSpan,
143
diffRemovedSpan: elements.removedSpan,
144
badge: elements.badge,
145
description: elements.description,
146
statusContainer: elements.statusContainer,
147
statusProviderIcon: elements.statusProviderIcon,
148
statusTime: elements.statusTime,
149
contextKeyService,
150
elementDisposable,
151
disposables
152
};
153
}
154
155
renderElement(session: ITreeNode<IAgentSession, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
156
157
// Clear old state
158
template.elementDisposable.clear();
159
template.diffFilesSpan.textContent = '';
160
template.diffAddedSpan.textContent = '';
161
template.diffRemovedSpan.textContent = '';
162
template.badge.textContent = '';
163
template.description.textContent = '';
164
165
// Archived
166
template.element.classList.toggle('archived', session.element.isArchived());
167
168
// Icon
169
template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(this.getIcon(session.element))}`;
170
171
// Title
172
const markdownTitle = new MarkdownString(session.element.label);
173
template.title.setLabel(renderAsPlaintext(markdownTitle), undefined, { matches: createMatches(session.filterData) });
174
175
// Title Actions - Update context keys
176
ChatContextKeys.isArchivedAgentSession.bindTo(template.contextKeyService).set(session.element.isArchived());
177
ChatContextKeys.isReadAgentSession.bindTo(template.contextKeyService).set(session.element.isRead());
178
ChatContextKeys.agentSessionType.bindTo(template.contextKeyService).set(session.element.providerType);
179
template.titleToolbar.context = session.element;
180
181
// Diff information
182
let hasDiff = false;
183
const { changes: diff } = session.element;
184
if (!isSessionInProgressStatus(session.element.status) && diff && hasValidDiff(diff)) {
185
if (this.renderDiff(session, template)) {
186
hasDiff = true;
187
}
188
}
189
template.diffContainer.classList.toggle('has-diff', hasDiff);
190
191
// Badge
192
let hasBadge = false;
193
if (!isSessionInProgressStatus(session.element.status)) {
194
hasBadge = this.renderBadge(session, template);
195
}
196
template.badge.classList.toggle('has-badge', hasBadge);
197
198
// Description (unless diff is shown)
199
if (!hasDiff) {
200
this.renderDescription(session, template, hasBadge);
201
}
202
203
// Status
204
this.renderStatus(session, template);
205
206
// Hover
207
this.renderHover(session, template);
208
}
209
210
private renderBadge(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
211
const badge = session.element.badge;
212
if (badge) {
213
this.renderMarkdownOrText(badge, template.badge, template.elementDisposable);
214
}
215
216
return !!badge;
217
}
218
219
private renderMarkdownOrText(content: string | IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {
220
if (typeof content === 'string') {
221
container.textContent = content;
222
} else {
223
disposables.add(this.markdownRendererService.render(content, {
224
sanitizerConfig: {
225
replaceWithPlaintext: true,
226
allowedTags: {
227
override: allowedChatMarkdownHtmlTags,
228
},
229
allowedLinkSchemes: { augment: [this.productService.urlProtocol] }
230
},
231
}, container));
232
}
233
}
234
235
private renderDiff(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
236
const diff = getAgentChangesSummary(session.element.changes);
237
if (!diff) {
238
return false;
239
}
240
241
if (diff.files > 0) {
242
template.diffFilesSpan.textContent = diff.files === 1 ? localize('diffFile', "1 file") : localize('diffFiles', "{0} files", diff.files);
243
}
244
245
if (diff.insertions >= 0 /* render even `0` for more homogeneity */) {
246
template.diffAddedSpan.textContent = `+${diff.insertions}`;
247
}
248
249
if (diff.deletions >= 0 /* render even `0` for more homogeneity */) {
250
template.diffRemovedSpan.textContent = `-${diff.deletions}`;
251
}
252
253
return true;
254
}
255
256
private getIcon(session: IAgentSession): ThemeIcon {
257
if (session.status === AgentSessionStatus.InProgress) {
258
return Codicon.sessionInProgress;
259
}
260
261
if (session.status === AgentSessionStatus.NeedsInput) {
262
return Codicon.report;
263
}
264
265
if (session.status === AgentSessionStatus.Failed) {
266
return Codicon.error;
267
}
268
269
if (!session.isRead() && !session.isArchived()) {
270
return Codicon.circleFilled;
271
}
272
273
return Codicon.circleSmallFilled;
274
}
275
276
private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate, hasBadge: boolean): void {
277
const description = session.element.description;
278
if (description) {
279
this.renderMarkdownOrText(description, template.description, template.elementDisposable);
280
}
281
282
// Fallback to state label
283
else {
284
if (session.element.status === AgentSessionStatus.InProgress) {
285
template.description.textContent = localize('chat.session.status.inProgress', "Working...");
286
} else if (session.element.status === AgentSessionStatus.NeedsInput) {
287
template.description.textContent = localize('chat.session.status.needsInput', "Input needed.");
288
} else if (hasBadge && session.element.status === AgentSessionStatus.Completed) {
289
template.description.textContent = ''; // no description if completed and has badge
290
} else if (
291
session.element.timing.finishedOrFailedTime &&
292
session.element.timing.inProgressTime &&
293
session.element.timing.finishedOrFailedTime > session.element.timing.inProgressTime
294
) {
295
const duration = this.toDuration(session.element.timing.inProgressTime, session.element.timing.finishedOrFailedTime, false);
296
297
template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
298
localize('chat.session.status.failedAfter', "Failed after {0}.", duration ?? '1s') :
299
localize('chat.session.status.completedAfter', "Completed in {0}.", duration ?? '1s');
300
} else {
301
template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
302
localize('chat.session.status.failed', "Failed") :
303
localize('chat.session.status.completed', "Completed");
304
}
305
}
306
}
307
308
private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean): string | undefined {
309
const elapsed = Math.round((endTime - startTime) / 1000) * 1000;
310
if (elapsed < 1000) {
311
return undefined;
312
}
313
314
return getDurationString(elapsed, useFullTimeWords);
315
}
316
317
private renderStatus(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
318
319
const getTimeLabel = (session: IAgentSession) => {
320
let timeLabel: string | undefined;
321
if (session.status === AgentSessionStatus.InProgress && session.timing.inProgressTime) {
322
timeLabel = this.toDuration(session.timing.inProgressTime, Date.now(), false);
323
}
324
325
if (!timeLabel) {
326
timeLabel = fromNow(session.timing.endTime || session.timing.startTime);
327
}
328
329
return timeLabel;
330
};
331
332
// Provider icon (hide for local sessions)
333
if (!isLocalAgentSessionItem(session.element)) {
334
template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`;
335
} else {
336
template.statusProviderIcon.className = 'agent-session-status-provider-icon hidden';
337
}
338
339
// Time label
340
template.statusTime.textContent = getTimeLabel(session.element);
341
const timer = template.elementDisposable.add(new IntervalTimer());
342
timer.cancelAndSet(() => template.statusTime.textContent = getTimeLabel(session.element), session.element.status === AgentSessionStatus.InProgress ? 1000 /* every second */ : 60 * 1000 /* every minute */);
343
}
344
345
private renderHover(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
346
template.elementDisposable.add(
347
this.hoverService.setupDelayedHover(template.element, () => ({
348
content: this.buildTooltip(session.element),
349
style: HoverStyle.Pointer,
350
position: {
351
hoverPosition: this.options.getHoverPosition()
352
}
353
}), { groupId: 'agent.sessions' })
354
);
355
}
356
357
private buildTooltip(session: IAgentSession): IMarkdownString {
358
const lines: string[] = [];
359
360
// Title
361
lines.push(`**${session.label}**`);
362
363
// Tooltip (from provider)
364
if (session.tooltip) {
365
const tooltip = typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value;
366
lines.push(tooltip);
367
} else {
368
369
// Description
370
if (session.description) {
371
const description = typeof session.description === 'string' ? session.description : session.description.value;
372
lines.push(description);
373
}
374
375
// Badge
376
if (session.badge) {
377
const badge = typeof session.badge === 'string' ? session.badge : session.badge.value;
378
lines.push(badge);
379
}
380
}
381
382
// Details line: Status • Provider • Duration/Time
383
const details: string[] = [];
384
385
// Status
386
details.push(toStatusLabel(session.status));
387
388
// Provider
389
details.push(session.providerLabel);
390
391
// Duration or start time
392
if (session.timing.finishedOrFailedTime && session.timing.inProgressTime) {
393
const duration = this.toDuration(session.timing.inProgressTime, session.timing.finishedOrFailedTime, true);
394
if (duration) {
395
details.push(duration);
396
}
397
} else {
398
details.push(fromNow(session.timing.startTime, true, true));
399
}
400
401
lines.push(details.join(' • '));
402
403
// Diff information
404
const diff = getAgentChangesSummary(session.changes);
405
if (diff && hasValidDiff(session.changes)) {
406
const diffParts: string[] = [];
407
if (diff.files > 0) {
408
diffParts.push(diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files));
409
}
410
if (diff.insertions > 0) {
411
diffParts.push(`+${diff.insertions}`);
412
}
413
if (diff.deletions > 0) {
414
diffParts.push(`-${diff.deletions}`);
415
}
416
if (diffParts.length > 0) {
417
lines.push(`$(diff) ${diffParts.join(', ')}`);
418
}
419
}
420
421
// Archived status
422
if (session.isArchived()) {
423
lines.push(`$(archive) ${localize('tooltip.archived', "Archived")}`);
424
}
425
426
return new MarkdownString(lines.join('\n\n'), { supportThemeIcons: true });
427
}
428
429
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSession>, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
430
throw new Error('Should never happen since session is incompressible');
431
}
432
433
disposeElement(element: ITreeNode<IAgentSession, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
434
template.elementDisposable.clear();
435
}
436
437
disposeTemplate(templateData: IAgentSessionItemTemplate): void {
438
templateData.disposables.dispose();
439
}
440
}
441
442
function toStatusLabel(status: AgentSessionStatus): string {
443
let statusLabel: string;
444
switch (status) {
445
case AgentSessionStatus.NeedsInput:
446
statusLabel = localize('agentSessionNeedsInput', "Needs Input");
447
break;
448
case AgentSessionStatus.InProgress:
449
statusLabel = localize('agentSessionInProgress', "In Progress");
450
break;
451
case AgentSessionStatus.Failed:
452
statusLabel = localize('agentSessionFailed', "Failed");
453
break;
454
default:
455
statusLabel = localize('agentSessionCompleted', "Completed");
456
}
457
458
return statusLabel;
459
}
460
461
//#endregion
462
463
//#region Section Header Renderer
464
465
interface IAgentSessionSectionTemplate {
466
readonly container: HTMLElement;
467
readonly label: HTMLSpanElement;
468
readonly toolbar: MenuWorkbenchToolBar;
469
readonly contextKeyService: IContextKeyService;
470
readonly disposables: IDisposable;
471
}
472
473
export class AgentSessionSectionRenderer implements ICompressibleTreeRenderer<IAgentSessionSection, FuzzyScore, IAgentSessionSectionTemplate> {
474
475
static readonly TEMPLATE_ID = 'agent-session-section';
476
477
readonly templateId = AgentSessionSectionRenderer.TEMPLATE_ID;
478
479
constructor(
480
@IInstantiationService private readonly instantiationService: IInstantiationService,
481
@IContextKeyService private readonly contextKeyService: IContextKeyService,
482
) { }
483
484
renderTemplate(container: HTMLElement): IAgentSessionSectionTemplate {
485
const disposables = new DisposableStore();
486
487
const elements = h(
488
'div.agent-session-section@container',
489
[
490
h('span.agent-session-section-label@label'),
491
h('div.agent-session-section-toolbar@toolbar')
492
]
493
);
494
495
const contextKeyService = disposables.add(this.contextKeyService.createScoped(elements.container));
496
const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
497
const toolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, elements.toolbar, MenuId.AgentSessionSectionToolbar, {
498
menuOptions: { shouldForwardArgs: true },
499
}));
500
501
container.appendChild(elements.container);
502
503
return {
504
container: elements.container,
505
label: elements.label,
506
toolbar,
507
contextKeyService,
508
disposables
509
};
510
}
511
512
renderElement(element: ITreeNode<IAgentSessionSection, FuzzyScore>, index: number, template: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {
513
514
// Label
515
template.label.textContent = element.element.label;
516
517
// Toolbar
518
ChatContextKeys.agentSessionSection.bindTo(template.contextKeyService).set(element.element.section);
519
template.toolbar.context = element.element;
520
}
521
522
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSessionSection>, FuzzyScore>, index: number, templateData: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {
523
throw new Error('Should never happen since section header is incompressible');
524
}
525
526
disposeElement(element: ITreeNode<IAgentSessionSection, FuzzyScore>, index: number, template: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {
527
// noop
528
}
529
530
disposeTemplate(templateData: IAgentSessionSectionTemplate): void {
531
templateData.disposables.dispose();
532
}
533
}
534
535
//#endregion
536
537
export class AgentSessionsListDelegate implements IListVirtualDelegate<AgentSessionListItem> {
538
539
static readonly ITEM_HEIGHT = 52;
540
static readonly SECTION_HEIGHT = 26;
541
542
getHeight(element: AgentSessionListItem): number {
543
if (isAgentSessionSection(element)) {
544
return AgentSessionsListDelegate.SECTION_HEIGHT;
545
}
546
547
return AgentSessionsListDelegate.ITEM_HEIGHT;
548
}
549
550
getTemplateId(element: AgentSessionListItem): string {
551
if (isAgentSessionSection(element)) {
552
return AgentSessionSectionRenderer.TEMPLATE_ID;
553
}
554
555
return AgentSessionRenderer.TEMPLATE_ID;
556
}
557
}
558
559
export class AgentSessionsAccessibilityProvider implements IListAccessibilityProvider<AgentSessionListItem> {
560
561
getWidgetAriaLabel(): string {
562
return localize('agentSessions', "Agent Sessions");
563
}
564
565
getAriaLabel(element: AgentSessionListItem): string | null {
566
if (isAgentSessionSection(element)) {
567
return localize('agentSessionSectionAriaLabel', "{0} sessions section", element.label);
568
}
569
570
return localize('agentSessionItemAriaLabel', "{0} session {1} ({2}), created {3}", element.providerLabel, element.label, toStatusLabel(element.status), new Date(element.timing.startTime).toLocaleString());
571
}
572
}
573
574
export interface IAgentSessionsFilterExcludes {
575
readonly providers: readonly string[];
576
readonly states: readonly AgentSessionStatus[];
577
578
readonly archived: boolean;
579
readonly read: boolean;
580
}
581
582
export interface IAgentSessionsFilter {
583
584
/**
585
* An event that fires when the filter changes and sessions
586
* should be re-evaluated.
587
*/
588
readonly onDidChange: Event<void>;
589
590
/**
591
* Optional limit on the number of sessions to show.
592
*/
593
readonly limitResults?: () => number | undefined;
594
595
/**
596
* Whether to show section headers to group sessions.
597
* When false, sessions are shown as a flat list.
598
*/
599
readonly groupResults?: () => boolean | undefined;
600
601
/**
602
* A callback to notify the filter about the number of
603
* results after filtering.
604
*/
605
notifyResults?(count: number): void;
606
607
/**
608
* The logic to exclude sessions from the view.
609
*/
610
exclude(session: IAgentSession): boolean;
611
612
/**
613
* Get the current filter excludes for display in the UI.
614
*/
615
getExcludes(): IAgentSessionsFilterExcludes;
616
}
617
618
export class AgentSessionsDataSource implements IAsyncDataSource<IAgentSessionsModel, AgentSessionListItem> {
619
620
constructor(
621
private readonly filter: IAgentSessionsFilter | undefined,
622
private readonly sorter: ITreeSorter<IAgentSession>,
623
) { }
624
625
hasChildren(element: IAgentSessionsModel | AgentSessionListItem): boolean {
626
627
// Sessions model
628
if (isAgentSessionsModel(element)) {
629
return true;
630
}
631
632
// Sessions section
633
else if (isAgentSessionSection(element)) {
634
return element.sessions.length > 0;
635
}
636
637
// Session element
638
else {
639
return false;
640
}
641
}
642
643
getChildren(element: IAgentSessionsModel | AgentSessionListItem): Iterable<AgentSessionListItem> {
644
645
// Sessions model
646
if (isAgentSessionsModel(element)) {
647
648
// Apply filter if configured
649
let filteredSessions = element.sessions.filter(session => !this.filter?.exclude(session));
650
651
// Apply sorter unless we group into sections or we are to limit results
652
const limitResultsCount = this.filter?.limitResults?.();
653
if (!this.filter?.groupResults?.() || typeof limitResultsCount === 'number') {
654
filteredSessions.sort(this.sorter.compare.bind(this.sorter));
655
}
656
657
// Apply limiter if configured (requires sorting)
658
if (typeof limitResultsCount === 'number') {
659
filteredSessions = filteredSessions.slice(0, limitResultsCount);
660
}
661
662
// Callback results count
663
this.filter?.notifyResults?.(filteredSessions.length);
664
665
// Group sessions into sections if enabled
666
if (this.filter?.groupResults?.()) {
667
return this.groupSessionsIntoSections(filteredSessions);
668
}
669
670
// Otherwise return flat sorted list
671
return filteredSessions;
672
}
673
674
// Sessions section
675
else if (isAgentSessionSection(element)) {
676
return element.sessions;
677
}
678
679
// Session element
680
else {
681
return [];
682
}
683
}
684
685
private groupSessionsIntoSections(sessions: IAgentSession[]): AgentSessionListItem[] {
686
const result: AgentSessionListItem[] = [];
687
688
const sortedSessions = sessions.sort(this.sorter.compare.bind(this.sorter));
689
const groupedSessions = groupAgentSessions(sortedSessions);
690
691
for (const { sessions, section, label } of groupedSessions.values()) {
692
if (sessions.length === 0) {
693
continue;
694
}
695
696
result.push({ section, label, sessions });
697
}
698
699
return result;
700
}
701
}
702
703
const DAY_THRESHOLD = 24 * 60 * 60 * 1000;
704
const WEEK_THRESHOLD = 7 * DAY_THRESHOLD;
705
706
export const AgentSessionSectionLabels = {
707
[AgentSessionSection.InProgress]: localize('agentSessions.inProgressSection', "In Progress"),
708
[AgentSessionSection.Today]: localize('agentSessions.todaySection', "Today"),
709
[AgentSessionSection.Yesterday]: localize('agentSessions.yesterdaySection', "Yesterday"),
710
[AgentSessionSection.Week]: localize('agentSessions.weekSection', "Last Week"),
711
[AgentSessionSection.Older]: localize('agentSessions.olderSection', "Older"),
712
[AgentSessionSection.Archived]: localize('agentSessions.archivedSection', "Archived"),
713
};
714
715
export function groupAgentSessions(sessions: IAgentSession[]): Map<AgentSessionSection, IAgentSessionSection> {
716
const now = Date.now();
717
const startOfToday = new Date(now).setHours(0, 0, 0, 0);
718
const startOfYesterday = startOfToday - DAY_THRESHOLD;
719
const weekThreshold = now - WEEK_THRESHOLD;
720
721
const inProgressSessions: IAgentSession[] = [];
722
const todaySessions: IAgentSession[] = [];
723
const yesterdaySessions: IAgentSession[] = [];
724
const weekSessions: IAgentSession[] = [];
725
const olderSessions: IAgentSession[] = [];
726
const archivedSessions: IAgentSession[] = [];
727
728
for (const session of sessions) {
729
if (isSessionInProgressStatus(session.status)) {
730
inProgressSessions.push(session);
731
} else if (session.isArchived()) {
732
archivedSessions.push(session);
733
} else {
734
const sessionTime = session.timing.endTime || session.timing.startTime;
735
if (sessionTime >= startOfToday) {
736
todaySessions.push(session);
737
} else if (sessionTime >= startOfYesterday) {
738
yesterdaySessions.push(session);
739
} else if (sessionTime >= weekThreshold) {
740
weekSessions.push(session);
741
} else {
742
olderSessions.push(session);
743
}
744
}
745
}
746
747
return new Map<AgentSessionSection, IAgentSessionSection>([
748
[AgentSessionSection.InProgress, { section: AgentSessionSection.InProgress, label: AgentSessionSectionLabels[AgentSessionSection.InProgress], sessions: inProgressSessions }],
749
[AgentSessionSection.Today, { section: AgentSessionSection.Today, label: AgentSessionSectionLabels[AgentSessionSection.Today], sessions: todaySessions }],
750
[AgentSessionSection.Yesterday, { section: AgentSessionSection.Yesterday, label: AgentSessionSectionLabels[AgentSessionSection.Yesterday], sessions: yesterdaySessions }],
751
[AgentSessionSection.Week, { section: AgentSessionSection.Week, label: AgentSessionSectionLabels[AgentSessionSection.Week], sessions: weekSessions }],
752
[AgentSessionSection.Older, { section: AgentSessionSection.Older, label: AgentSessionSectionLabels[AgentSessionSection.Older], sessions: olderSessions }],
753
[AgentSessionSection.Archived, { section: AgentSessionSection.Archived, label: localize('agentSessions.archivedSectionWithCount', "Archived ({0})", archivedSessions.length), sessions: archivedSessions }],
754
]);
755
}
756
757
export class AgentSessionsIdentityProvider implements IIdentityProvider<IAgentSessionsModel | AgentSessionListItem> {
758
759
getId(element: IAgentSessionsModel | AgentSessionListItem): string {
760
if (isAgentSessionSection(element)) {
761
return `section-${element.section}`;
762
}
763
764
if (isAgentSession(element)) {
765
return element.resource.toString();
766
}
767
768
return 'agent-sessions-id';
769
}
770
}
771
772
export class AgentSessionsCompressionDelegate implements ITreeCompressionDelegate<AgentSessionListItem> {
773
774
isIncompressible(element: AgentSessionListItem): boolean {
775
return true;
776
}
777
}
778
779
export interface IAgentSessionsSorterOptions {
780
overrideCompare?(sessionA: IAgentSession, sessionB: IAgentSession): number | undefined;
781
}
782
783
export class AgentSessionsSorter implements ITreeSorter<IAgentSession> {
784
785
constructor(private readonly options?: IAgentSessionsSorterOptions) { }
786
787
compare(sessionA: IAgentSession, sessionB: IAgentSession): number {
788
789
// Input Needed
790
const aNeedsInput = sessionA.status === AgentSessionStatus.NeedsInput;
791
const bNeedsInput = sessionB.status === AgentSessionStatus.NeedsInput;
792
793
if (aNeedsInput && !bNeedsInput) {
794
return -1; // a (needs input) comes before b (other)
795
}
796
if (!aNeedsInput && bNeedsInput) {
797
return 1; // a (other) comes after b (needs input)
798
}
799
800
// In Progress
801
const aInProgress = sessionA.status === AgentSessionStatus.InProgress;
802
const bInProgress = sessionB.status === AgentSessionStatus.InProgress;
803
804
if (aInProgress && !bInProgress) {
805
return -1; // a (in-progress) comes before b (finished)
806
}
807
if (!aInProgress && bInProgress) {
808
return 1; // a (finished) comes after b (in-progress)
809
}
810
811
// Archived
812
const aArchived = sessionA.isArchived();
813
const bArchived = sessionB.isArchived();
814
815
if (!aArchived && bArchived) {
816
return -1; // a (non-archived) comes before b (archived)
817
}
818
if (aArchived && !bArchived) {
819
return 1; // a (archived) comes after b (non-archived)
820
}
821
822
// Before we compare by time, allow override
823
const override = this.options?.overrideCompare?.(sessionA, sessionB);
824
if (typeof override === 'number') {
825
return override;
826
}
827
828
//Sort by end or start time (most recent first)
829
return (sessionB.timing.endTime || sessionB.timing.startTime) - (sessionA.timing.endTime || sessionA.timing.startTime);
830
}
831
}
832
833
export class AgentSessionsKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider<AgentSessionListItem> {
834
835
getKeyboardNavigationLabel(element: AgentSessionListItem): string {
836
if (isAgentSessionSection(element)) {
837
return element.label;
838
}
839
840
return element.label;
841
}
842
843
getCompressedNodeKeyboardNavigationLabel(elements: AgentSessionListItem[]): { toString(): string | undefined } | undefined {
844
return undefined; // not enabled
845
}
846
}
847
848
export class AgentSessionsDragAndDrop extends Disposable implements ITreeDragAndDrop<AgentSessionListItem> {
849
850
constructor(
851
@IInstantiationService private readonly instantiationService: IInstantiationService
852
) {
853
super();
854
}
855
856
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
857
const elements = (data.getData() as AgentSessionListItem[]).filter(e => isAgentSession(e));
858
const uris = coalesce(elements.map(e => e.resource));
859
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, uris, originalEvent));
860
}
861
862
getDragURI(element: AgentSessionListItem): string | null {
863
if (isAgentSessionSection(element)) {
864
return null; // section headers are not draggable
865
}
866
867
return element.resource.toString();
868
}
869
870
getDragLabel?(elements: AgentSessionListItem[], originalEvent: DragEvent): string | undefined {
871
const sessions = elements.filter(e => isAgentSession(e));
872
if (sessions.length === 1) {
873
return sessions[0].label;
874
}
875
876
return localize('agentSessions.dragLabel', "{0} agent sessions", sessions.length);
877
}
878
879
onDragOver(data: IDragAndDropData, targetElement: AgentSessionListItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
880
return false;
881
}
882
883
drop(data: IDragAndDropData, targetElement: AgentSessionListItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void { }
884
}
885
886