From c99f8109a67528cd9ccb6de2d513bd5a7d52aa1f Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Fri, 20 Mar 2026 16:51:41 +0000 Subject: [PATCH] Sessions: Enhance collapsed sidebar widget with session status indicators and panel toggle button (#303521) * Enhance collapsed sidebar widget with session status indicators and panel toggle button * Refactor sidebar toggle button and update CSS class for session status Co-authored-by: Copilot --------- Co-authored-by: mrleemurray Co-authored-by: Copilot --- .../sessions/browser/collapsedPartWidgets.ts | 126 ++++++++++-------- .../browser/media/collapsedPanelWidget.css | 59 +++++++- 2 files changed, 128 insertions(+), 57 deletions(-) diff --git a/src/vs/sessions/browser/collapsedPartWidgets.ts b/src/vs/sessions/browser/collapsedPartWidgets.ts index 27fce20f559..e5c1789651b 100644 --- a/src/vs/sessions/browser/collapsedPartWidgets.ts +++ b/src/vs/sessions/browser/collapsedPartWidgets.ts @@ -44,11 +44,16 @@ export class CollapsedSidebarWidget extends Disposable { super(); this.element = append(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')); - this.indicatorContainer = append(this.element, $('.collapsed-panel-buttons')); - // New session button + // Sidebar toggle button (leftmost) + this._register(this.createSidebarToggleButton()); + + // New session button (next to panel toggle) this._register(this.createNewSessionButton()); + // Session status indicators (rightmost) + this.indicatorContainer = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-status')); + // Listen for session changes this._register(this.agentSessionsService.model.onDidChangeSessions(() => this.rebuildIndicators())); @@ -72,6 +77,35 @@ export class CollapsedSidebarWidget extends Disposable { return store; } + private createSidebarToggleButton(): DisposableStore { + const store = new DisposableStore(); + const btn = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-panel-toggle')); + let iconElement: HTMLElement | undefined; + + const updateIcon = () => { + const sidebarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); + const icon = sidebarVisible ? Codicon.layoutSidebarLeft : Codicon.layoutSidebarLeftOff; + iconElement?.remove(); + iconElement = append(btn, $(ThemeIcon.asCSSSelector(icon))); + }; + + updateIcon(); + + store.add(this.hoverService.setupManagedHover(this.hoverDelegate, btn, localize('toggleSidebar', "Toggle Side Bar"))); + + store.add(addDisposableListener(btn, EventType.CLICK, () => { + this.commandService.executeCommand('workbench.action.agentToggleSidebarVisibility'); + })); + + store.add(this.layoutService.onDidChangePartVisibility(e => { + if (e.partId === Parts.SIDEBAR_PART) { + updateIcon(); + } + })); + + return store; + } + private rebuildIndicators(): void { this.indicatorDisposables.clear(); this.indicatorContainer.textContent = ''; @@ -79,75 +113,61 @@ export class CollapsedSidebarWidget extends Disposable { const sessions = this.agentSessionsService.model.sessions; const counts = this.countSessionsByStatus(sessions); - // In-progress indicator + const tooltipParts: string[] = []; + + // In-progress (matches agentSessionsViewer: sessionInProgress) if (counts.inProgress > 0) { - this.createIndicator( - Codicon.loading, - `${counts.inProgress}`, - localize('sessionsInProgress', "{0} session(s) in progress", counts.inProgress), - 'collapsed-sidebar-indicator-active' - ); + this.appendStatusSegment(Codicon.sessionInProgress, `${counts.inProgress}`, 'collapsed-sidebar-indicator-active'); + tooltipParts.push(localize('sessionsInProgress', "{0} session(s) in progress", counts.inProgress)); } - // Needs input indicator + // Needs input (matches agentSessionsViewer: circleFilled) if (counts.needsInput > 0) { - this.createIndicator( - Codicon.bell, - `${counts.needsInput}`, - localize('sessionsNeedInput', "{0} session(s) need input", counts.needsInput), - 'collapsed-sidebar-indicator-input' - ); + this.appendStatusSegment(Codicon.circleFilled, `${counts.needsInput}`, 'collapsed-sidebar-indicator-input'); + tooltipParts.push(localize('sessionsNeedInput', "{0} session(s) need input", counts.needsInput)); } - // Error indicator + // Failed (matches agentSessionsViewer: error) if (counts.failed > 0) { - this.createIndicator( - Codicon.error, - `${counts.failed}`, - localize('sessionsFailed', "{0} session(s) with errors", counts.failed), - 'collapsed-sidebar-indicator-error' - ); + this.appendStatusSegment(Codicon.error, `${counts.failed}`, 'collapsed-sidebar-indicator-error'); + tooltipParts.push(localize('sessionsFailed', "{0} session(s) with errors", counts.failed)); } - // Completed indicator - if (counts.completed > 0) { - this.createIndicator( - Codicon.check, - `${counts.completed}`, - localize('sessionsCompleted', "{0} session(s) completed", counts.completed), - 'collapsed-sidebar-indicator-done' - ); + // Unread (matches agentSessionsViewer: circleFilled with textLink-foreground) + if (counts.unread > 0) { + this.appendStatusSegment(Codicon.circleFilled, `${counts.unread}`, 'collapsed-sidebar-indicator-unread'); + tooltipParts.push(localize('sessionsUnread', "{0} unread session(s)", counts.unread)); } - // If no sessions at all, show a total count + // If no sessions at all if (sessions.length === 0) { - this.createIndicator( - Codicon.commentDiscussion, - '0', - localize('noSessions', "No sessions"), - 'collapsed-sidebar-indicator-empty' - ); + this.appendStatusSegment(Codicon.commentDiscussion, '0', 'collapsed-sidebar-indicator-empty'); + tooltipParts.push(localize('noSessions', "No sessions")); + } + + if (tooltipParts.length > 0) { + this.indicatorDisposables.add(this.hoverService.setupManagedHover( + this.hoverDelegate, this.indicatorContainer, tooltipParts.join('\n') + )); + + this.indicatorDisposables.add(addDisposableListener(this.indicatorContainer, EventType.CLICK, () => { + this.layoutService.setPartHidden(false, Parts.SIDEBAR_PART); + })); } } - private createIndicator(icon: ThemeIcon, count: string, tooltip: string, className: string): void { - const indicator = append(this.indicatorContainer, $(`.collapsed-panel-button.${className}`)); - append(indicator, $(ThemeIcon.asCSSSelector(icon))); - const label = append(indicator, $('span.collapsed-sidebar-count')); + private appendStatusSegment(icon: ThemeIcon, count: string, className: string): void { + const segment = append(this.indicatorContainer, $(`span.collapsed-sidebar-segment.${className}`)); + append(segment, $(ThemeIcon.asCSSSelector(icon))); + const label = append(segment, $('span.collapsed-sidebar-count')); label.textContent = count; - - this.indicatorDisposables.add(this.hoverService.setupManagedHover(this.hoverDelegate, indicator, tooltip)); - - this.indicatorDisposables.add(addDisposableListener(indicator, EventType.CLICK, () => { - this.layoutService.setPartHidden(false, Parts.SIDEBAR_PART); - })); } - private countSessionsByStatus(sessions: IAgentSession[]): { inProgress: number; needsInput: number; failed: number; completed: number } { + private countSessionsByStatus(sessions: IAgentSession[]): { inProgress: number; needsInput: number; failed: number; unread: number } { let inProgress = 0; let needsInput = 0; let failed = 0; - let completed = 0; + let unread = 0; for (const session of sessions) { if (session.isArchived()) { @@ -164,12 +184,14 @@ export class CollapsedSidebarWidget extends Disposable { failed++; break; case AgentSessionStatus.Completed: - completed++; + if (!session.isRead()) { + unread++; + } break; } } - return { inProgress, needsInput, failed, completed }; + return { inProgress, needsInput, failed, unread }; } show(): void { diff --git a/src/vs/sessions/browser/media/collapsedPanelWidget.css b/src/vs/sessions/browser/media/collapsedPanelWidget.css index 40e46bd9cef..b7d2d4e78c4 100644 --- a/src/vs/sessions/browser/media/collapsedPanelWidget.css +++ b/src/vs/sessions/browser/media/collapsedPanelWidget.css @@ -74,6 +74,22 @@ color: inherit; } +/* ---- Consolidated session status button ---- */ + +.agent-sessions-workbench .collapsed-panel-button.collapsed-sidebar-status { + gap: 6px; +} + +.agent-sessions-workbench .collapsed-sidebar-segment { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.agent-sessions-workbench .collapsed-panel-button .collapsed-sidebar-segment .codicon { + font-size: 14px; +} + /* ---- Sidebar indicators ---- */ .agent-sessions-workbench .collapsed-sidebar-count, @@ -84,16 +100,49 @@ color: inherit; } -.agent-sessions-workbench .collapsed-sidebar-indicator-active .codicon { - animation: codicon-spin 1.5s infinite linear; +.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-active .codicon { + color: var(--vscode-textLink-foreground); } -.agent-sessions-workbench .collapsed-sidebar-indicator-error { +.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-error { color: var(--vscode-errorForeground); } -.agent-sessions-workbench .collapsed-sidebar-indicator-input { - color: var(--vscode-notificationsInfoIcon-foreground); +.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-input { + color: var(--vscode-list-warningForeground); +} + +.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-input .codicon { + animation: collapsed-sidebar-needs-input-pulse 2s ease-in-out infinite; +} + +@keyframes collapsed-sidebar-needs-input-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} + +@media (prefers-reduced-motion: reduce) { + .agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-input .codicon { + animation: none; + } +} + +.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-unread { + color: var(--vscode-textLink-foreground); +} + +/* ---- Panel toggle button ---- */ + +.agent-sessions-workbench .collapsed-sidebar-panel-toggle { + opacity: 0.7; +} + +.agent-sessions-workbench .collapsed-sidebar-panel-toggle:hover { + opacity: 1; } /* ---- Auxiliary bar indicators ---- */