mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
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:
@@ -5,7 +5,7 @@
|
||||
"type": "dark",
|
||||
"colors": {
|
||||
"foreground": "#bfbfbf",
|
||||
"disabledForeground": "#444444",
|
||||
"disabledForeground": "#666666",
|
||||
"errorForeground": "#f48771",
|
||||
"descriptionForeground": "#999999",
|
||||
"icon.foreground": "#888888",
|
||||
|
||||
@@ -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.")
|
||||
);
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user