Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/agentsessionsviewer.css';6import { h } from '../../../../../base/browser/dom.js';7import { localize } from '../../../../../nls.js';8import { IIdentityProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';9import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js';10import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js';11import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js';12import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js';13import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js';14import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';15import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isLocalAgentSessionItem, isSessionInProgressStatus } from './agentSessionsModel.js';16import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js';17import { ThemeIcon } from '../../../../../base/common/themables.js';18import { Codicon } from '../../../../../base/common/codicons.js';19import { fromNow, getDurationString } from '../../../../../base/common/date.js';20import { FuzzyScore, createMatches } from '../../../../../base/common/filters.js';21import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';22import { allowedChatMarkdownHtmlTags } from '../widget/chatContentMarkdownRenderer.js';23import { IProductService } from '../../../../../platform/product/common/productService.js';24import { IDragAndDropData } from '../../../../../base/browser/dnd.js';25import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listView.js';26import { coalesce } from '../../../../../base/common/arrays.js';27import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';28import { fillEditorsDragData } from '../../../../browser/dnd.js';29import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js';30import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';31import { IHoverService } from '../../../../../platform/hover/browser/hover.js';32import { IntervalTimer } from '../../../../../base/common/async.js';33import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';34import { MenuId } from '../../../../../platform/actions/common/actions.js';35import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';36import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';37import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';38import { Event } from '../../../../../base/common/event.js';39import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';40import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js';4142export type AgentSessionListItem = IAgentSession | IAgentSessionSection;4344//#region Agent Session Renderer4546interface IAgentSessionItemTemplate {47readonly element: HTMLElement;4849// Column 150readonly icon: HTMLElement;5152// Column 2 Row 153readonly title: IconLabel;54readonly titleToolbar: MenuWorkbenchToolBar;5556// Column 2 Row 257readonly diffContainer: HTMLElement;58readonly diffFilesSpan: HTMLSpanElement;59readonly diffAddedSpan: HTMLSpanElement;60readonly diffRemovedSpan: HTMLSpanElement;6162readonly badge: HTMLElement;63readonly description: HTMLElement;6465readonly statusContainer: HTMLElement;66readonly statusProviderIcon: HTMLElement;67readonly statusTime: HTMLElement;6869readonly contextKeyService: IContextKeyService;70readonly elementDisposable: DisposableStore;71readonly disposables: IDisposable;72}7374export interface IAgentSessionRendererOptions {75getHoverPosition(): HoverPosition;76}7778export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSession, FuzzyScore, IAgentSessionItemTemplate> {7980static readonly TEMPLATE_ID = 'agent-session';8182readonly templateId = AgentSessionRenderer.TEMPLATE_ID;8384constructor(85private readonly options: IAgentSessionRendererOptions,86@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,87@IProductService private readonly productService: IProductService,88@IHoverService private readonly hoverService: IHoverService,89@IInstantiationService private readonly instantiationService: IInstantiationService,90@IContextKeyService private readonly contextKeyService: IContextKeyService,91) { }9293renderTemplate(container: HTMLElement): IAgentSessionItemTemplate {94const disposables = new DisposableStore();95const elementDisposable = disposables.add(new DisposableStore());9697const elements = h(98'div.agent-session-item@item',99[100h('div.agent-session-icon-col', [101h('div.agent-session-icon@icon')102]),103h('div.agent-session-main-col', [104h('div.agent-session-title-row', [105h('div.agent-session-title@title'),106h('div.agent-session-title-toolbar@titleToolbar'),107]),108h('div.agent-session-details-row', [109h('div.agent-session-diff-container@diffContainer',110[111h('span.agent-session-diff-files@filesSpan'),112h('span.agent-session-diff-added@addedSpan'),113h('span.agent-session-diff-removed@removedSpan')114]),115h('div.agent-session-badge@badge'),116h('div.agent-session-description@description'),117h('div.agent-session-status@statusContainer', [118h('span.agent-session-status-provider-icon@statusProviderIcon'),119h('span.agent-session-status-time@statusTime')120])121])122])123]124);125126const contextKeyService = disposables.add(this.contextKeyService.createScoped(elements.item));127const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));128const titleToolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, elements.titleToolbar, MenuId.AgentSessionItemToolbar, {129menuOptions: { shouldForwardArgs: true },130}));131132container.appendChild(elements.item);133134return {135element: elements.item,136icon: elements.icon,137title: disposables.add(new IconLabel(elements.title, { supportHighlights: true, supportIcons: true })),138titleToolbar,139diffContainer: elements.diffContainer,140diffFilesSpan: elements.filesSpan,141diffAddedSpan: elements.addedSpan,142diffRemovedSpan: elements.removedSpan,143badge: elements.badge,144description: elements.description,145statusContainer: elements.statusContainer,146statusProviderIcon: elements.statusProviderIcon,147statusTime: elements.statusTime,148contextKeyService,149elementDisposable,150disposables151};152}153154renderElement(session: ITreeNode<IAgentSession, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {155156// Clear old state157template.elementDisposable.clear();158template.diffFilesSpan.textContent = '';159template.diffAddedSpan.textContent = '';160template.diffRemovedSpan.textContent = '';161template.badge.textContent = '';162template.description.textContent = '';163164// Archived165template.element.classList.toggle('archived', session.element.isArchived());166167// Icon168template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(this.getIcon(session.element))}`;169170// Title171const markdownTitle = new MarkdownString(session.element.label);172template.title.setLabel(renderAsPlaintext(markdownTitle), undefined, { matches: createMatches(session.filterData) });173174// Title Actions - Update context keys175ChatContextKeys.isArchivedAgentSession.bindTo(template.contextKeyService).set(session.element.isArchived());176ChatContextKeys.isReadAgentSession.bindTo(template.contextKeyService).set(session.element.isRead());177ChatContextKeys.agentSessionType.bindTo(template.contextKeyService).set(session.element.providerType);178template.titleToolbar.context = session.element;179180// Diff information181let hasDiff = false;182const { changes: diff } = session.element;183if (!isSessionInProgressStatus(session.element.status) && diff && hasValidDiff(diff)) {184if (this.renderDiff(session, template)) {185hasDiff = true;186}187}188template.diffContainer.classList.toggle('has-diff', hasDiff);189190// Badge191let hasBadge = false;192if (!isSessionInProgressStatus(session.element.status)) {193hasBadge = this.renderBadge(session, template);194}195template.badge.classList.toggle('has-badge', hasBadge);196197// Description (unless diff is shown)198if (!hasDiff) {199this.renderDescription(session, template, hasBadge);200}201202// Status203this.renderStatus(session, template);204205// Hover206this.renderHover(session, template);207}208209private renderBadge(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {210const badge = session.element.badge;211if (badge) {212this.renderMarkdownOrText(badge, template.badge, template.elementDisposable);213}214215return !!badge;216}217218private renderMarkdownOrText(content: string | IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {219if (typeof content === 'string') {220container.textContent = content;221} else {222disposables.add(this.markdownRendererService.render(content, {223sanitizerConfig: {224replaceWithPlaintext: true,225allowedTags: {226override: allowedChatMarkdownHtmlTags,227},228allowedLinkSchemes: { augment: [this.productService.urlProtocol] }229},230}, container));231}232}233234private renderDiff(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {235const diff = getAgentChangesSummary(session.element.changes);236if (!diff) {237return false;238}239240if (diff.files > 0) {241template.diffFilesSpan.textContent = diff.files === 1 ? localize('diffFile', "1 file") : localize('diffFiles', "{0} files", diff.files);242}243244if (diff.insertions >= 0 /* render even `0` for more homogeneity */) {245template.diffAddedSpan.textContent = `+${diff.insertions}`;246}247248if (diff.deletions >= 0 /* render even `0` for more homogeneity */) {249template.diffRemovedSpan.textContent = `-${diff.deletions}`;250}251252return true;253}254255private getIcon(session: IAgentSession): ThemeIcon {256if (session.status === AgentSessionStatus.InProgress) {257return Codicon.sessionInProgress;258}259260if (session.status === AgentSessionStatus.NeedsInput) {261return Codicon.report;262}263264if (session.status === AgentSessionStatus.Failed) {265return Codicon.error;266}267268if (!session.isRead() && !session.isArchived()) {269return Codicon.circleFilled;270}271272return Codicon.circleSmallFilled;273}274275private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate, hasBadge: boolean): void {276const description = session.element.description;277if (description) {278this.renderMarkdownOrText(description, template.description, template.elementDisposable);279}280281// Fallback to state label282else {283if (session.element.status === AgentSessionStatus.InProgress) {284template.description.textContent = localize('chat.session.status.inProgress', "Working...");285} else if (session.element.status === AgentSessionStatus.NeedsInput) {286template.description.textContent = localize('chat.session.status.needsInput', "Input needed.");287} else if (hasBadge && session.element.status === AgentSessionStatus.Completed) {288template.description.textContent = ''; // no description if completed and has badge289} else if (290session.element.timing.finishedOrFailedTime &&291session.element.timing.inProgressTime &&292session.element.timing.finishedOrFailedTime > session.element.timing.inProgressTime293) {294const duration = this.toDuration(session.element.timing.inProgressTime, session.element.timing.finishedOrFailedTime, false);295296template.description.textContent = session.element.status === AgentSessionStatus.Failed ?297localize('chat.session.status.failedAfter', "Failed after {0}.", duration ?? '1s') :298localize('chat.session.status.completedAfter', "Completed in {0}.", duration ?? '1s');299} else {300template.description.textContent = session.element.status === AgentSessionStatus.Failed ?301localize('chat.session.status.failed', "Failed") :302localize('chat.session.status.completed', "Completed");303}304}305}306307private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean): string | undefined {308const elapsed = Math.round((endTime - startTime) / 1000) * 1000;309if (elapsed < 1000) {310return undefined;311}312313return getDurationString(elapsed, useFullTimeWords);314}315316private renderStatus(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {317318const getTimeLabel = (session: IAgentSession) => {319let timeLabel: string | undefined;320if (session.status === AgentSessionStatus.InProgress && session.timing.inProgressTime) {321timeLabel = this.toDuration(session.timing.inProgressTime, Date.now(), false);322}323324if (!timeLabel) {325timeLabel = fromNow(session.timing.endTime || session.timing.startTime);326}327328return timeLabel;329};330331// Provider icon (hide for local sessions)332if (!isLocalAgentSessionItem(session.element)) {333template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`;334} else {335template.statusProviderIcon.className = 'agent-session-status-provider-icon hidden';336}337338// Time label339template.statusTime.textContent = getTimeLabel(session.element);340const timer = template.elementDisposable.add(new IntervalTimer());341timer.cancelAndSet(() => template.statusTime.textContent = getTimeLabel(session.element), session.element.status === AgentSessionStatus.InProgress ? 1000 /* every second */ : 60 * 1000 /* every minute */);342}343344private renderHover(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {345template.elementDisposable.add(346this.hoverService.setupDelayedHover(template.element, () => ({347content: this.buildTooltip(session.element),348style: HoverStyle.Pointer,349position: {350hoverPosition: this.options.getHoverPosition()351}352}), { groupId: 'agent.sessions' })353);354}355356private buildTooltip(session: IAgentSession): IMarkdownString {357const lines: string[] = [];358359// Title360lines.push(`**${session.label}**`);361362// Tooltip (from provider)363if (session.tooltip) {364const tooltip = typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value;365lines.push(tooltip);366} else {367368// Description369if (session.description) {370const description = typeof session.description === 'string' ? session.description : session.description.value;371lines.push(description);372}373374// Badge375if (session.badge) {376const badge = typeof session.badge === 'string' ? session.badge : session.badge.value;377lines.push(badge);378}379}380381// Details line: Status • Provider • Duration/Time382const details: string[] = [];383384// Status385details.push(toStatusLabel(session.status));386387// Provider388details.push(session.providerLabel);389390// Duration or start time391if (session.timing.finishedOrFailedTime && session.timing.inProgressTime) {392const duration = this.toDuration(session.timing.inProgressTime, session.timing.finishedOrFailedTime, true);393if (duration) {394details.push(duration);395}396} else {397details.push(fromNow(session.timing.startTime, true, true));398}399400lines.push(details.join(' • '));401402// Diff information403const diff = getAgentChangesSummary(session.changes);404if (diff && hasValidDiff(session.changes)) {405const diffParts: string[] = [];406if (diff.files > 0) {407diffParts.push(diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files));408}409if (diff.insertions > 0) {410diffParts.push(`+${diff.insertions}`);411}412if (diff.deletions > 0) {413diffParts.push(`-${diff.deletions}`);414}415if (diffParts.length > 0) {416lines.push(`$(diff) ${diffParts.join(', ')}`);417}418}419420// Archived status421if (session.isArchived()) {422lines.push(`$(archive) ${localize('tooltip.archived', "Archived")}`);423}424425return new MarkdownString(lines.join('\n\n'), { supportThemeIcons: true });426}427428renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSession>, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {429throw new Error('Should never happen since session is incompressible');430}431432disposeElement(element: ITreeNode<IAgentSession, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {433template.elementDisposable.clear();434}435436disposeTemplate(templateData: IAgentSessionItemTemplate): void {437templateData.disposables.dispose();438}439}440441function toStatusLabel(status: AgentSessionStatus): string {442let statusLabel: string;443switch (status) {444case AgentSessionStatus.NeedsInput:445statusLabel = localize('agentSessionNeedsInput', "Needs Input");446break;447case AgentSessionStatus.InProgress:448statusLabel = localize('agentSessionInProgress', "In Progress");449break;450case AgentSessionStatus.Failed:451statusLabel = localize('agentSessionFailed', "Failed");452break;453default:454statusLabel = localize('agentSessionCompleted', "Completed");455}456457return statusLabel;458}459460//#endregion461462//#region Section Header Renderer463464interface IAgentSessionSectionTemplate {465readonly container: HTMLElement;466readonly label: HTMLSpanElement;467readonly toolbar: MenuWorkbenchToolBar;468readonly contextKeyService: IContextKeyService;469readonly disposables: IDisposable;470}471472export class AgentSessionSectionRenderer implements ICompressibleTreeRenderer<IAgentSessionSection, FuzzyScore, IAgentSessionSectionTemplate> {473474static readonly TEMPLATE_ID = 'agent-session-section';475476readonly templateId = AgentSessionSectionRenderer.TEMPLATE_ID;477478constructor(479@IInstantiationService private readonly instantiationService: IInstantiationService,480@IContextKeyService private readonly contextKeyService: IContextKeyService,481) { }482483renderTemplate(container: HTMLElement): IAgentSessionSectionTemplate {484const disposables = new DisposableStore();485486const elements = h(487'div.agent-session-section@container',488[489h('span.agent-session-section-label@label'),490h('div.agent-session-section-toolbar@toolbar')491]492);493494const contextKeyService = disposables.add(this.contextKeyService.createScoped(elements.container));495const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));496const toolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, elements.toolbar, MenuId.AgentSessionSectionToolbar, {497menuOptions: { shouldForwardArgs: true },498}));499500container.appendChild(elements.container);501502return {503container: elements.container,504label: elements.label,505toolbar,506contextKeyService,507disposables508};509}510511renderElement(element: ITreeNode<IAgentSessionSection, FuzzyScore>, index: number, template: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {512513// Label514template.label.textContent = element.element.label;515516// Toolbar517ChatContextKeys.agentSessionSection.bindTo(template.contextKeyService).set(element.element.section);518template.toolbar.context = element.element;519}520521renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSessionSection>, FuzzyScore>, index: number, templateData: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {522throw new Error('Should never happen since section header is incompressible');523}524525disposeElement(element: ITreeNode<IAgentSessionSection, FuzzyScore>, index: number, template: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {526// noop527}528529disposeTemplate(templateData: IAgentSessionSectionTemplate): void {530templateData.disposables.dispose();531}532}533534//#endregion535536export class AgentSessionsListDelegate implements IListVirtualDelegate<AgentSessionListItem> {537538static readonly ITEM_HEIGHT = 52;539static readonly SECTION_HEIGHT = 26;540541getHeight(element: AgentSessionListItem): number {542if (isAgentSessionSection(element)) {543return AgentSessionsListDelegate.SECTION_HEIGHT;544}545546return AgentSessionsListDelegate.ITEM_HEIGHT;547}548549getTemplateId(element: AgentSessionListItem): string {550if (isAgentSessionSection(element)) {551return AgentSessionSectionRenderer.TEMPLATE_ID;552}553554return AgentSessionRenderer.TEMPLATE_ID;555}556}557558export class AgentSessionsAccessibilityProvider implements IListAccessibilityProvider<AgentSessionListItem> {559560getWidgetAriaLabel(): string {561return localize('agentSessions', "Agent Sessions");562}563564getAriaLabel(element: AgentSessionListItem): string | null {565if (isAgentSessionSection(element)) {566return localize('agentSessionSectionAriaLabel', "{0} sessions section", element.label);567}568569return localize('agentSessionItemAriaLabel', "{0} session {1} ({2}), created {3}", element.providerLabel, element.label, toStatusLabel(element.status), new Date(element.timing.startTime).toLocaleString());570}571}572573export interface IAgentSessionsFilterExcludes {574readonly providers: readonly string[];575readonly states: readonly AgentSessionStatus[];576577readonly archived: boolean;578readonly read: boolean;579}580581export interface IAgentSessionsFilter {582583/**584* An event that fires when the filter changes and sessions585* should be re-evaluated.586*/587readonly onDidChange: Event<void>;588589/**590* Optional limit on the number of sessions to show.591*/592readonly limitResults?: () => number | undefined;593594/**595* Whether to show section headers to group sessions.596* When false, sessions are shown as a flat list.597*/598readonly groupResults?: () => boolean | undefined;599600/**601* A callback to notify the filter about the number of602* results after filtering.603*/604notifyResults?(count: number): void;605606/**607* The logic to exclude sessions from the view.608*/609exclude(session: IAgentSession): boolean;610611/**612* Get the current filter excludes for display in the UI.613*/614getExcludes(): IAgentSessionsFilterExcludes;615}616617export class AgentSessionsDataSource implements IAsyncDataSource<IAgentSessionsModel, AgentSessionListItem> {618619constructor(620private readonly filter: IAgentSessionsFilter | undefined,621private readonly sorter: ITreeSorter<IAgentSession>,622) { }623624hasChildren(element: IAgentSessionsModel | AgentSessionListItem): boolean {625626// Sessions model627if (isAgentSessionsModel(element)) {628return true;629}630631// Sessions section632else if (isAgentSessionSection(element)) {633return element.sessions.length > 0;634}635636// Session element637else {638return false;639}640}641642getChildren(element: IAgentSessionsModel | AgentSessionListItem): Iterable<AgentSessionListItem> {643644// Sessions model645if (isAgentSessionsModel(element)) {646647// Apply filter if configured648let filteredSessions = element.sessions.filter(session => !this.filter?.exclude(session));649650// Apply sorter unless we group into sections or we are to limit results651const limitResultsCount = this.filter?.limitResults?.();652if (!this.filter?.groupResults?.() || typeof limitResultsCount === 'number') {653filteredSessions.sort(this.sorter.compare.bind(this.sorter));654}655656// Apply limiter if configured (requires sorting)657if (typeof limitResultsCount === 'number') {658filteredSessions = filteredSessions.slice(0, limitResultsCount);659}660661// Callback results count662this.filter?.notifyResults?.(filteredSessions.length);663664// Group sessions into sections if enabled665if (this.filter?.groupResults?.()) {666return this.groupSessionsIntoSections(filteredSessions);667}668669// Otherwise return flat sorted list670return filteredSessions;671}672673// Sessions section674else if (isAgentSessionSection(element)) {675return element.sessions;676}677678// Session element679else {680return [];681}682}683684private groupSessionsIntoSections(sessions: IAgentSession[]): AgentSessionListItem[] {685const result: AgentSessionListItem[] = [];686687const sortedSessions = sessions.sort(this.sorter.compare.bind(this.sorter));688const groupedSessions = groupAgentSessions(sortedSessions);689690for (const { sessions, section, label } of groupedSessions.values()) {691if (sessions.length === 0) {692continue;693}694695result.push({ section, label, sessions });696}697698return result;699}700}701702const DAY_THRESHOLD = 24 * 60 * 60 * 1000;703const WEEK_THRESHOLD = 7 * DAY_THRESHOLD;704705export const AgentSessionSectionLabels = {706[AgentSessionSection.InProgress]: localize('agentSessions.inProgressSection', "In Progress"),707[AgentSessionSection.Today]: localize('agentSessions.todaySection', "Today"),708[AgentSessionSection.Yesterday]: localize('agentSessions.yesterdaySection', "Yesterday"),709[AgentSessionSection.Week]: localize('agentSessions.weekSection', "Last Week"),710[AgentSessionSection.Older]: localize('agentSessions.olderSection', "Older"),711[AgentSessionSection.Archived]: localize('agentSessions.archivedSection', "Archived"),712};713714export function groupAgentSessions(sessions: IAgentSession[]): Map<AgentSessionSection, IAgentSessionSection> {715const now = Date.now();716const startOfToday = new Date(now).setHours(0, 0, 0, 0);717const startOfYesterday = startOfToday - DAY_THRESHOLD;718const weekThreshold = now - WEEK_THRESHOLD;719720const inProgressSessions: IAgentSession[] = [];721const todaySessions: IAgentSession[] = [];722const yesterdaySessions: IAgentSession[] = [];723const weekSessions: IAgentSession[] = [];724const olderSessions: IAgentSession[] = [];725const archivedSessions: IAgentSession[] = [];726727for (const session of sessions) {728if (isSessionInProgressStatus(session.status)) {729inProgressSessions.push(session);730} else if (session.isArchived()) {731archivedSessions.push(session);732} else {733const sessionTime = session.timing.endTime || session.timing.startTime;734if (sessionTime >= startOfToday) {735todaySessions.push(session);736} else if (sessionTime >= startOfYesterday) {737yesterdaySessions.push(session);738} else if (sessionTime >= weekThreshold) {739weekSessions.push(session);740} else {741olderSessions.push(session);742}743}744}745746return new Map<AgentSessionSection, IAgentSessionSection>([747[AgentSessionSection.InProgress, { section: AgentSessionSection.InProgress, label: AgentSessionSectionLabels[AgentSessionSection.InProgress], sessions: inProgressSessions }],748[AgentSessionSection.Today, { section: AgentSessionSection.Today, label: AgentSessionSectionLabels[AgentSessionSection.Today], sessions: todaySessions }],749[AgentSessionSection.Yesterday, { section: AgentSessionSection.Yesterday, label: AgentSessionSectionLabels[AgentSessionSection.Yesterday], sessions: yesterdaySessions }],750[AgentSessionSection.Week, { section: AgentSessionSection.Week, label: AgentSessionSectionLabels[AgentSessionSection.Week], sessions: weekSessions }],751[AgentSessionSection.Older, { section: AgentSessionSection.Older, label: AgentSessionSectionLabels[AgentSessionSection.Older], sessions: olderSessions }],752[AgentSessionSection.Archived, { section: AgentSessionSection.Archived, label: localize('agentSessions.archivedSectionWithCount', "Archived ({0})", archivedSessions.length), sessions: archivedSessions }],753]);754}755756export class AgentSessionsIdentityProvider implements IIdentityProvider<IAgentSessionsModel | AgentSessionListItem> {757758getId(element: IAgentSessionsModel | AgentSessionListItem): string {759if (isAgentSessionSection(element)) {760return `section-${element.section}`;761}762763if (isAgentSession(element)) {764return element.resource.toString();765}766767return 'agent-sessions-id';768}769}770771export class AgentSessionsCompressionDelegate implements ITreeCompressionDelegate<AgentSessionListItem> {772773isIncompressible(element: AgentSessionListItem): boolean {774return true;775}776}777778export interface IAgentSessionsSorterOptions {779overrideCompare?(sessionA: IAgentSession, sessionB: IAgentSession): number | undefined;780}781782export class AgentSessionsSorter implements ITreeSorter<IAgentSession> {783784constructor(private readonly options?: IAgentSessionsSorterOptions) { }785786compare(sessionA: IAgentSession, sessionB: IAgentSession): number {787788// Input Needed789const aNeedsInput = sessionA.status === AgentSessionStatus.NeedsInput;790const bNeedsInput = sessionB.status === AgentSessionStatus.NeedsInput;791792if (aNeedsInput && !bNeedsInput) {793return -1; // a (needs input) comes before b (other)794}795if (!aNeedsInput && bNeedsInput) {796return 1; // a (other) comes after b (needs input)797}798799// In Progress800const aInProgress = sessionA.status === AgentSessionStatus.InProgress;801const bInProgress = sessionB.status === AgentSessionStatus.InProgress;802803if (aInProgress && !bInProgress) {804return -1; // a (in-progress) comes before b (finished)805}806if (!aInProgress && bInProgress) {807return 1; // a (finished) comes after b (in-progress)808}809810// Archived811const aArchived = sessionA.isArchived();812const bArchived = sessionB.isArchived();813814if (!aArchived && bArchived) {815return -1; // a (non-archived) comes before b (archived)816}817if (aArchived && !bArchived) {818return 1; // a (archived) comes after b (non-archived)819}820821// Before we compare by time, allow override822const override = this.options?.overrideCompare?.(sessionA, sessionB);823if (typeof override === 'number') {824return override;825}826827//Sort by end or start time (most recent first)828return (sessionB.timing.endTime || sessionB.timing.startTime) - (sessionA.timing.endTime || sessionA.timing.startTime);829}830}831832export class AgentSessionsKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider<AgentSessionListItem> {833834getKeyboardNavigationLabel(element: AgentSessionListItem): string {835if (isAgentSessionSection(element)) {836return element.label;837}838839return element.label;840}841842getCompressedNodeKeyboardNavigationLabel(elements: AgentSessionListItem[]): { toString(): string | undefined } | undefined {843return undefined; // not enabled844}845}846847export class AgentSessionsDragAndDrop extends Disposable implements ITreeDragAndDrop<AgentSessionListItem> {848849constructor(850@IInstantiationService private readonly instantiationService: IInstantiationService851) {852super();853}854855onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {856const elements = (data.getData() as AgentSessionListItem[]).filter(e => isAgentSession(e));857const uris = coalesce(elements.map(e => e.resource));858this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, uris, originalEvent));859}860861getDragURI(element: AgentSessionListItem): string | null {862if (isAgentSessionSection(element)) {863return null; // section headers are not draggable864}865866return element.resource.toString();867}868869getDragLabel?(elements: AgentSessionListItem[], originalEvent: DragEvent): string | undefined {870const sessions = elements.filter(e => isAgentSession(e));871if (sessions.length === 1) {872return sessions[0].label;873}874875return localize('agentSessions.dragLabel', "{0} agent sessions", sessions.length);876}877878onDragOver(data: IDragAndDropData, targetElement: AgentSessionListItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {879return false;880}881882drop(data: IDragAndDropData, targetElement: AgentSessionListItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void { }883}884885886