agent sessions - redesign UI (#275921)

This commit is contained in:
Benjamin Pasero
2025-11-06 21:00:11 +01:00
committed by GitHub
parent 548e73f30b
commit 5ba1636c1d
6 changed files with 132 additions and 50 deletions
@@ -5,6 +5,7 @@
import { ThrottledDelayer } from '../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
@@ -12,7 +13,8 @@ import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { localize } from '../../../../../nls.js';
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
import { ChatSessionStatus, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
import { ChatSessionStatus, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
import { AgentSessionProviders } from './agentSessions.js';
//#region Interfaces, Types
@@ -31,6 +33,7 @@ export interface IAgentSessionsViewModel {
export interface IAgentSessionViewModel {
readonly provider: IChatSessionItemProvider;
readonly providerLabel: string;
readonly resource: URI;
@@ -39,7 +42,7 @@ export interface IAgentSessionViewModel {
readonly label: string;
readonly description: string | IMarkdownString;
readonly icon?: ThemeIcon;
readonly icon: ThemeIcon;
readonly timing: {
readonly startTime: number;
@@ -134,6 +137,11 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
const providersToResolve = Array.from(this.providersToResolve);
this.providersToResolve.clear();
const mapSessionContributionToType = new Map<string, IChatSessionsExtensionPoint>();
for (const contribution of this.chatSessionsService.getAllChatSessionContributions()) {
mapSessionContributionToType.set(contribution.type, contribution);
}
const newSessions: IAgentSessionViewModel[] = [];
for (const provider of this.chatSessionsService.getAllChatSessionItemProviders()) {
if (!providersToResolve.includes(undefined) && !providersToResolve.includes(provider.chatSessionType)) {
@@ -157,23 +165,45 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
} else {
switch (session.status) {
case ChatSessionStatus.InProgress:
description = localize('chat.session.status.inProgress', 'Working...');
description = localize('chat.session.status.inProgress', "Working...");
break;
case ChatSessionStatus.Failed:
description = localize('chat.session.status.error', 'Failed');
description = localize('chat.session.status.error', "Failed");
break;
default:
description = localize('chat.session.status.completed', 'Finished');
description = localize('chat.session.status.completed', "Finished");
break;
}
}
let icon: ThemeIcon;
let providerLabel: string;
switch ((provider.chatSessionType)) {
case localChatSessionType:
providerLabel = localize('chat.session.providerLabel.local', "Local");
icon = Codicon.window;
break;
case AgentSessionProviders.Background:
providerLabel = localize('chat.session.providerLabel.background', "Background");
icon = Codicon.layers;
break;
case AgentSessionProviders.Cloud:
providerLabel = localize('chat.session.providerLabel.cloud', "Cloud");
icon = Codicon.cloud;
break;
default: {
providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType;
icon = session.iconPath ?? Codicon.terminal;
}
}
newSessions.push({
provider,
providerLabel,
resource: session.resource,
label: session.label,
description,
icon: session.iconPath,
icon,
tooltip: session.tooltip,
status: session.status,
timing: {
@@ -3,5 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localChatSessionType } from '../../common/chatSessionsService.js';
export const AGENT_SESSIONS_VIEW_CONTAINER_ID = 'workbench.viewContainer.agentSessions';
export const AGENT_SESSIONS_VIEW_ID = 'workbench.view.agentSessions';
export enum AgentSessionProviders {
Local = localChatSessionType,
Background = 'copilotcli',
Cloud = 'copilot-cloud-agent',
}
@@ -49,7 +49,7 @@ import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
import { getActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { IChatService } from '../../common/chatService.js';
import { IChatWidgetService } from '../chat.js';
import { AGENT_SESSIONS_VIEW_ID, AGENT_SESSIONS_VIEW_CONTAINER_ID } from './agentSessions.js';
import { AGENT_SESSIONS_VIEW_ID, AGENT_SESSIONS_VIEW_CONTAINER_ID, AgentSessionProviders } from './agentSessions.js';
import { TreeFindMode } from '../../../../../base/browser/ui/tree/abstractTree.js';
export class AgentSessionsView extends ViewPane {
@@ -263,12 +263,31 @@ export class AgentSessionsView extends ViewPane {
// Default action
actions.push(toAction({
id: 'newChatSession.default',
label: localize('newChatSessionDefault', "New Agent Session"),
label: localize('newChatSessionDefault', "New Local Agent Session"),
run: () => this.commandService.executeCommand(ACTION_ID_OPEN_CHAT)
}));
// Background (CLI)
actions.push(toAction({
id: 'newChatSessionFromProvider.background',
label: localize('newBackgroundSession', "New Background Agent Session"),
run: () => this.commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${AgentSessionProviders.Background}`)
}));
// Cloud
actions.push(toAction({
id: 'newChatSessionFromProvider.cloud',
label: localize('newCloudSession', "New Cloud Agent Session"),
run: () => this.commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${AgentSessionProviders.Cloud}`)
}));
actions.push(new Separator());
for (const provider of this.chatSessionsService.getAllChatSessionContributions()) {
if (provider.type === AgentSessionProviders.Background || provider.type === AgentSessionProviders.Cloud) {
continue; // already added above
}
const menuActions = this.menuService.getMenuActions(MenuId.ChatSessionsCreateSubMenu, this.scopedContextKeyService.createOverlay([
[ChatContextKeys.sessionType.key, provider.type]
]));
@@ -34,19 +34,22 @@ import { IWorkbenchLayoutService, Position } from '../../../../services/layout/b
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { AGENT_SESSIONS_VIEW_ID } from './agentSessions.js';
import { IntervalTimer } from '../../../../../base/common/async.js';
interface IAgentSessionItemTemplate {
readonly element: HTMLElement;
// Row 1
readonly title: IconLabel;
// Column 1
readonly icon: HTMLElement;
readonly timestamp: HTMLElement;
// Row 2
// Column 2 Row 1
readonly title: IconLabel;
// Column 2 Row 2
readonly description: HTMLElement;
readonly diffAdded: HTMLElement;
readonly diffRemoved: HTMLElement;
readonly status: HTMLElement;
readonly elementDisposable: DisposableStore;
readonly disposables: IDisposable;
@@ -77,11 +80,12 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
const elements = h(
'div.agent-session-item@item',
[
h('div.agent-session-icon-col', [
h('div.agent-session-icon@icon')
]),
h('div.agent-session-main-col', [
h('div.agent-session-title-row', [
h('div.agent-session-title@titleContainer'),
h('div.agent-session-icon@icon'),
h('div.agent-session-timestamp@timestamp')
h('div.agent-session-title@title'),
]),
h('div.agent-session-details-row', [
h('div.agent-session-description@description'),
@@ -89,6 +93,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
h('span.agent-session-diff-added@diffAdded'),
h('span.agent-session-diff-removed@diffRemoved')
]),
h('div.agent-session-status@status')
])
])
]
@@ -99,11 +104,11 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
return {
element: elements.item,
icon: elements.icon,
title: disposables.add(new IconLabel(elements.titleContainer, { supportHighlights: true, supportIcons: true })),
title: disposables.add(new IconLabel(elements.title, { supportHighlights: true, supportIcons: true })),
description: elements.description,
timestamp: elements.timestamp,
diffAdded: elements.diffAdded,
diffRemoved: elements.diffRemoved,
status: elements.status,
elementDisposable,
disposables
};
@@ -112,17 +117,18 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
renderElement(session: ITreeNode<IAgentSessionViewModel, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
template.elementDisposable.clear();
const icon = this.statusToIcon(session.element.status) ?? session.element.icon;
if (icon) {
template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(icon)}`;
}
// Icon
template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(this.getIcon(session.element))}`;
// Title
template.title.setLabel(session.element.label, undefined, { matches: createMatches(session.filterData) });
// Diff
const { statistics: diff } = session.element;
template.diffAdded.textContent = diff ? `+${diff.insertions}` : '';
template.diffRemoved.textContent = diff ? `-${diff.deletions}` : '';
// Description
if (typeof session.element.description === 'string') {
template.description.textContent = session.element.description;
} else {
@@ -144,7 +150,10 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
template.elementDisposable.add(addDisposableListener(template.description, EventType.AUXCLICK, e => e.stopPropagation()));
}
template.timestamp.textContent = fromNow(session.element.timing.startTime);
// Status (updated every minute)
template.status.textContent = this.getStatus(session.element);
const timer = template.elementDisposable.add(new IntervalTimer());
timer.cancelAndSet(() => template.status.textContent = this.getStatus(session.element), 60 * 1000);
this.renderHover(session, template);
}
@@ -175,17 +184,20 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
}
}
private statusToIcon(status?: ChatSessionStatus): ThemeIcon | undefined {
switch (status) {
case ChatSessionStatus.InProgress:
return ThemeIcon.modify(Codicon.loading, 'spin');
case ChatSessionStatus.Completed:
return Codicon.pass;
case ChatSessionStatus.Failed:
return Codicon.error;
private getIcon(session: IAgentSessionViewModel): ThemeIcon {
if (session.status === ChatSessionStatus.InProgress) {
return ThemeIcon.modify(Codicon.loading, 'spin');
}
return undefined;
if (session.status === ChatSessionStatus.Failed) {
return Codicon.error;
}
return session.icon;
}
private getStatus(session: IAgentSessionViewModel): string {
return `${session.providerLabel}${fromNow(session.timing.startTime)}`;
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSessionViewModel>, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
@@ -18,7 +18,8 @@
.agent-session-item {
display: flex;
flex-direction: row;
padding: 0 6px;
padding: 0 12px;
gap: 2px;
.agent-session-main-col,
.agent-session-title-row,
@@ -27,16 +28,34 @@
min-width: 0;
}
.agent-session-icon-col {
display: flex;
align-items: flex-start;
padding-top: 4px;
.agent-session-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
font-size: 16px;
}
}
.agent-session-title-row,
.agent-session-details-row {
display: flex;
align-items: center;
line-height: 22px;
line-height: 20px; /* ends up as 22px with the padding below */
gap: 6px;
padding: 0 6px;
}
.agent-session-title-row {
padding: 2px 6px 0 6px;
}
.agent-session-details-row {
padding: 0 6px 2px 6px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
.rendered-markdown {
@@ -50,31 +69,16 @@
}
}
.agent-session-title {
flex: 1;
}
.agent-session-title,
.agent-session-description {
text-overflow: ellipsis;
overflow: hidden;
}
.agent-session-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
font-size: 16px;
}
.agent-session-timestamp {
font-size: 11px;
}
/* #region Diff Styling */
.agent-session-diff {
font-size: 11px;
flex: 1; /* push status to the end */
font-weight: 700;
display: flex;
gap: 4px;
@@ -16,6 +16,7 @@ import { LocalChatSessionUri } from '../../common/chatUri.js';
import { MockChatSessionsService } from '../common/mockChatSessionsService.js';
import { TestLifecycleService } from '../../../../test/browser/workbenchTestServices.js';
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
import { Codicon } from '../../../../../base/common/codicons.js';
suite('AgentSessionsViewModel', () => {
@@ -860,6 +861,8 @@ suite('AgentSessionsViewModel - Helper Functions', () => {
onDidChangeChatSessionItems: Event.None,
provideChatSessionItems: async () => []
},
providerLabel: 'Local',
icon: Codicon.chatSparkle,
resource: URI.parse('test://local-1'),
label: 'Local',
description: 'test',
@@ -872,6 +875,8 @@ suite('AgentSessionsViewModel - Helper Functions', () => {
onDidChangeChatSessionItems: Event.None,
provideChatSessionItems: async () => []
},
providerLabel: 'Local',
icon: Codicon.chatSparkle,
resource: URI.parse('test://remote-1'),
label: 'Remote',
description: 'test',
@@ -889,6 +894,8 @@ suite('AgentSessionsViewModel - Helper Functions', () => {
onDidChangeChatSessionItems: Event.None,
provideChatSessionItems: async () => []
},
providerLabel: 'Local',
icon: Codicon.chatSparkle,
resource: URI.parse('test://test-1'),
label: 'Test',
description: 'test',
@@ -910,6 +917,8 @@ suite('AgentSessionsViewModel - Helper Functions', () => {
onDidChangeChatSessionItems: Event.None,
provideChatSessionItems: async () => []
},
providerLabel: 'Local',
icon: Codicon.chatSparkle,
resource: URI.parse('test://test-1'),
label: 'Test',
description: 'test',