mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
sessions - tweaks to sessions list for better readability (#302433)
This commit is contained in:
17
.github/workflows/sessions-e2e.yml
vendored
17
.github/workflows/sessions-e2e.yml
vendored
@@ -1,13 +1,14 @@
|
||||
name: Sessions E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'release/*'
|
||||
paths:
|
||||
- 'src/vs/sessions/**'
|
||||
- 'scripts/code-sessions-web.*'
|
||||
# Disabled: Flaky
|
||||
# on:
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - main
|
||||
# - 'release/*'
|
||||
# paths:
|
||||
# - 'src/vs/sessions/**'
|
||||
# - 'scripts/code-sessions-web.*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -82,3 +82,9 @@
|
||||
padding: 4px 8px 6px 8px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ---- Session List ---- */
|
||||
|
||||
.agent-sessions-workbench .agent-session-title {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import { localize } from '../../../../../nls.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
|
||||
import { IChatSessionTiming } from '../../common/chatService/chatService.js';
|
||||
import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { getChatSessionType } from '../../common/model/chatUri.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
@@ -180,7 +178,3 @@ export const agentSessionSelectedUnfocusedBadgeBorder = registerColor(
|
||||
|
||||
export const AGENT_SESSION_RENAME_ACTION_ID = 'agentSession.rename';
|
||||
export const AGENT_SESSION_DELETE_ACTION_ID = 'agentSession.delete';
|
||||
|
||||
export function getAgentSessionTime(timing: IChatSessionTiming): number {
|
||||
return timing.lastRequestStarted ?? timing.created;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent
|
||||
import { KeyCode } from '../../../../../base/common/keyCodes.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js';
|
||||
import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js';
|
||||
import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter } from './agentSessionsViewer.js';
|
||||
import { AgentSessionsGrouping } from './agentSessionsFilter.js';
|
||||
import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js';
|
||||
import { FuzzyScore } from '../../../../../base/common/filters.js';
|
||||
@@ -33,7 +33,7 @@ import { IAgentSessionsService } from './agentSessionsService.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js';
|
||||
import { IStyleOverride } from '../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { getAgentSessionTime, IAgentSessionsControl } from './agentSessions.js';
|
||||
import { IAgentSessionsControl } from './agentSessions.js';
|
||||
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ISessionOpenOptions, openSession } from './agentSessionsOpener.js';
|
||||
@@ -44,7 +44,7 @@ import { IChatWidget } from '../chat.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
|
||||
import { ILogService } from '../../../../../platform/log/common/log.js';
|
||||
|
||||
export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions {
|
||||
export interface IAgentSessionsControlOptions {
|
||||
readonly overrideStyles: IStyleOverride<IListStyles>;
|
||||
readonly filter: IAgentSessionsFilter;
|
||||
readonly source: string;
|
||||
@@ -243,7 +243,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
|
||||
return false;
|
||||
};
|
||||
|
||||
const sorter = new AgentSessionsSorter(this.options);
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const approvalModel = this.options.enableApprovalRow ? this._register(this.instantiationService.createInstance(AgentSessionApprovalModel)) : undefined;
|
||||
const activeSessionResource = observableValue<URI | undefined>(this, undefined);
|
||||
const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, {
|
||||
@@ -369,7 +369,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
|
||||
|
||||
return this.agentSessionsService.model.sessions.some(session =>
|
||||
!session.isArchived() &&
|
||||
getAgentSessionTime(session.timing) >= startOfToday
|
||||
session.timing.created >= startOfToday
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ISessionOpenOptions, openSession } from './agentSessionsOpener.js';
|
||||
import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js';
|
||||
import { IAgentSessionsService } from './agentSessionsService.js';
|
||||
import { AgentSessionsSorter, groupAgentSessionsByDate, sessionDateFromNow } from './agentSessionsViewer.js';
|
||||
import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID, getAgentSessionTime } from './agentSessions.js';
|
||||
import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID } from './agentSessions.js';
|
||||
import { AgentSessionsFilter } from './agentSessionsFilter.js';
|
||||
|
||||
interface ISessionPickItem extends IQuickPickItem {
|
||||
@@ -44,7 +44,7 @@ export const deleteButton: IQuickInputButton = {
|
||||
|
||||
export function getSessionDescription(session: IAgentSession): string {
|
||||
const descriptionText = typeof session.description === 'string' ? session.description : session.description ? renderAsPlaintext(session.description) : undefined;
|
||||
const timeAgo = sessionDateFromNow(getAgentSessionTime(session.timing));
|
||||
const timeAgo = sessionDateFromNow(session.timing.created);
|
||||
const descriptionParts = [descriptionText, session.providerLabel, timeAgo].filter(part => !!part);
|
||||
|
||||
return descriptionParts.join(' • ');
|
||||
|
||||
@@ -41,7 +41,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
|
||||
import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js';
|
||||
import { AgentSessionProviders, getAgentSessionTime } from './agentSessions.js';
|
||||
import { AgentSessionProviders } from './agentSessions.js';
|
||||
import { AgentSessionsGrouping } from './agentSessionsFilter.js';
|
||||
import { autorun, IObservable } from '../../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
@@ -144,19 +144,17 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
h('div.agent-session-title-toolbar@titleToolbar'),
|
||||
]),
|
||||
h('div.agent-session-details-row', [
|
||||
h('div.agent-session-badge@badge'),
|
||||
h('span.agent-session-separator@separator'),
|
||||
h('div.agent-session-diff-container@diffContainer',
|
||||
[
|
||||
h('span.agent-session-diff-added@addedSpan'),
|
||||
h('span.agent-session-diff-removed@removedSpan')
|
||||
]),
|
||||
h('div.agent-session-description@description'),
|
||||
h('div.agent-session-details-right', [
|
||||
h('div.agent-session-badge@badge'),
|
||||
h('span.agent-session-separator@separator'),
|
||||
h('div.agent-session-status@statusContainer', [
|
||||
h('span.agent-session-status-provider-icon@statusProviderIcon'),
|
||||
h('span.agent-session-status-time@statusTime')
|
||||
]),
|
||||
h('div.agent-session-status@statusContainer', [
|
||||
h('span.agent-session-status-provider-icon@statusProviderIcon'),
|
||||
h('span.agent-session-status-time@statusTime')
|
||||
]),
|
||||
]),
|
||||
h('div.agent-session-approval-row@approvalRow', [
|
||||
@@ -180,11 +178,11 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
icon: elements.icon,
|
||||
title: disposables.add(new IconLabel(elements.title, { supportHighlights: true, supportIcons: true })),
|
||||
titleToolbar,
|
||||
badge: elements.badge,
|
||||
separator: elements.separator,
|
||||
diffContainer: elements.diffContainer,
|
||||
diffAddedSpan: elements.addedSpan,
|
||||
diffRemovedSpan: elements.removedSpan,
|
||||
badge: elements.badge,
|
||||
separator: elements.separator,
|
||||
description: elements.description,
|
||||
statusContainer: elements.statusContainer,
|
||||
statusProviderIcon: elements.statusProviderIcon,
|
||||
@@ -211,7 +209,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
template.element.classList.toggle('archived', session.element.isArchived());
|
||||
|
||||
// Icon
|
||||
template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(this.getIcon(session.element))}`;
|
||||
template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(this.getIcon(session.element))}${session.element.status === AgentSessionStatus.NeedsInput ? ' needs-input' : ''}`;
|
||||
|
||||
// Title
|
||||
const markdownTitle = new MarkdownString(session.element.label);
|
||||
@@ -223,6 +221,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
ChatContextKeys.agentSessionType.bindTo(template.contextKeyService).set(session.element.providerType);
|
||||
template.titleToolbar.context = session.element;
|
||||
|
||||
// Badge
|
||||
const hasBadge = this.renderBadge(session, template);
|
||||
|
||||
// Diff information
|
||||
let hasDiff = false;
|
||||
const { changes: diff } = session.element;
|
||||
@@ -231,7 +232,6 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
hasDiff = true;
|
||||
}
|
||||
}
|
||||
template.diffContainer.classList.toggle('has-diff', hasDiff);
|
||||
|
||||
let hasAgentSessionChanges = false;
|
||||
if (
|
||||
@@ -248,20 +248,21 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
|
||||
ChatContextKeys.hasAgentSessionChanges.bindTo(template.contextKeyService).set(hasAgentSessionChanges);
|
||||
|
||||
// Badge
|
||||
const hasBadge = this.renderBadge(session, template);
|
||||
template.badge.classList.toggle('has-badge', hasBadge);
|
||||
|
||||
// Description (unless diff is shown)
|
||||
if (!hasDiff) {
|
||||
this.renderDescription(session, template);
|
||||
}
|
||||
|
||||
// Separator (dot between badge and timestamp)
|
||||
template.separator.classList.toggle('has-separator', hasBadge);
|
||||
// Description
|
||||
const hasDescription = this.renderDescription(session, template);
|
||||
|
||||
// Status
|
||||
this.renderStatus(session, template);
|
||||
const hasStatus = this.renderStatus(session, template);
|
||||
|
||||
// When in progress with a description, only show description in the details row
|
||||
const hideDetails = hasDescription && isSessionInProgressStatus(session.element.status);
|
||||
template.badge.classList.toggle('has-badge', hasBadge && !hideDetails);
|
||||
template.diffContainer.classList.toggle('has-diff', hasDiff && !hideDetails);
|
||||
template.statusContainer.classList.toggle('hidden', hideDetails);
|
||||
template.separator.classList.toggle('has-separator', !hideDetails && hasBadge && hasDiff);
|
||||
template.description.classList.toggle('has-separator', hasDescription && !hideDetails && (hasBadge || hasDiff));
|
||||
template.statusContainer.classList.toggle('has-separator', !hideDetails && hasStatus && (hasBadge || hasDiff || hasDescription));
|
||||
|
||||
// Hover
|
||||
this.renderHover(session, template);
|
||||
@@ -295,10 +296,21 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
}
|
||||
}
|
||||
|
||||
this.renderMarkdownOrText(badge, template.badge, template.elementDisposable);
|
||||
this.renderMarkdownOrText(this.stripCodicons(badge), template.badge, template.elementDisposable);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private stripCodicons(content: string | IMarkdownString): string | IMarkdownString {
|
||||
const raw = typeof content === 'string' ? content : content.value;
|
||||
const stripped = raw.replace(/\$\([a-z0-9\-]+\)\s*/gi, '').trim();
|
||||
if (typeof content === 'string') {
|
||||
return stripped;
|
||||
}
|
||||
|
||||
return MarkdownString.lift({ ...content, value: stripped });
|
||||
}
|
||||
|
||||
private renderMarkdownOrText(content: string | IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {
|
||||
if (typeof content === 'string') {
|
||||
container.textContent = content;
|
||||
@@ -321,6 +333,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
return false;
|
||||
}
|
||||
|
||||
if (diff.insertions === 0 && diff.deletions === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (diff.insertions >= 0 /* render even `0` for more homogeneity */) {
|
||||
template.diffAddedSpan.textContent = `+${diff.insertions}`;
|
||||
}
|
||||
@@ -338,7 +354,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
}
|
||||
|
||||
if (session.status === AgentSessionStatus.NeedsInput) {
|
||||
return Codicon.report;
|
||||
return Codicon.circleFilled;
|
||||
}
|
||||
|
||||
if (session.status === AgentSessionStatus.Failed) {
|
||||
@@ -352,33 +368,27 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
return Codicon.circleSmallFilled;
|
||||
}
|
||||
|
||||
private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
|
||||
private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
|
||||
const description = session.element.description;
|
||||
if (description) {
|
||||
this.renderMarkdownOrText(description, template.description, template.elementDisposable);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to state label
|
||||
if (session.element.status === AgentSessionStatus.InProgress) {
|
||||
template.description.textContent = localize('chat.session.status.inProgress', "Working...");
|
||||
return true;
|
||||
} else if (session.element.status === AgentSessionStatus.NeedsInput) {
|
||||
template.description.textContent = localize('chat.session.status.needsInput', "Input needed.");
|
||||
} else if (
|
||||
session.element.timing.lastRequestEnded &&
|
||||
session.element.timing.lastRequestStarted &&
|
||||
session.element.timing.lastRequestEnded > session.element.timing.lastRequestStarted
|
||||
) {
|
||||
const duration = this.toDuration(session.element.timing.lastRequestStarted, session.element.timing.lastRequestEnded, false, true);
|
||||
|
||||
template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
|
||||
localize('chat.session.status.failedAfter', "Failed after {0}", duration) :
|
||||
localize('chat.session.status.completedAfter', "Completed in {0}", duration);
|
||||
} else {
|
||||
template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
|
||||
localize('chat.session.status.failed', "Failed") :
|
||||
localize('chat.session.status.completed', "Completed");
|
||||
return true;
|
||||
} else if (session.element.status === AgentSessionStatus.Failed) {
|
||||
template.description.textContent = localize('chat.session.status.failed', "Failed");
|
||||
return true;
|
||||
}
|
||||
|
||||
template.description.textContent = '';
|
||||
return false;
|
||||
}
|
||||
|
||||
private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean, disallowNow: boolean): string {
|
||||
@@ -390,7 +400,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
return getDurationString(elapsed, useFullTimeWords);
|
||||
}
|
||||
|
||||
private renderStatus(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
|
||||
private renderStatus(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
|
||||
|
||||
const getTimeLabel = (session: IAgentSession) => {
|
||||
let timeLabel: string | undefined;
|
||||
@@ -399,12 +409,12 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
}
|
||||
|
||||
if (!timeLabel) {
|
||||
const date = getAgentSessionTime(session.timing);
|
||||
const date = session.timing.created;
|
||||
const seconds = Math.round((new Date().getTime() - date) / 1000);
|
||||
if (seconds < 60) {
|
||||
timeLabel = localize('secondsDuration', "now");
|
||||
} else {
|
||||
timeLabel = sessionDateFromNow(date);
|
||||
timeLabel = sessionDateFromNow(date, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +438,8 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
|
||||
template.statusTime.textContent = getTimeLabel(session.element);
|
||||
const timer = template.elementDisposable.add(new IntervalTimer());
|
||||
timer.cancelAndSet(() => template.statusTime.textContent = getTimeLabel(session.element), session.element.status === AgentSessionStatus.InProgress ? 1000 /* every second */ : 60 * 1000 /* every minute */);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private renderHover(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
|
||||
@@ -821,9 +833,14 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou
|
||||
}
|
||||
|
||||
private groupSessionsIntoSections(sessions: IAgentSession[]): AgentSessionListItem[] {
|
||||
const sortedSessions = sessions.sort(this.sorter.compare.bind(this.sorter));
|
||||
const isCapped = this.filter?.groupResults?.() === AgentSessionsGrouping.Capped;
|
||||
|
||||
if (this.filter?.groupResults?.() === AgentSessionsGrouping.Capped) {
|
||||
const sorter = this.sorter;
|
||||
const sortedSessions = sorter instanceof AgentSessionsSorter
|
||||
? sessions.sort((a, b) => sorter.compare(a, b, isCapped /* special sorting for when results are capped to keep active ones top */))
|
||||
: sessions.sort(sorter.compare.bind(sorter));
|
||||
|
||||
if (isCapped) {
|
||||
if (this.filter?.getExcludes().read) {
|
||||
return sortedSessions; // When filtering to show only unread sessions, show a flat list
|
||||
}
|
||||
@@ -1098,7 +1115,7 @@ export function groupAgentSessionsByDate(sessions: IAgentSession[]): Map<AgentSe
|
||||
if (session.isArchived()) {
|
||||
archivedSessions.push(session);
|
||||
} else {
|
||||
const sessionTime = getAgentSessionTime(session.timing);
|
||||
const sessionTime = session.timing.created;
|
||||
if (sessionTime >= startOfToday) {
|
||||
todaySessions.push(session);
|
||||
} else if (sessionTime >= startOfYesterday) {
|
||||
@@ -1120,7 +1137,7 @@ export function groupAgentSessionsByDate(sessions: IAgentSession[]): Map<AgentSe
|
||||
]);
|
||||
}
|
||||
|
||||
export function sessionDateFromNow(sessionTime: number): string {
|
||||
export function sessionDateFromNow(sessionTime: number, appendAgoLabel?: boolean): string {
|
||||
const now = Date.now();
|
||||
const startOfToday = new Date(now).setHours(0, 0, 0, 0);
|
||||
const startOfYesterday = startOfToday - DAY_THRESHOLD;
|
||||
@@ -1133,14 +1150,18 @@ export function sessionDateFromNow(sessionTime: number): string {
|
||||
// normalization logic.
|
||||
|
||||
if (sessionTime < startOfToday && sessionTime >= startOfYesterday) {
|
||||
return localize('date.fromNow.days.singular', '1 day');
|
||||
return appendAgoLabel
|
||||
? localize('date.fromNow.days.singular.ago', '1 day ago')
|
||||
: localize('date.fromNow.days.singular', '1 day');
|
||||
}
|
||||
|
||||
if (sessionTime < startOfYesterday && sessionTime >= startOfTwoDaysAgo) {
|
||||
return localize('date.fromNow.days.multiple', '2 days');
|
||||
return appendAgoLabel
|
||||
? localize('date.fromNow.days.multiple.ago', '2 days ago')
|
||||
: localize('date.fromNow.days.multiple', '2 days');
|
||||
}
|
||||
|
||||
return fromNow(sessionTime, false);
|
||||
return fromNow(sessionTime, appendAgoLabel);
|
||||
}
|
||||
|
||||
export class AgentSessionsIdentityProvider implements IIdentityProvider<IAgentSessionsModel | AgentSessionListItem> {
|
||||
@@ -1172,25 +1193,21 @@ export class AgentSessionsCompressionDelegate implements ITreeCompressionDelegat
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAgentSessionsSorterOptions {
|
||||
overrideCompare?(sessionA: IAgentSession, sessionB: IAgentSession): number | undefined;
|
||||
}
|
||||
|
||||
export class AgentSessionsSorter implements ITreeSorter<IAgentSession> {
|
||||
|
||||
constructor(private readonly options?: IAgentSessionsSorterOptions) { }
|
||||
compare(sessionA: IAgentSession, sessionB: IAgentSession, prioritizeActiveSessions = false): number {
|
||||
|
||||
compare(sessionA: IAgentSession, sessionB: IAgentSession): number {
|
||||
// Special sorting if enabled
|
||||
if (prioritizeActiveSessions) {
|
||||
const aNeedsInput = sessionA.status === AgentSessionStatus.NeedsInput;
|
||||
const bNeedsInput = sessionB.status === AgentSessionStatus.NeedsInput;
|
||||
|
||||
// Input Needed
|
||||
const aNeedsInput = sessionA.status === AgentSessionStatus.NeedsInput;
|
||||
const bNeedsInput = sessionB.status === AgentSessionStatus.NeedsInput;
|
||||
|
||||
if (aNeedsInput && !bNeedsInput) {
|
||||
return -1; // a (needs input) comes before b (other)
|
||||
}
|
||||
if (!aNeedsInput && bNeedsInput) {
|
||||
return 1; // a (other) comes after b (needs input)
|
||||
if (aNeedsInput && !bNeedsInput) {
|
||||
return -1; // a (needs input) comes before b (other)
|
||||
}
|
||||
if (!aNeedsInput && bNeedsInput) {
|
||||
return 1; // a (other) comes after b (needs input)
|
||||
}
|
||||
}
|
||||
|
||||
// Archived
|
||||
@@ -1204,15 +1221,9 @@ export class AgentSessionsSorter implements ITreeSorter<IAgentSession> {
|
||||
return 1; // a (archived) comes after b (non-archived)
|
||||
}
|
||||
|
||||
// Before we compare by time, allow override
|
||||
const override = this.options?.overrideCompare?.(sessionA, sessionB);
|
||||
if (typeof override === 'number') {
|
||||
return override;
|
||||
}
|
||||
|
||||
// Sort by end or start time (most recent first)
|
||||
const timeA = getAgentSessionTime(sessionA.timing);
|
||||
const timeB = getAgentSessionTime(sessionB.timing);
|
||||
// Sort by time
|
||||
const timeA = prioritizeActiveSessions ? sessionA.timing.lastRequestStarted ?? sessionA.timing.created : sessionA.timing.created;
|
||||
const timeB = prioritizeActiveSessions ? sessionB.timing.lastRequestStarted ?? sessionB.timing.created : sessionB.timing.created;
|
||||
return timeB - timeA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
.agent-session-title {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.agent-session-status .agent-session-status-provider-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-session-item {
|
||||
@@ -112,17 +116,24 @@
|
||||
color: var(--vscode-errorForeground);
|
||||
}
|
||||
|
||||
&.codicon.codicon-report {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
&.codicon.codicon-circle-filled {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
&.codicon.codicon-circle-filled.needs-input {
|
||||
color: var(--vscode-list-warningForeground);
|
||||
animation: agent-session-needs-input-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.codicon.codicon-circle-small-filled {
|
||||
color: var(--vscode-agentSessionReadIndicator-foreground);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&.codicon.codicon-circle-filled.needs-input {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,10 +218,6 @@
|
||||
&:not(.has-badge) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codicon {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,17 +230,17 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.agent-session-title {
|
||||
font-size: 13px;
|
||||
.agent-session-description:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-session-details-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
.agent-session-description.has-separator::before {
|
||||
content: '\00B7';
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.agent-session-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-session-status {
|
||||
@@ -242,13 +249,19 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
|
||||
&.has-separator::before {
|
||||
content: '\00B7';
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-session-status-provider-icon {
|
||||
font-size: 11px;
|
||||
margin-right: 4px;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.agent-session-approval-row {
|
||||
@@ -367,3 +380,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-session-needs-input-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import { CHAT_CATEGORY } from '../actions/chatActions.js';
|
||||
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
|
||||
import { IAgentSession, isAgentSession } from '../agentSessions/agentSessionsModel.js';
|
||||
import { AgentSessionProviders } from '../agentSessions/agentSessions.js';
|
||||
import { IsSessionsWindowContext } from '../../../../common/contextkeys.js';
|
||||
|
||||
export abstract class EditingSessionAction extends Action2 {
|
||||
|
||||
@@ -358,12 +357,6 @@ export class ViewAllSessionChangesAction extends Action2 {
|
||||
group: 'navigation',
|
||||
order: 10,
|
||||
when: ChatContextKeys.hasAgentSessionChanges
|
||||
},
|
||||
{
|
||||
id: MenuId.AgentSessionItemToolbar,
|
||||
group: 'navigation',
|
||||
order: 0,
|
||||
when: ContextKeyExpr.and(ChatContextKeys.hasAgentSessionChanges, IsSessionsWindowContext.negate())
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
@@ -6,47 +6,13 @@
|
||||
import assert from 'assert';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { AgentSessionsDataSource, AgentSessionListItem, IAgentSessionsFilter, sessionDateFromNow, getRepositoryName } from '../../../browser/agentSessions/agentSessionsViewer.js';
|
||||
import { AgentSessionsDataSource, AgentSessionListItem, IAgentSessionsFilter, sessionDateFromNow, getRepositoryName, AgentSessionsSorter } from '../../../browser/agentSessions/agentSessionsViewer.js';
|
||||
import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSessionSection } from '../../../browser/agentSessions/agentSessionsModel.js';
|
||||
import { ChatSessionStatus } from '../../../common/chatSessionsService.js';
|
||||
import { ITreeSorter } from '../../../../../../base/browser/ui/tree/tree.js';
|
||||
import { Codicon } from '../../../../../../base/common/codicons.js';
|
||||
import { Event } from '../../../../../../base/common/event.js';
|
||||
import { AgentSessionsGrouping } from '../../../browser/agentSessions/agentSessionsFilter.js';
|
||||
import { getAgentSessionTime } from '../../../browser/agentSessions/agentSessions.js';
|
||||
import { IChatSessionTiming } from '../../../common/chatService/chatService.js';
|
||||
|
||||
suite('getAgentSessionTime', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('returns lastRequestStarted when available', () => {
|
||||
const timing: IChatSessionTiming = {
|
||||
created: 1000,
|
||||
lastRequestStarted: 2000,
|
||||
lastRequestEnded: 3000,
|
||||
};
|
||||
assert.strictEqual(getAgentSessionTime(timing), 2000);
|
||||
});
|
||||
|
||||
test('returns lastRequestStarted even when lastRequestEnded is undefined', () => {
|
||||
const timing: IChatSessionTiming = {
|
||||
created: 1000,
|
||||
lastRequestStarted: 2000,
|
||||
lastRequestEnded: undefined,
|
||||
};
|
||||
assert.strictEqual(getAgentSessionTime(timing), 2000);
|
||||
});
|
||||
|
||||
test('returns created when lastRequestStarted is undefined', () => {
|
||||
const timing: IChatSessionTiming = {
|
||||
created: 1000,
|
||||
lastRequestStarted: undefined,
|
||||
lastRequestEnded: undefined,
|
||||
};
|
||||
assert.strictEqual(getAgentSessionTime(timing), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
suite('sessionDateFromNow', () => {
|
||||
|
||||
@@ -91,6 +57,22 @@ suite('sessionDateFromNow', () => {
|
||||
assert.ok(result.includes('day'), `Expected days ago, got: ${result}`);
|
||||
assert.ok(!result.includes('1 day') && !result.includes('2 days'), `Should not be 1 or 2 days ago, got: ${result}`);
|
||||
});
|
||||
|
||||
test('appends "ago" when appendAgoLabel is true', () => {
|
||||
const now = Date.now();
|
||||
const startOfToday = new Date(now).setHours(0, 0, 0, 0);
|
||||
|
||||
const yesterday = startOfToday - ONE_DAY / 2;
|
||||
assert.strictEqual(sessionDateFromNow(yesterday, true), '1 day ago');
|
||||
|
||||
const startOfYesterday = startOfToday - ONE_DAY;
|
||||
const twoDaysAgo = startOfYesterday - ONE_DAY / 2;
|
||||
assert.strictEqual(sessionDateFromNow(twoDaysAgo, true), '2 days ago');
|
||||
|
||||
const fiveDaysAgo = startOfToday - 5 * ONE_DAY;
|
||||
const result = sessionDateFromNow(fiveDaysAgo, true);
|
||||
assert.ok(result.includes('ago'), `Expected "ago" in result, got: ${result}`);
|
||||
});
|
||||
});
|
||||
|
||||
suite('AgentSessionsDataSource', () => {
|
||||
@@ -166,9 +148,9 @@ suite('AgentSessionsDataSource', () => {
|
||||
function createMockSorter(): ITreeSorter<IAgentSession> {
|
||||
return {
|
||||
compare: (a, b) => {
|
||||
// Sort by end time, most recent first
|
||||
const aTime = getAgentSessionTime(a.timing);
|
||||
const bTime = getAgentSessionTime(b.timing);
|
||||
// Sort by creation time, most recent first
|
||||
const aTime = a.timing.created;
|
||||
const bTime = b.timing.created;
|
||||
return bTime - aTime;
|
||||
}
|
||||
};
|
||||
@@ -875,3 +857,92 @@ suite('AgentSessionsDataSource', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('AgentSessionsSorter', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function createSession(overrides: Partial<{
|
||||
id: string;
|
||||
status: ChatSessionStatus;
|
||||
isArchived: boolean;
|
||||
created: number;
|
||||
lastRequestStarted: number;
|
||||
}>): IAgentSession {
|
||||
const now = Date.now();
|
||||
return {
|
||||
providerType: 'test',
|
||||
providerLabel: 'Test',
|
||||
resource: URI.parse(`test://session/${overrides.id ?? 'default'}`),
|
||||
status: overrides.status ?? ChatSessionStatus.Completed,
|
||||
label: `Session ${overrides.id ?? 'default'}`,
|
||||
icon: Codicon.terminal,
|
||||
timing: {
|
||||
created: overrides.created ?? now,
|
||||
lastRequestEnded: undefined,
|
||||
lastRequestStarted: overrides.lastRequestStarted,
|
||||
},
|
||||
changes: undefined,
|
||||
metadata: undefined,
|
||||
isArchived: () => overrides.isArchived ?? false,
|
||||
setArchived: () => { },
|
||||
isRead: () => true,
|
||||
isMarkedUnread: () => false,
|
||||
setRead: () => { },
|
||||
};
|
||||
}
|
||||
|
||||
test('default: sorts by creation time (most recent first)', () => {
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const old = createSession({ id: 'old', created: 1000 });
|
||||
const recent = createSession({ id: 'recent', created: 2000 });
|
||||
|
||||
const sorted = [old, recent].sort((a, b) => sorter.compare(a, b));
|
||||
assert.deepStrictEqual(sorted.map(s => s.label), ['Session recent', 'Session old']);
|
||||
});
|
||||
|
||||
test('default: archived sessions come last', () => {
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const archived = createSession({ id: 'archived', isArchived: true, created: 3000 });
|
||||
const active = createSession({ id: 'active', created: 1000 });
|
||||
|
||||
const sorted = [archived, active].sort((a, b) => sorter.compare(a, b));
|
||||
assert.deepStrictEqual(sorted.map(s => s.label), ['Session active', 'Session archived']);
|
||||
});
|
||||
|
||||
test('default: does NOT prioritize needs-input sessions', () => {
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const needsInput = createSession({ id: 'needs', status: ChatSessionStatus.NeedsInput, created: 1000 });
|
||||
const completed = createSession({ id: 'done', status: ChatSessionStatus.Completed, created: 2000 });
|
||||
|
||||
const sorted = [needsInput, completed].sort((a, b) => sorter.compare(a, b));
|
||||
assert.deepStrictEqual(sorted.map(s => s.label), ['Session done', 'Session needs']);
|
||||
});
|
||||
|
||||
test('prioritizeActive: needs-input sessions come first', () => {
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const needsInput = createSession({ id: 'needs', status: ChatSessionStatus.NeedsInput, created: 1000 });
|
||||
const completed = createSession({ id: 'done', status: ChatSessionStatus.Completed, created: 2000 });
|
||||
|
||||
const sorted = [completed, needsInput].sort((a, b) => sorter.compare(a, b, true));
|
||||
assert.deepStrictEqual(sorted.map(s => s.label), ['Session needs', 'Session done']);
|
||||
});
|
||||
|
||||
test('prioritizeActive: archived still come last when not active', () => {
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const archived = createSession({ id: 'archived', isArchived: true, created: 3000 });
|
||||
const active = createSession({ id: 'active', created: 1000 });
|
||||
|
||||
const sorted = [archived, active].sort((a, b) => sorter.compare(a, b, true));
|
||||
assert.deepStrictEqual(sorted.map(s => s.label), ['Session active', 'Session archived']);
|
||||
});
|
||||
|
||||
test('prioritizeActive: uses lastRequestStarted for time sorting', () => {
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const recentlyActive = createSession({ id: 'recent-active', created: 1000, lastRequestStarted: 5000 });
|
||||
const recentlyCreated = createSession({ id: 'recent-created', created: 3000 });
|
||||
|
||||
const sorted = [recentlyCreated, recentlyActive].sort((a, b) => sorter.compare(a, b, true));
|
||||
assert.deepStrictEqual(sorted.map(s => s.label), ['Session recent-active', 'Session recent-created']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user