Polish agent sessions list UI (#293523)

* Polish agent sessions list UI

- Show description alongside diff badge with dot separator
- Use regular foreground for active selection, descriptionForeground for inactive
- Remove background/outlines from diff badge
- Bump read indicator opacity to 20%

* Fix sessionDateFromNow test expectations

* Add white-space: nowrap to title/description for clean truncation

* Move compact time formatting to shared date utils

Add useCompactUnits option to fromNow() and getDurationString() for
single-letter compact units (5m, 2h, 3d) and remove custom functions
from agentSessionsViewer.

* Hide description when diff badge is shown

* Revert date formatting to pre-PR style (e.g. '4 hrs ago')

* Drop 'ago' suffix from session date labels

* Revert date.ts to upstream (remove unused useCompactUnits)

* Remove trailing period from completion status labels
This commit is contained in:
David Dossett
2026-02-06 18:46:21 -08:00
committed by GitHub
parent d20d8cbc71
commit 0ed85cc5ac
7 changed files with 117 additions and 58 deletions

View File

@@ -5,7 +5,7 @@
"type": "dark",
"colors": {
"foreground": "#bfbfbf",
"disabledForeground": "#444444",
"disabledForeground": "#666666",
"errorForeground": "#f48771",
"descriptionForeground": "#999999",
"icon.foreground": "#888888",

View File

@@ -134,7 +134,7 @@ export interface IAgentSessionsControl {
export const agentSessionReadIndicatorForeground = registerColor(
'agentSessionReadIndicator.foreground',
{ dark: transparent(foreground, 0.15), light: transparent(foreground, 0.15), hcDark: null, hcLight: null },
{ dark: transparent(foreground, 0.2), light: transparent(foreground, 0.2), hcDark: null, hcLight: null },
localize('agentSessionReadIndicatorForeground', "Foreground color for the read indicator in an agent session.")
);

View File

@@ -8,7 +8,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js';
import { $, append, EventHelper } from '../../../../../base/browser/dom.js';
import { $, addDisposableListener, append, EventHelper } from '../../../../../base/browser/dom.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 { FuzzyScore } from '../../../../../base/common/filters.js';
@@ -176,6 +176,14 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
ChatContextKeys.agentSessionsViewerFocused.bindTo(list.contextKeyService);
// Track mouse vs keyboard focus to suppress focus outlines on mouse clicks
this._register(addDisposableListener(this.sessionsContainer!, 'mousedown', () => {
this.sessionsContainer!.classList.add('mouse-focused');
}));
this._register(addDisposableListener(this.sessionsContainer!, 'keydown', () => {
this.sessionsContainer!.classList.remove('mouse-focused');
}));
const model = this.agentSessionsService.model;
this._register(this.options.filter.onDidChange(async () => {

View File

@@ -56,6 +56,9 @@ interface IAgentSessionItemTemplate {
// Column 2 Row 1
readonly title: IconLabel;
readonly statusContainer: HTMLElement;
readonly statusProviderIcon: HTMLElement;
readonly statusTime: HTMLElement;
readonly titleToolbar: MenuWorkbenchToolBar;
// Column 2 Row 2
@@ -64,12 +67,9 @@ interface IAgentSessionItemTemplate {
readonly diffRemovedSpan: HTMLSpanElement;
readonly badge: HTMLElement;
readonly separator: HTMLElement;
readonly description: HTMLElement;
readonly statusContainer: HTMLElement;
readonly statusProviderIcon: HTMLElement;
readonly statusTime: HTMLElement;
readonly contextKeyService: IContextKeyService;
readonly elementDisposable: DisposableStore;
readonly disposables: IDisposable;
@@ -111,6 +111,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
h('div.agent-session-main-col', [
h('div.agent-session-title-row', [
h('div.agent-session-title@title'),
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-title-toolbar@titleToolbar'),
]),
h('div.agent-session-details-row', [
@@ -120,11 +124,8 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
h('span.agent-session-diff-removed@removedSpan')
]),
h('div.agent-session-badge@badge'),
h('span.agent-session-separator@separator'),
h('div.agent-session-description@description'),
h('div.agent-session-status@statusContainer', [
h('span.agent-session-status-provider-icon@statusProviderIcon'),
h('span.agent-session-status-time@statusTime')
])
])
])
]
@@ -147,6 +148,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
diffAddedSpan: elements.addedSpan,
diffRemovedSpan: elements.removedSpan,
badge: elements.badge,
separator: elements.separator,
description: elements.description,
statusContainer: elements.statusContainer,
statusProviderIcon: elements.statusProviderIcon,
@@ -216,6 +218,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
this.renderDescription(session, template, hasBadge);
}
// Separator (dot between badge and description)
template.separator.classList.toggle('has-separator', hasBadge && !hasDiff);
// Status
this.renderStatus(session, template);
@@ -307,8 +312,8 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre
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);
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") :
@@ -497,7 +502,7 @@ export class AgentSessionSectionRenderer implements ICompressibleTreeRenderer<IA
export class AgentSessionsListDelegate implements IListVirtualDelegate<AgentSessionListItem> {
static readonly ITEM_HEIGHT = 52;
static readonly ITEM_HEIGHT = 40;
static readonly SECTION_HEIGHT = 26;
getHeight(element: AgentSessionListItem): number {
@@ -708,10 +713,10 @@ export class AgentSessionsDataSource implements IAsyncDataSource<IAgentSessionsM
}
export const AgentSessionSectionLabels = {
[AgentSessionSection.InProgress]: localize('agentSessions.inProgressSection', "In Progress"),
[AgentSessionSection.InProgress]: localize('agentSessions.inProgressSection', "In progress"),
[AgentSessionSection.Today]: localize('agentSessions.todaySection', "Today"),
[AgentSessionSection.Yesterday]: localize('agentSessions.yesterdaySection', "Yesterday"),
[AgentSessionSection.Week]: localize('agentSessions.weekSection', "Last 7 Days"),
[AgentSessionSection.Week]: localize('agentSessions.weekSection', "Last 7 days"),
[AgentSessionSection.Older]: localize('agentSessions.olderSection', "Older"),
[AgentSessionSection.Archived]: localize('agentSessions.archivedSection', "Archived"),
[AgentSessionSection.More]: localize('agentSessions.moreSection', "More"),
@@ -775,14 +780,14 @@ export function sessionDateFromNow(sessionTime: number): string {
// normalization logic.
if (sessionTime < startOfToday && sessionTime >= startOfYesterday) {
return localize('date.fromNow.days.singular.ago', '1 day ago');
return localize('date.fromNow.days.singular', '1 day');
}
if (sessionTime < startOfYesterday && sessionTime >= startOfTwoDaysAgo) {
return localize('date.fromNow.days.multiple.ago', '2 days ago');
return localize('date.fromNow.days.multiple', '2 days');
}
return fromNow(sessionTime, true);
return fromNow(sessionTime, false);
}
export class AgentSessionsIdentityProvider implements IIdentityProvider<IAgentSessionsModel | AgentSessionListItem> {

View File

@@ -9,6 +9,20 @@
height: 100%;
min-height: 0;
.monaco-scrollable-element {
padding: 0 6px;
}
.monaco-list-row {
border-radius: 6px;
}
/* Hide focus outlines on mouse click, preserve for keyboard navigation */
&.mouse-focused .monaco-list-row.focused,
&.mouse-focused .monaco-list-row.focused.selected {
outline: none !important;
}
.monaco-list-row .force-no-twistie {
display: none !important;
}
@@ -21,55 +35,60 @@
color: unset;
}
}
}
.monaco-list:focus .monaco-list-row.selected .agent-session-details-row {
.agent-session-diff-container {
background-color: unset;
outline: 1px solid var(--vscode-agentSessionSelectedBadge-border);
.agent-session-diff-added,
.agent-session-diff-removed {
color: unset;
color: unset;
}
}
.agent-session-badge {
background-color: unset;
outline: 1px solid var(--vscode-agentSessionSelectedBadge-border);
}
}
.monaco-list:not(:focus) .monaco-list-row.selected .agent-session-details-row .agent-session-diff-container,
.monaco-list:not(:focus) .monaco-list-row.selected .agent-session-details-row .agent-session-badge {
outline: 1px solid var(--vscode-agentSessionSelectedUnfocusedBadge-border);
.monaco-list-row.selected .agent-session-title {
color: unset;
}
.monaco-list-row.selected .agent-session-status {
color: unset;
}
.monaco-list:not(:focus) .monaco-list-row.selected .agent-session-details-row {
color: var(--vscode-descriptionForeground);
}
.monaco-list-row .agent-session-title-toolbar {
/* for the absolute positioning of the toolbar below */
position: relative;
height: 16px;
display: none;
.monaco-toolbar {
/* this is required because the overal height (including the padding needed for hover feedback) would push down the title otherwise */
position: relative;
right: 0;
top: 0;
display: none;
}
}
.monaco-list-row:hover .agent-session-title-toolbar,
.monaco-list-row.focused .agent-session-title-toolbar {
/* On hover or keyboard focus: show toolbar, hide status */
.monaco-list-row:hover,
&:not(.mouse-focused) .monaco-list-row.focused {
.monaco-toolbar {
.agent-session-title-toolbar {
display: block;
}
.agent-session-status {
display: none;
}
}
.agent-session-item {
display: flex;
flex-direction: row;
/* to offset from possible scrollbar */
padding: 8px 12px 8px 8px;
padding: 4px 6px;
&.archived {
color: var(--vscode-descriptionForeground);
@@ -85,10 +104,16 @@
.agent-session-icon-col {
display: flex;
align-items: flex-start;
line-height: 16px;
.agent-session-icon {
flex-shrink: 0;
font-size: 16px;
font-size: 12px;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
&.codicon.codicon-session-in-progress {
color: var(--vscode-textLink-foreground);
@@ -113,24 +138,26 @@
}
.agent-session-main-col {
padding-left: 8px;
padding-left: 6px;
}
.agent-session-title-row,
.agent-session-details-row {
display: flex;
align-items: center;
line-height: 16px;
}
.agent-session-title-row {
padding-bottom: 4px;
line-height: 16px;
padding-bottom: 2px;
}
.agent-session-details-row {
gap: 4px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-size: 11px;
line-height: 14px;
max-height: 14px;
color: var(--vscode-disabledForeground);
.rendered-markdown {
p {
@@ -150,11 +177,8 @@
.agent-session-diff-container,
.agent-session-badge {
background-color: var(--vscode-toolbar-hoverBackground);
font-weight: 500;
padding: 0 4px;
font-variant-numeric: tabular-nums;
border-radius: 5px;
overflow: hidden;
}
@@ -175,6 +199,18 @@
}
}
.agent-session-separator {
display: none;
&.has-separator {
display: inline;
&::before {
content: '\00B7';
}
}
}
.agent-session-badge {
p {
@@ -186,7 +222,7 @@
}
.codicon {
font-size: 12px;
font-size: 11px;
}
}
}
@@ -195,17 +231,28 @@
.agent-session-description {
/* push other items to the end */
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-right: 16px;
mask-image: linear-gradient(to right, black calc(100% - 32px), transparent);
-webkit-mask-image: linear-gradient(to right, black calc(100% - 32px), transparent);
}
.agent-session-title {
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.agent-session-status {
display: flex;
align-items: center;
font-variant-numeric: tabular-nums;
font-size: 11px;
color: var(--vscode-disabledForeground);
white-space: nowrap;
.agent-session-status-provider-icon {
font-size: 12px;
font-size: 11px;
margin-right: 4px;
&.hidden {
@@ -219,11 +266,10 @@
display: flex;
align-items: center;
font-size: 11px;
font-weight: 500;
color: var(--vscode-descriptionForeground);
text-transform: uppercase;
letter-spacing: 0.5px;
/* align with session item padding */
padding: 0 12px 0 8px;
padding: 0 6px;
.agent-session-section-label {
flex: 1;

View File

@@ -100,7 +100,7 @@
}
.agent-sessions-new-button-container {
padding: 8px 12px;
padding: 8px;
}
}
@@ -127,7 +127,7 @@
}
.agent-session-section {
padding: 0 12px 0 20px;
padding: 0 12px;
}
/* Right position: symmetric padding */

View File

@@ -54,21 +54,21 @@ suite('sessionDateFromNow', () => {
const ONE_DAY = 24 * 60 * 60 * 1000;
test('returns "1 day ago" for yesterday', () => {
test('returns "1 day" for yesterday', () => {
const now = Date.now();
const startOfToday = new Date(now).setHours(0, 0, 0, 0);
// Time in the middle of yesterday
const yesterday = startOfToday - ONE_DAY / 2;
assert.strictEqual(sessionDateFromNow(yesterday), '1 day ago');
assert.strictEqual(sessionDateFromNow(yesterday), '1 day');
});
test('returns "2 days ago" for two days ago', () => {
test('returns "2 days" for two days ago', () => {
const now = Date.now();
const startOfToday = new Date(now).setHours(0, 0, 0, 0);
const startOfYesterday = startOfToday - ONE_DAY;
// Time in the middle of two days ago
const twoDaysAgo = startOfYesterday - ONE_DAY / 2;
assert.strictEqual(sessionDateFromNow(twoDaysAgo), '2 days ago');
assert.strictEqual(sessionDateFromNow(twoDaysAgo), '2 days');
});
test('returns fromNow result for today', () => {