mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
agent sessions - redesign UI (#275921)
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user