diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts index 8d1c110e6da..867944a4610 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts @@ -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(); + 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: { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index 0d54b5c2fa1..deb1d3b7507 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -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', +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts index 0b34a7017b8..c968556d490 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts @@ -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] ])); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 6f4a3c9ac8c..59f4e80121b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -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, 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 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, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 3d3427b7473..599fa2c2e2e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -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; diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts index 807dc36b2ef..e077e6eec1a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts @@ -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',