From 148436bef67eb22e9e421c934eeacd6faedf019e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 23 Mar 2026 12:50:05 +0000 Subject: [PATCH 1/5] refactor: rename CollapsedSidebarWidget to SessionStatusWidget and update its functionality in the titlebar Co-authored-by: Copilot --- .../sessions/browser/collapsedPartWidgets.ts | 162 ++++-------------- .../browser/media/collapsedPanelWidget.css | 68 ++------ src/vs/sessions/browser/parts/sidebarPart.ts | 7 + src/vs/sessions/browser/workbench.ts | 18 +- 4 files changed, 66 insertions(+), 189 deletions(-) diff --git a/src/vs/sessions/browser/collapsedPartWidgets.ts b/src/vs/sessions/browser/collapsedPartWidgets.ts index e5c1789651b..b7250dbf416 100644 --- a/src/vs/sessions/browser/collapsedPartWidgets.ts +++ b/src/vs/sessions/browser/collapsedPartWidgets.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/collapsedPanelWidget.css'; -import { $, addDisposableListener, append, EventType } from '../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType, prepend } from '../../base/browser/dom.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { IWorkbenchLayoutService, Parts } from '../../workbench/services/layout/browser/layoutService.js'; import { IHoverService } from '../../platform/hover/browser/hover.js'; @@ -14,7 +14,7 @@ import { ThemeIcon } from '../../base/common/themables.js'; import { Codicon } from '../../base/common/codicons.js'; import { IAgentSessionsService } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionStatus, getAgentChangesSummary, IAgentSession } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; -import { ICommandService } from '../../platform/commands/common/commands.js'; + import { IPaneCompositePartService } from '../../workbench/services/panecomposite/browser/panecomposite.js'; import { ViewContainerLocation } from '../../workbench/common/views.js'; import { URI } from '../../base/common/uri.js'; @@ -24,35 +24,38 @@ import { Event } from '../../base/common/event.js'; const CHANGES_VIEW_CONTAINER_ID = 'workbench.view.agentSessions.changesContainer'; /** - * Collapsed widget shown in the bottom-left corner when the sidebar is hidden. - * Shows session status counts (active, errors, completed) and a new session button. + * Session status widget displaying a tasklist icon with the number of unread sessions. + * Can be placed in either the sidebar title area or the titlebar. + * When `prepend` option is true, the element is prepended to the parent instead of appended. */ -export class CollapsedSidebarWidget extends Disposable { +export class SessionStatusWidget extends Disposable { private readonly element: HTMLElement; - private readonly indicatorContainer: HTMLElement; + private readonly statusBtn: HTMLElement; private readonly indicatorDisposables = this._register(new DisposableStore()); private readonly hoverDelegate = this._register(createInstantHoverDelegate()); constructor( parent: HTMLElement, + options: { prependToParent?: boolean } | undefined, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IHoverService private readonly hoverService: IHoverService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, - @ICommandService private readonly commandService: ICommandService, ) { super(); - this.element = append(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')); + this.element = options?.prependToParent + ? prepend(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')) + : append(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')); - // Sidebar toggle button (leftmost) - this._register(this.createSidebarToggleButton()); + // Status button: tasklist icon + unread badge + this.statusBtn = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-status')); - // 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')); + // Click toggles sidebar visibility + this._register(addDisposableListener(this.statusBtn, EventType.CLICK, () => { + const isVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); + this.layoutService.setPartHidden(isVisible, Parts.SIDEBAR_PART); + })); // Listen for session changes this._register(this.agentSessionsService.model.onDidChangeSessions(() => this.rebuildIndicators())); @@ -63,135 +66,44 @@ export class CollapsedSidebarWidget extends Disposable { this.hide(); } - private createNewSessionButton(): DisposableStore { - const store = new DisposableStore(); - const btn = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-new-session')); - append(btn, $(ThemeIcon.asCSSSelector(Codicon.newSession))); - - store.add(this.hoverService.setupManagedHover(this.hoverDelegate, btn, localize('newSession', "New Session"))); - - store.add(addDisposableListener(btn, EventType.CLICK, () => { - this.commandService.executeCommand('workbench.action.sessions.newChat'); - })); - - 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 = ''; + this.statusBtn.textContent = ''; const sessions = this.agentSessionsService.model.sessions; - const counts = this.countSessionsByStatus(sessions); + const unreadCount = this.countUnreadSessions(sessions); - const tooltipParts: string[] = []; + // Always show the tasklist icon + append(this.statusBtn, $(ThemeIcon.asCSSSelector(Codicon.tasklist))); - // In-progress (matches agentSessionsViewer: sessionInProgress) - if (counts.inProgress > 0) { - this.appendStatusSegment(Codicon.sessionInProgress, `${counts.inProgress}`, 'collapsed-sidebar-indicator-active'); - tooltipParts.push(localize('sessionsInProgress', "{0} session(s) in progress", counts.inProgress)); - } + // Show unread count badge only when there are unread sessions + if (unreadCount > 0) { + const badge = append(this.statusBtn, $('span.collapsed-sidebar-count')); + badge.textContent = `${unreadCount}`; - // Needs input (matches agentSessionsViewer: circleFilled) - if (counts.needsInput > 0) { - this.appendStatusSegment(Codicon.circleFilled, `${counts.needsInput}`, 'collapsed-sidebar-indicator-input'); - tooltipParts.push(localize('sessionsNeedInput', "{0} session(s) need input", counts.needsInput)); - } - - // Failed (matches agentSessionsViewer: error) - if (counts.failed > 0) { - this.appendStatusSegment(Codicon.error, `${counts.failed}`, 'collapsed-sidebar-indicator-error'); - tooltipParts.push(localize('sessionsFailed', "{0} session(s) with errors", counts.failed)); - } - - // 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 - if (sessions.length === 0) { - 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.hoverDelegate, this.statusBtn, + localize('sessionsUnread', "{0} unread session(s)", unreadCount) + )); + } else { + this.indicatorDisposables.add(this.hoverService.setupManagedHover( + this.hoverDelegate, this.statusBtn, + localize('toggleSidebar', "Toggle Side Bar") )); - - this.indicatorDisposables.add(addDisposableListener(this.indicatorContainer, EventType.CLICK, () => { - this.layoutService.setPartHidden(false, Parts.SIDEBAR_PART); - })); } } - 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; - } - - private countSessionsByStatus(sessions: IAgentSession[]): { inProgress: number; needsInput: number; failed: number; unread: number } { - let inProgress = 0; - let needsInput = 0; - let failed = 0; + private countUnreadSessions(sessions: IAgentSession[]): number { let unread = 0; - for (const session of sessions) { if (session.isArchived()) { continue; } - switch (session.status) { - case AgentSessionStatus.InProgress: - inProgress++; - break; - case AgentSessionStatus.NeedsInput: - needsInput++; - break; - case AgentSessionStatus.Failed: - failed++; - break; - case AgentSessionStatus.Completed: - if (!session.isRead()) { - unread++; - } - break; + if (session.status === AgentSessionStatus.Completed && !session.isRead()) { + unread++; } } - - return { inProgress, needsInput, failed, unread }; + return unread; } show(): void { diff --git a/src/vs/sessions/browser/media/collapsedPanelWidget.css b/src/vs/sessions/browser/media/collapsedPanelWidget.css index b7d2d4e78c4..038cb04de5a 100644 --- a/src/vs/sessions/browser/media/collapsedPanelWidget.css +++ b/src/vs/sessions/browser/media/collapsedPanelWidget.css @@ -21,13 +21,19 @@ display: none; } -/* ---- Sidebar widget (in titlebar-left) ---- */ + +/* ---- Sidebar widget (in titlebar-left, always visible, far left) ---- */ .agent-sessions-workbench .collapsed-sidebar-widget { - order: 10; + order: 0; padding-left: 8px; } +/* When inside the sidebar title area (panel open), no left padding */ +.agent-sessions-workbench .part.sidebar .collapsed-sidebar-widget { + padding-left: 0; +} + /* ---- Auxiliary Bar widget (in titlebar-right) ---- */ .agent-sessions-workbench .collapsed-auxbar-widget { @@ -74,23 +80,13 @@ color: inherit; } -/* ---- Consolidated session status button ---- */ +/* ---- Session status button (tasklist icon + unread badge) ---- */ .agent-sessions-workbench .collapsed-panel-button.collapsed-sidebar-status { - gap: 6px; + gap: 4px; } -.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 ---- */ +/* ---- Shared count styling ---- */ .agent-sessions-workbench .collapsed-sidebar-count, .agent-sessions-workbench .collapsed-auxbar-count { @@ -100,50 +96,10 @@ color: inherit; } -.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-active .codicon { +.agent-sessions-workbench .collapsed-sidebar-count { color: var(--vscode-textLink-foreground); } -.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-error { - color: var(--vscode-errorForeground); -} - -.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 ---- */ diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index 75eb74869dd..a36b81bf7a6 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -40,6 +40,7 @@ import { isFullscreen, onDidChangeFullscreen } from '../../../base/browser/brows import { mainWindow } from '../../../base/browser/window.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; import { hasNativeTitlebar, getTitleBarStyle } from '../../../platform/window/common/window.js'; +import { SessionStatusWidget } from '../collapsedPartWidgets.js'; /** * Sidebar part specifically for agent sessions workbench. @@ -151,6 +152,12 @@ export class SidebarPart extends AbstractPaneCompositePart { prepend(titleArea, $('div.titlebar-drag-region')); } + // Session status widget (far left of sidebar header, across from filter) + if (titleArea) { + const widget = this._register(this.instantiationService.createInstance(SessionStatusWidget, titleArea, { prependToParent: true })); + widget.show(); + } + // macOS native: the sidebar spans full height and the traffic lights // overlay the top-left corner. Add a fixed-width spacer inside the // title area to push content horizontally past the traffic lights. diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 2436595fd59..af286054153 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -5,7 +5,7 @@ import '../../workbench/browser/style.js'; import './media/style.css'; -import { CollapsedSidebarWidget, CollapsedAuxiliaryBarWidget } from './collapsedPartWidgets.js'; +import { SessionStatusWidget, CollapsedAuxiliaryBarWidget } from './collapsedPartWidgets.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { Emitter, Event, setGlobalLeakWarningThreshold } from '../../base/common/event.js'; import { getActiveDocument, getActiveElement, getClientArea, getWindowId, getWindows, IDimension, isAncestorUsingFlowTo, size, Dimension, runWhenWindowIdle } from '../../base/browser/dom.js'; @@ -244,7 +244,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { private chatBarPartView!: ISerializableView; - private collapsedSidebarWidget: CollapsedSidebarWidget | undefined; + private titlebarSessionStatusWidget: SessionStatusWidget | undefined; private collapsedAuxiliaryBarWidget: CollapsedAuxiliaryBarWidget | undefined; private readonly partVisibility: IPartVisibilityState = { @@ -385,11 +385,11 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { // Layout this.layout(); - // Collapsed Sidebar Widget (shown when sidebar is hidden) + // Session status widget in titlebar (shown when sidebar is collapsed) const titlebarPart = this.getPart(Parts.TITLEBAR_PART) as TitlebarPart; - this.collapsedSidebarWidget = this._register(instantiationService.createInstance(CollapsedSidebarWidget, titlebarPart.leftContainer)); + this.titlebarSessionStatusWidget = this._register(instantiationService.createInstance(SessionStatusWidget, titlebarPart.leftContainer, undefined)); if (!this.partVisibility.sidebar) { - this.collapsedSidebarWidget.show(); + this.titlebarSessionStatusWidget.show(); } // Auxiliary bar changes widget (always visible, acts as a toggle) @@ -1105,13 +1105,15 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !hidden, ); - // Toggle collapsed sidebar widget + // Toggle titlebar session status widget (visible when sidebar is hidden) if (hidden) { - this.collapsedSidebarWidget?.show(); + this.titlebarSessionStatusWidget?.show(); } else { - this.collapsedSidebarWidget?.hide(); + this.titlebarSessionStatusWidget?.hide(); } + + // If sidebar becomes hidden, also hide the current active pane composite if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); From f8aa82cf2168f1808c38e47674dcf93f898b1949 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 23 Mar 2026 16:26:19 +0000 Subject: [PATCH 2/5] feat: implement session count widget in titlebar with unread session tracking Co-authored-by: Copilot --- src/vs/sessions/browser/layoutActions.ts | 8 +-- .../browser/media/collapsedPanelWidget.css | 7 ++- src/vs/sessions/browser/media/style.css | 3 +- .../browser/parts/media/sidebarPart.css | 39 +++++++++++- src/vs/sessions/browser/parts/sidebarPart.ts | 62 +++++++++++++++++-- src/vs/sessions/browser/workbench.ts | 18 +----- .../browser/media/sessionsTitleBarWidget.css | 29 +++++++++ .../browser/sessionsTitleBarWidget.ts | 46 +++++++++++++- 8 files changed, 178 insertions(+), 34 deletions(-) diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index 8f6701aecb4..74e7b88061d 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -14,7 +14,7 @@ import { Menus } from './menus.js'; import { ServicesAccessor } from '../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../platform/keybinding/common/keybindingsRegistry.js'; import { registerIcon } from '../../platform/theme/common/iconRegistry.js'; -import { AuxiliaryBarVisibleContext, IsAuxiliaryWindowContext, IsWindowAlwaysOnTopContext, SideBarVisibleContext } from '../../workbench/common/contextkeys.js'; +import { AuxiliaryBarVisibleContext, IsAuxiliaryWindowContext, IsWindowAlwaysOnTopContext } from '../../workbench/common/contextkeys.js'; import { IWorkbenchLayoutService, Parts } from '../../workbench/services/layout/browser/layoutService.js'; import { SessionsWelcomeVisibleContext } from '../common/contextkeys.js'; @@ -41,12 +41,6 @@ class ToggleSidebarVisibilityAction extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.KeyB }, menu: [ - { - id: Menus.SidebarTitle, - group: 'navigation', - order: 100, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SideBarVisibleContext, SessionsWelcomeVisibleContext.toNegated()) - }, { id: Menus.TitleBarContext, group: 'navigation', diff --git a/src/vs/sessions/browser/media/collapsedPanelWidget.css b/src/vs/sessions/browser/media/collapsedPanelWidget.css index 038cb04de5a..5b7bb2c3423 100644 --- a/src/vs/sessions/browser/media/collapsedPanelWidget.css +++ b/src/vs/sessions/browser/media/collapsedPanelWidget.css @@ -34,6 +34,11 @@ padding-left: 0; } +/* When inside the sidebar title area (panel open), no left padding */ +.agent-sessions-workbench .part.sidebar .collapsed-sidebar-widget { + padding-left: 0; +} + /* ---- Auxiliary Bar widget (in titlebar-right) ---- */ .agent-sessions-workbench .collapsed-auxbar-widget { @@ -97,7 +102,7 @@ } .agent-sessions-workbench .collapsed-sidebar-count { - color: var(--vscode-textLink-foreground); + color: inherit; } diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index c029113b4ea..16373c8c63f 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -15,7 +15,6 @@ * * Margin values (must match the constants in the Part classes): * Sidebar: no card (flush, spans full height) - * Chat bar: top=16, bottom=2, left=16, right=16 * Auxiliary bar: top=16, bottom=18, right=16 * Panel: bottom=18, left=16, right=16 */ @@ -88,7 +87,7 @@ margin: 0 auto !important; display: inherit !important; /* Align with panel (terminal) card margin */ - padding: 4px 16px !important; + padding: 4px 16px 16px 16px !important; box-sizing: border-box; } diff --git a/src/vs/sessions/browser/parts/media/sidebarPart.css b/src/vs/sessions/browser/parts/media/sidebarPart.css index 0162bcb26d0..ff443c1f396 100644 --- a/src/vs/sessions/browser/parts/media/sidebarPart.css +++ b/src/vs/sessions/browser/parts/media/sidebarPart.css @@ -24,7 +24,8 @@ } /* Interactive elements in the title area must not be draggable */ -.agent-sessions-workbench .part.sidebar > .composite.title .action-item { +.agent-sessions-workbench .part.sidebar > .composite.title .action-item, +.agent-sessions-workbench .part.sidebar > .composite.title > .session-status-toggle { -webkit-app-region: no-drag; } @@ -56,3 +57,39 @@ max-width: 100%; cursor: default; } + +/* Session status toggle — standalone button in sidebar title area */ +.agent-sessions-workbench .part.sidebar .session-status-toggle { + display: flex; + align-items: center; + align-self: center; + gap: 3px; + height: 22px; + padding: 0 4px; + margin-right: 4px; + border: none; + border-radius: 4px; + cursor: pointer; + color: inherit; + font: inherit; + background: var(--vscode-toolbar-activeBackground); + outline: none; + position: relative; + z-index: 1; +} + +.agent-sessions-workbench .part.sidebar .session-status-toggle:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.agent-sessions-workbench .part.sidebar .session-status-toggle .codicon { + font-size: 16px; +} + +.agent-sessions-workbench .part.sidebar .session-status-toggle-badge { + font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", sans-serif; + font-size: 12px; + font-variant-numeric: tabular-nums; + line-height: 16px; + color: inherit; +} diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index a36b81bf7a6..a109e091aab 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -33,14 +33,18 @@ import { Separator } from '../../../base/common/actions.js'; import { IHoverService } from '../../../platform/hover/browser/hover.js'; import { Extensions } from '../../../workbench/browser/panecomposite.js'; import { Menus } from '../menus.js'; -import { $, append, getWindowId, prepend } from '../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType, getWindowId, prepend } from '../../../base/browser/dom.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js'; import { isMacintosh, isNative } from '../../../base/common/platform.js'; import { isFullscreen, onDidChangeFullscreen } from '../../../base/browser/browser.js'; import { mainWindow } from '../../../base/browser/window.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; import { hasNativeTitlebar, getTitleBarStyle } from '../../../platform/window/common/window.js'; -import { SessionStatusWidget } from '../collapsedPartWidgets.js'; +import { ThemeIcon } from '../../../base/common/themables.js'; +import { Codicon } from '../../../base/common/codicons.js'; +import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { IAgentSessionsService } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { AgentSessionStatus } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; /** * Sidebar part specifically for agent sessions workbench. @@ -152,10 +156,9 @@ export class SidebarPart extends AbstractPaneCompositePart { prepend(titleArea, $('div.titlebar-drag-region')); } - // Session status widget (far left of sidebar header, across from filter) + // Session toggle widget (right side of title area) if (titleArea) { - const widget = this._register(this.instantiationService.createInstance(SessionStatusWidget, titleArea, { prependToParent: true })); - widget.show(); + this.createSessionsToggle(titleArea); } // macOS native: the sidebar spans full height and the traffic lights @@ -184,6 +187,55 @@ export class SidebarPart extends AbstractPaneCompositePart { return titleArea; } + /** + * Creates a standalone session toggle widget appended to the sidebar title area. + * Displays a tasklist icon with an optional unread badge. Clicking hides the sidebar. + */ + private createSessionsToggle(titleArea: HTMLElement): void { + const widgetDisposables = this._register(new DisposableStore()); + + const widget = append(titleArea, $('button.session-status-toggle')) as HTMLButtonElement; + widget.type = 'button'; + widget.tabIndex = 0; + widget.setAttribute('role', 'button'); + append(widget, $(ThemeIcon.asCSSSelector(Codicon.tasklist))); + const badge = append(widget, $('span.session-status-toggle-badge')); + + // Toggle sidebar on click + widgetDisposables.add(addDisposableListener(widget, EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this.layoutService.setPartHidden(true, Parts.SIDEBAR_PART); + })); + + // Update badge on session changes (deferred to avoid service unavailability) + const updateBadge = () => { + try { + const svc = this.instantiationService.invokeFunction(accessor => accessor.get(IAgentSessionsService)); + let unread = 0; + for (const session of svc.model.sessions) { + if (!session.isArchived() && session.status === AgentSessionStatus.Completed && !session.isRead()) { + unread++; + } + } + badge.textContent = unread > 0 ? `${unread}` : ''; + badge.style.display = unread > 0 ? '' : 'none'; + } catch { + badge.style.display = 'none'; + } + }; + + setTimeout(() => { + updateBadge(); + try { + const svc = this.instantiationService.invokeFunction(accessor => accessor.get(IAgentSessionsService)); + widgetDisposables.add(svc.model.onDidChangeSessions(() => updateBadge())); + } catch { + // Service not yet available + } + }, 0); + } + private createFooter(parent: HTMLElement): void { const footer = append(parent, $('.sidebar-footer.sidebar-action-list')); this.footerContainer = footer; diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index af286054153..0ff4bb561ad 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -5,7 +5,7 @@ import '../../workbench/browser/style.js'; import './media/style.css'; -import { SessionStatusWidget, CollapsedAuxiliaryBarWidget } from './collapsedPartWidgets.js'; +import { CollapsedAuxiliaryBarWidget } from './collapsedPartWidgets.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { Emitter, Event, setGlobalLeakWarningThreshold } from '../../base/common/event.js'; import { getActiveDocument, getActiveElement, getClientArea, getWindowId, getWindows, IDimension, isAncestorUsingFlowTo, size, Dimension, runWhenWindowIdle } from '../../base/browser/dom.js'; @@ -244,7 +244,6 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { private chatBarPartView!: ISerializableView; - private titlebarSessionStatusWidget: SessionStatusWidget | undefined; private collapsedAuxiliaryBarWidget: CollapsedAuxiliaryBarWidget | undefined; private readonly partVisibility: IPartVisibilityState = { @@ -385,14 +384,8 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { // Layout this.layout(); - // Session status widget in titlebar (shown when sidebar is collapsed) - const titlebarPart = this.getPart(Parts.TITLEBAR_PART) as TitlebarPart; - this.titlebarSessionStatusWidget = this._register(instantiationService.createInstance(SessionStatusWidget, titlebarPart.leftContainer, undefined)); - if (!this.partVisibility.sidebar) { - this.titlebarSessionStatusWidget.show(); - } - // Auxiliary bar changes widget (always visible, acts as a toggle) + const titlebarPart = this.getPart(Parts.TITLEBAR_PART) as TitlebarPart; this.collapsedAuxiliaryBarWidget = this._register(instantiationService.createInstance(CollapsedAuxiliaryBarWidget, titlebarPart.rightContainer, titlebarPart.rightWindowControlsContainer)); this.collapsedAuxiliaryBarWidget.updateActiveState(this.partVisibility.auxiliaryBar); @@ -1105,13 +1098,6 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !hidden, ); - // Toggle titlebar session status widget (visible when sidebar is hidden) - if (hidden) { - this.titlebarSessionStatusWidget?.show(); - } else { - this.titlebarSessionStatusWidget?.hide(); - } - // If sidebar becomes hidden, also hide the current active pane composite diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index 6a5ea2b3e82..fa7f8c85b77 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -77,4 +77,33 @@ flex-shrink: 0; } +/* Session count widget (tasklist icon + unread count, left of pill) */ +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; + cursor: pointer; + padding: 0 4px; + height: 22px; + border-radius: 4px; + color: inherit; +} +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count.toggled { + background: var(--vscode-toolbar-activeBackground); +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count .codicon { + font-size: 16px; +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count-label { + font-size: 12px; + font-variant-numeric: tabular-nums; + line-height: 16px; +} diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 774577eeb23..7a512bd6d2a 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -19,8 +19,9 @@ import { IMenuService, MenuId, MenuRegistry, SubmenuItemAction } from '../../../ import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { IMarshalledAgentSessionContext } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IMarshalledAgentSessionContext, AgentSessionStatus } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; @@ -76,6 +77,7 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, ) { super(undefined, action, options); @@ -128,8 +130,9 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { const label = this._getActiveSessionLabel(); const icon = this._getActiveSessionIcon(); const repoLabel = this._getRepositoryLabel(); + const unreadCount = this._countUnreadSessions(); // Build a render-state key from all displayed data - const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}`; + const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}|${unreadCount}`; // Skip re-render if state hasn't changed if (this._lastRenderState === renderState) { @@ -194,6 +197,35 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { this._container.appendChild(sessionPill); + // Session count widget (to the left of the pill) — toggles sidebar + const countWidget = $('span.agent-sessions-titlebar-count'); + const countIcon = $(ThemeIcon.asCSSSelector(Codicon.tasklist)); + countWidget.appendChild(countIcon); + if (unreadCount > 0) { + const countLabel = $('span.agent-sessions-titlebar-count-label'); + countLabel.textContent = `${unreadCount}`; + countWidget.appendChild(countLabel); + } + // Toggle visual state based on sidebar visibility + const updateToggleState = () => { + const sidebarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); + countWidget.classList.toggle('toggled', sidebarVisible); + countWidget.style.display = sidebarVisible ? 'none' : ''; + }; + updateToggleState(); + this._dynamicDisposables.add(this.layoutService.onDidChangePartVisibility(e => { + if (e.partId === Parts.SIDEBAR_PART) { + updateToggleState(); + } + })); + this._dynamicDisposables.add(addDisposableListener(countWidget, EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + const isVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); + this.layoutService.setPartHidden(isVisible, Parts.SIDEBAR_PART); + })); + this._container.insertBefore(countWidget, sessionPill); + // Hover this._dynamicDisposables.add(this.hoverService.setupManagedHover( getDefaultHoverDelegate('mouse'), @@ -305,6 +337,16 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { return basename(uri); } + private _countUnreadSessions(): number { + let unread = 0; + for (const session of this.agentSessionsService.model.sessions) { + if (!session.isArchived() && session.status === AgentSessionStatus.Completed && !session.isRead()) { + unread++; + } + } + return unread; + } + private _showContextMenu(e: MouseEvent): void { const activeSession = this.activeSessionService.getActiveSession(); if (!activeSession) { From 85dff957e62440ef20f605ccfe48c84ae6696e27 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 24 Mar 2026 11:04:09 +0000 Subject: [PATCH 3/5] refactor: update session status widget and titlebar count button for improved accessibility and styling Co-authored-by: Copilot --- .../sessions/browser/collapsedPartWidgets.ts | 162 ++++++++++++++---- .../browser/parts/media/sidebarPart.css | 1 - src/vs/sessions/browser/parts/sidebarPart.ts | 6 +- src/vs/sessions/browser/workbench.ts | 2 - .../browser/media/sessionsTitleBarWidget.css | 8 +- .../browser/sessionsTitleBarWidget.ts | 22 +-- 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/src/vs/sessions/browser/collapsedPartWidgets.ts b/src/vs/sessions/browser/collapsedPartWidgets.ts index b7250dbf416..e5c1789651b 100644 --- a/src/vs/sessions/browser/collapsedPartWidgets.ts +++ b/src/vs/sessions/browser/collapsedPartWidgets.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/collapsedPanelWidget.css'; -import { $, addDisposableListener, append, EventType, prepend } from '../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType } from '../../base/browser/dom.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { IWorkbenchLayoutService, Parts } from '../../workbench/services/layout/browser/layoutService.js'; import { IHoverService } from '../../platform/hover/browser/hover.js'; @@ -14,7 +14,7 @@ import { ThemeIcon } from '../../base/common/themables.js'; import { Codicon } from '../../base/common/codicons.js'; import { IAgentSessionsService } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionStatus, getAgentChangesSummary, IAgentSession } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; - +import { ICommandService } from '../../platform/commands/common/commands.js'; import { IPaneCompositePartService } from '../../workbench/services/panecomposite/browser/panecomposite.js'; import { ViewContainerLocation } from '../../workbench/common/views.js'; import { URI } from '../../base/common/uri.js'; @@ -24,38 +24,35 @@ import { Event } from '../../base/common/event.js'; const CHANGES_VIEW_CONTAINER_ID = 'workbench.view.agentSessions.changesContainer'; /** - * Session status widget displaying a tasklist icon with the number of unread sessions. - * Can be placed in either the sidebar title area or the titlebar. - * When `prepend` option is true, the element is prepended to the parent instead of appended. + * Collapsed widget shown in the bottom-left corner when the sidebar is hidden. + * Shows session status counts (active, errors, completed) and a new session button. */ -export class SessionStatusWidget extends Disposable { +export class CollapsedSidebarWidget extends Disposable { private readonly element: HTMLElement; - private readonly statusBtn: HTMLElement; + private readonly indicatorContainer: HTMLElement; private readonly indicatorDisposables = this._register(new DisposableStore()); private readonly hoverDelegate = this._register(createInstantHoverDelegate()); constructor( parent: HTMLElement, - options: { prependToParent?: boolean } | undefined, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IHoverService private readonly hoverService: IHoverService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @ICommandService private readonly commandService: ICommandService, ) { super(); - this.element = options?.prependToParent - ? prepend(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')) - : append(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')); + this.element = append(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')); - // Status button: tasklist icon + unread badge - this.statusBtn = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-status')); + // Sidebar toggle button (leftmost) + this._register(this.createSidebarToggleButton()); - // Click toggles sidebar visibility - this._register(addDisposableListener(this.statusBtn, EventType.CLICK, () => { - const isVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); - this.layoutService.setPartHidden(isVisible, Parts.SIDEBAR_PART); - })); + // 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())); @@ -66,44 +63,135 @@ export class SessionStatusWidget extends Disposable { this.hide(); } + private createNewSessionButton(): DisposableStore { + const store = new DisposableStore(); + const btn = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-new-session')); + append(btn, $(ThemeIcon.asCSSSelector(Codicon.newSession))); + + store.add(this.hoverService.setupManagedHover(this.hoverDelegate, btn, localize('newSession', "New Session"))); + + store.add(addDisposableListener(btn, EventType.CLICK, () => { + this.commandService.executeCommand('workbench.action.sessions.newChat'); + })); + + 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.statusBtn.textContent = ''; + this.indicatorContainer.textContent = ''; const sessions = this.agentSessionsService.model.sessions; - const unreadCount = this.countUnreadSessions(sessions); + const counts = this.countSessionsByStatus(sessions); - // Always show the tasklist icon - append(this.statusBtn, $(ThemeIcon.asCSSSelector(Codicon.tasklist))); + const tooltipParts: string[] = []; - // Show unread count badge only when there are unread sessions - if (unreadCount > 0) { - const badge = append(this.statusBtn, $('span.collapsed-sidebar-count')); - badge.textContent = `${unreadCount}`; + // In-progress (matches agentSessionsViewer: sessionInProgress) + if (counts.inProgress > 0) { + this.appendStatusSegment(Codicon.sessionInProgress, `${counts.inProgress}`, 'collapsed-sidebar-indicator-active'); + tooltipParts.push(localize('sessionsInProgress', "{0} session(s) in progress", counts.inProgress)); + } + // Needs input (matches agentSessionsViewer: circleFilled) + if (counts.needsInput > 0) { + this.appendStatusSegment(Codicon.circleFilled, `${counts.needsInput}`, 'collapsed-sidebar-indicator-input'); + tooltipParts.push(localize('sessionsNeedInput', "{0} session(s) need input", counts.needsInput)); + } + + // Failed (matches agentSessionsViewer: error) + if (counts.failed > 0) { + this.appendStatusSegment(Codicon.error, `${counts.failed}`, 'collapsed-sidebar-indicator-error'); + tooltipParts.push(localize('sessionsFailed', "{0} session(s) with errors", counts.failed)); + } + + // 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 + if (sessions.length === 0) { + 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.statusBtn, - localize('sessionsUnread', "{0} unread session(s)", unreadCount) - )); - } else { - this.indicatorDisposables.add(this.hoverService.setupManagedHover( - this.hoverDelegate, this.statusBtn, - localize('toggleSidebar', "Toggle Side Bar") + this.hoverDelegate, this.indicatorContainer, tooltipParts.join('\n') )); + + this.indicatorDisposables.add(addDisposableListener(this.indicatorContainer, EventType.CLICK, () => { + this.layoutService.setPartHidden(false, Parts.SIDEBAR_PART); + })); } } - private countUnreadSessions(sessions: IAgentSession[]): number { + 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; + } + + private countSessionsByStatus(sessions: IAgentSession[]): { inProgress: number; needsInput: number; failed: number; unread: number } { + let inProgress = 0; + let needsInput = 0; + let failed = 0; let unread = 0; + for (const session of sessions) { if (session.isArchived()) { continue; } - if (session.status === AgentSessionStatus.Completed && !session.isRead()) { - unread++; + switch (session.status) { + case AgentSessionStatus.InProgress: + inProgress++; + break; + case AgentSessionStatus.NeedsInput: + needsInput++; + break; + case AgentSessionStatus.Failed: + failed++; + break; + case AgentSessionStatus.Completed: + if (!session.isRead()) { + unread++; + } + break; } } - return unread; + + return { inProgress, needsInput, failed, unread }; } show(): void { diff --git a/src/vs/sessions/browser/parts/media/sidebarPart.css b/src/vs/sessions/browser/parts/media/sidebarPart.css index ff443c1f396..1b68c782f84 100644 --- a/src/vs/sessions/browser/parts/media/sidebarPart.css +++ b/src/vs/sessions/browser/parts/media/sidebarPart.css @@ -87,7 +87,6 @@ } .agent-sessions-workbench .part.sidebar .session-status-toggle-badge { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", sans-serif; font-size: 12px; font-variant-numeric: tabular-nums; line-height: 16px; diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index a109e091aab..a4df40f1ddc 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -45,6 +45,7 @@ import { Codicon } from '../../../base/common/codicons.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; import { IAgentSessionsService } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionStatus } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { localize } from '../../../nls.js'; /** * Sidebar part specifically for agent sessions workbench. @@ -197,7 +198,7 @@ export class SidebarPart extends AbstractPaneCompositePart { const widget = append(titleArea, $('button.session-status-toggle')) as HTMLButtonElement; widget.type = 'button'; widget.tabIndex = 0; - widget.setAttribute('role', 'button'); + widget.setAttribute('aria-label', localize('hideSidebar', "Hide Side Bar")); append(widget, $(ThemeIcon.asCSSSelector(Codicon.tasklist))); const badge = append(widget, $('span.session-status-toggle-badge')); @@ -220,6 +221,9 @@ export class SidebarPart extends AbstractPaneCompositePart { } badge.textContent = unread > 0 ? `${unread}` : ''; badge.style.display = unread > 0 ? '' : 'none'; + widget.setAttribute('aria-label', unread > 0 + ? localize('hideSidebarUnread', "Hide Side Bar, {0} unread session(s)", unread) + : localize('hideSidebar', "Hide Side Bar")); } catch { badge.style.display = 'none'; } diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 0ff4bb561ad..db3ae9fda11 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -1098,8 +1098,6 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { !hidden, ); - - // If sidebar becomes hidden, also hide the current active pane composite if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index fa7f8c85b77..567da1a2e66 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -86,18 +86,18 @@ cursor: pointer; padding: 0 4px; height: 22px; + border: none; border-radius: 4px; + background: transparent; color: inherit; + font: inherit; + outline: none; } .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count:hover { background-color: var(--vscode-toolbar-hoverBackground); } -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count.toggled { - background: var(--vscode-toolbar-activeBackground); -} - .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count .codicon { font-size: 16px; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 7a512bd6d2a..28f55020eb3 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -198,31 +198,33 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { this._container.appendChild(sessionPill); // Session count widget (to the left of the pill) — toggles sidebar - const countWidget = $('span.agent-sessions-titlebar-count'); + const countWidget = $('button.agent-sessions-titlebar-count') as HTMLButtonElement; + countWidget.type = 'button'; + countWidget.tabIndex = 0; const countIcon = $(ThemeIcon.asCSSSelector(Codicon.tasklist)); countWidget.appendChild(countIcon); if (unreadCount > 0) { const countLabel = $('span.agent-sessions-titlebar-count-label'); countLabel.textContent = `${unreadCount}`; countWidget.appendChild(countLabel); + countWidget.setAttribute('aria-label', localize('showSidebarUnread', "Show Side Bar, {0} unread session(s)", unreadCount)); + } else { + countWidget.setAttribute('aria-label', localize('showSidebar', "Show Side Bar")); } - // Toggle visual state based on sidebar visibility - const updateToggleState = () => { - const sidebarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); - countWidget.classList.toggle('toggled', sidebarVisible); - countWidget.style.display = sidebarVisible ? 'none' : ''; + // Hide when sidebar is visible (only shown when sidebar is hidden) + const updateVisibility = () => { + countWidget.style.display = this.layoutService.isVisible(Parts.SIDEBAR_PART) ? 'none' : ''; }; - updateToggleState(); + updateVisibility(); this._dynamicDisposables.add(this.layoutService.onDidChangePartVisibility(e => { if (e.partId === Parts.SIDEBAR_PART) { - updateToggleState(); + updateVisibility(); } })); this._dynamicDisposables.add(addDisposableListener(countWidget, EventType.CLICK, (e) => { e.preventDefault(); e.stopPropagation(); - const isVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); - this.layoutService.setPartHidden(isVisible, Parts.SIDEBAR_PART); + this.layoutService.setPartHidden(false, Parts.SIDEBAR_PART); })); this._container.insertBefore(countWidget, sessionPill); From 17abe50723124f9070ee57ef66f7efa5c2d771bb Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 24 Mar 2026 11:06:47 +0000 Subject: [PATCH 4/5] feat: add countUnreadSessions function to streamline unread session counting in sidebar and title bar Co-authored-by: Copilot --- src/vs/sessions/browser/parts/sidebarPart.ts | 31 +++++++------------ .../browser/sessionsTitleBarWidget.ts | 10 ++---- .../agentSessions/agentSessionsModel.ts | 10 ++++++ 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index a4df40f1ddc..2b03d46e25e 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -44,7 +44,7 @@ import { ThemeIcon } from '../../../base/common/themables.js'; import { Codicon } from '../../../base/common/codicons.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; import { IAgentSessionsService } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { AgentSessionStatus } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { countUnreadSessions } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { localize } from '../../../nls.js'; /** @@ -210,32 +210,23 @@ export class SidebarPart extends AbstractPaneCompositePart { })); // Update badge on session changes (deferred to avoid service unavailability) - const updateBadge = () => { - try { - const svc = this.instantiationService.invokeFunction(accessor => accessor.get(IAgentSessionsService)); - let unread = 0; - for (const session of svc.model.sessions) { - if (!session.isArchived() && session.status === AgentSessionStatus.Completed && !session.isRead()) { - unread++; - } - } - badge.textContent = unread > 0 ? `${unread}` : ''; - badge.style.display = unread > 0 ? '' : 'none'; - widget.setAttribute('aria-label', unread > 0 - ? localize('hideSidebarUnread', "Hide Side Bar, {0} unread session(s)", unread) - : localize('hideSidebar', "Hide Side Bar")); - } catch { - badge.style.display = 'none'; - } + const updateBadge = (svc: IAgentSessionsService) => { + const unread = countUnreadSessions(svc.model.sessions); + badge.textContent = unread > 0 ? `${unread}` : ''; + badge.style.display = unread > 0 ? '' : 'none'; + widget.setAttribute('aria-label', unread > 0 + ? localize('hideSidebarUnread', "Hide Side Bar, {0} unread session(s)", unread) + : localize('hideSidebar', "Hide Side Bar")); }; setTimeout(() => { - updateBadge(); try { const svc = this.instantiationService.invokeFunction(accessor => accessor.get(IAgentSessionsService)); - widgetDisposables.add(svc.model.onDidChangeSessions(() => updateBadge())); + updateBadge(svc); + widgetDisposables.add(svc.model.onDidChangeSessions(() => updateBadge(svc))); } catch { // Service not yet available + badge.style.display = 'none'; } }, 0); } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 28f55020eb3..d568036ca54 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -19,7 +19,7 @@ import { IMenuService, MenuId, MenuRegistry, SubmenuItemAction } from '../../../ import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { IMarshalledAgentSessionContext, AgentSessionStatus } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IMarshalledAgentSessionContext, countUnreadSessions } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; import { Menus } from '../../../browser/menus.js'; @@ -340,13 +340,7 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { } private _countUnreadSessions(): number { - let unread = 0; - for (const session of this.agentSessionsService.model.sessions) { - if (!session.isArchived() && session.status === AgentSessionStatus.Completed && !session.isRead()) { - unread++; - } - } - return unread; + return countUnreadSessions(this.agentSessionsService.model.sessions); } private _showContextMenu(e: MouseEvent): void { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index b98bfa148af..05de1113bf4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -158,6 +158,16 @@ export function isAgentSessionsModel(obj: unknown): obj is IAgentSessionsModel { return Array.isArray(sessionsModel?.sessions) && typeof sessionsModel?.getSession === 'function'; } +export function countUnreadSessions(sessions: IAgentSession[]): number { + let unread = 0; + for (const session of sessions) { + if (!session.isArchived() && session.status === AgentSessionStatus.Completed && !session.isRead()) { + unread++; + } + } + return unread; +} + interface IAgentSessionState { readonly archived?: boolean; readonly pinned?: boolean; From 5d16f08a8c04b30de7e8c7768f6c7e7ba4ace3e4 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 24 Mar 2026 11:10:51 +0000 Subject: [PATCH 5/5] refactor: remove unused CollapsedSidebarWidget class and related CSS styles Co-authored-by: Copilot --- .../sessions/browser/collapsedPartWidgets.ts | 183 +----------------- .../browser/media/collapsedPanelWidget.css | 10 - 2 files changed, 1 insertion(+), 192 deletions(-) diff --git a/src/vs/sessions/browser/collapsedPartWidgets.ts b/src/vs/sessions/browser/collapsedPartWidgets.ts index e5c1789651b..387a0af8fda 100644 --- a/src/vs/sessions/browser/collapsedPartWidgets.ts +++ b/src/vs/sessions/browser/collapsedPartWidgets.ts @@ -13,8 +13,7 @@ import { localize } from '../../nls.js'; import { ThemeIcon } from '../../base/common/themables.js'; import { Codicon } from '../../base/common/codicons.js'; import { IAgentSessionsService } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { AgentSessionStatus, getAgentChangesSummary, IAgentSession } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; -import { ICommandService } from '../../platform/commands/common/commands.js'; +import { getAgentChangesSummary } from '../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IPaneCompositePartService } from '../../workbench/services/panecomposite/browser/panecomposite.js'; import { ViewContainerLocation } from '../../workbench/common/views.js'; import { URI } from '../../base/common/uri.js'; @@ -23,186 +22,6 @@ import { Event } from '../../base/common/event.js'; // Duplicated from vs/sessions/contrib/changes/browser/changesView.ts to avoid a layering import. const CHANGES_VIEW_CONTAINER_ID = 'workbench.view.agentSessions.changesContainer'; -/** - * Collapsed widget shown in the bottom-left corner when the sidebar is hidden. - * Shows session status counts (active, errors, completed) and a new session button. - */ -export class CollapsedSidebarWidget extends Disposable { - - private readonly element: HTMLElement; - private readonly indicatorContainer: HTMLElement; - private readonly indicatorDisposables = this._register(new DisposableStore()); - private readonly hoverDelegate = this._register(createInstantHoverDelegate()); - - constructor( - parent: HTMLElement, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IHoverService private readonly hoverService: IHoverService, - @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, - @ICommandService private readonly commandService: ICommandService, - ) { - super(); - - this.element = append(parent, $('.collapsed-panel-widget.collapsed-sidebar-widget')); - - // 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())); - - // Initial build - this.rebuildIndicators(); - - this.hide(); - } - - private createNewSessionButton(): DisposableStore { - const store = new DisposableStore(); - const btn = append(this.element, $('.collapsed-panel-button.collapsed-sidebar-new-session')); - append(btn, $(ThemeIcon.asCSSSelector(Codicon.newSession))); - - store.add(this.hoverService.setupManagedHover(this.hoverDelegate, btn, localize('newSession', "New Session"))); - - store.add(addDisposableListener(btn, EventType.CLICK, () => { - this.commandService.executeCommand('workbench.action.sessions.newChat'); - })); - - 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 = ''; - - const sessions = this.agentSessionsService.model.sessions; - const counts = this.countSessionsByStatus(sessions); - - const tooltipParts: string[] = []; - - // In-progress (matches agentSessionsViewer: sessionInProgress) - if (counts.inProgress > 0) { - this.appendStatusSegment(Codicon.sessionInProgress, `${counts.inProgress}`, 'collapsed-sidebar-indicator-active'); - tooltipParts.push(localize('sessionsInProgress', "{0} session(s) in progress", counts.inProgress)); - } - - // Needs input (matches agentSessionsViewer: circleFilled) - if (counts.needsInput > 0) { - this.appendStatusSegment(Codicon.circleFilled, `${counts.needsInput}`, 'collapsed-sidebar-indicator-input'); - tooltipParts.push(localize('sessionsNeedInput', "{0} session(s) need input", counts.needsInput)); - } - - // Failed (matches agentSessionsViewer: error) - if (counts.failed > 0) { - this.appendStatusSegment(Codicon.error, `${counts.failed}`, 'collapsed-sidebar-indicator-error'); - tooltipParts.push(localize('sessionsFailed', "{0} session(s) with errors", counts.failed)); - } - - // 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 - if (sessions.length === 0) { - 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 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; - } - - private countSessionsByStatus(sessions: IAgentSession[]): { inProgress: number; needsInput: number; failed: number; unread: number } { - let inProgress = 0; - let needsInput = 0; - let failed = 0; - let unread = 0; - - for (const session of sessions) { - if (session.isArchived()) { - continue; - } - switch (session.status) { - case AgentSessionStatus.InProgress: - inProgress++; - break; - case AgentSessionStatus.NeedsInput: - needsInput++; - break; - case AgentSessionStatus.Failed: - failed++; - break; - case AgentSessionStatus.Completed: - if (!session.isRead()) { - unread++; - } - break; - } - } - - return { inProgress, needsInput, failed, unread }; - } - - show(): void { - this.element.classList.remove('collapsed-panel-hidden'); - } - - hide(): void { - this.element.classList.add('collapsed-panel-hidden'); - } -} - /** * Widget shown in the titlebar right area showing file change counts * (files, insertions, deletions) from the active session. diff --git a/src/vs/sessions/browser/media/collapsedPanelWidget.css b/src/vs/sessions/browser/media/collapsedPanelWidget.css index 5b7bb2c3423..05a2b89d867 100644 --- a/src/vs/sessions/browser/media/collapsedPanelWidget.css +++ b/src/vs/sessions/browser/media/collapsedPanelWidget.css @@ -34,11 +34,6 @@ padding-left: 0; } -/* When inside the sidebar title area (panel open), no left padding */ -.agent-sessions-workbench .part.sidebar .collapsed-sidebar-widget { - padding-left: 0; -} - /* ---- Auxiliary Bar widget (in titlebar-right) ---- */ .agent-sessions-workbench .collapsed-auxbar-widget { @@ -101,11 +96,6 @@ color: inherit; } -.agent-sessions-workbench .collapsed-sidebar-count { - color: inherit; -} - - /* ---- Auxiliary bar indicators ---- */ .agent-sessions-workbench .collapsed-auxbar-insertions {