From 3bc76baebedbad2935205c4e90953f27205dd2fa Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Mon, 30 Mar 2026 10:26:39 +0100 Subject: [PATCH] feat: enhance sidebar functionality with toggle action and unread badge Co-authored-by: Copilot --- src/vs/sessions/browser/layoutActions.ts | 14 +- .../browser/parts/media/sidebarPart.css | 38 +----- src/vs/sessions/browser/parts/sidebarPart.ts | 34 +---- .../browser/media/sessionsTitleBarWidget.css | 53 +++----- .../browser/sessionsTitleBarWidget.ts | 123 +++++++++++------- 5 files changed, 108 insertions(+), 154 deletions(-) diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index 996acb9fdeb..6a435b0c33a 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -13,11 +13,12 @@ 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 { IsAuxiliaryWindowContext, IsWindowAlwaysOnTopContext } from '../../workbench/common/contextkeys.js'; +import { IsAuxiliaryWindowContext, IsWindowAlwaysOnTopContext, SideBarVisibleContext } from '../../workbench/common/contextkeys.js'; import { IWorkbenchLayoutService, Parts } from '../../workbench/services/layout/browser/layoutService.js'; // Register Icons const panelCloseIcon = registerIcon('agent-panel-close', Codicon.close, localize('agentPanelCloseIcon', "Icon to close the panel.")); +const sidebarToggleIcon = registerIcon('agent-sidebar-toggle', Codicon.tasklist, localize('agentSidebarToggleIcon', "Icon to toggle the sessions sidebar.")); class ToggleSidebarVisibilityAction extends Action2 { @@ -28,7 +29,10 @@ class ToggleSidebarVisibilityAction extends Action2 { super({ id: ToggleSidebarVisibilityAction.ID, title: localize2('toggleSidebar', 'Toggle Primary Side Bar Visibility'), - icon: panelCloseIcon, + icon: sidebarToggleIcon, + toggled: { + condition: SideBarVisibleContext, + }, metadata: { description: localize('openAndCloseSidebar', 'Open/Show and Close/Hide Sidebar'), }, @@ -39,6 +43,12 @@ class ToggleSidebarVisibilityAction extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.KeyB }, menu: [ + { + id: Menus.TitleBarLeftLayout, + group: 'navigation', + order: 0, + when: IsAuxiliaryWindowContext.toNegated() + }, { id: Menus.TitleBarContext, group: 'navigation', diff --git a/src/vs/sessions/browser/parts/media/sidebarPart.css b/src/vs/sessions/browser/parts/media/sidebarPart.css index 0442acdd65d..1ddbeedf2c2 100644 --- a/src/vs/sessions/browser/parts/media/sidebarPart.css +++ b/src/vs/sessions/browser/parts/media/sidebarPart.css @@ -24,8 +24,7 @@ } /* 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 > .session-status-toggle { +.agent-sessions-workbench .part.sidebar > .composite.title .action-item { -webkit-app-region: no-drag; } @@ -73,38 +72,3 @@ 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-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 6f848d84fdc..0bd85fb27d6 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -32,17 +32,13 @@ 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 { $, addDisposableListener, append, EventType, getWindowId, prepend } from '../../../base/browser/dom.js'; +import { $, append, 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 { ThemeIcon } from '../../../base/common/themables.js'; -import { Codicon } from '../../../base/common/codicons.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; -import { localize } from '../../../nls.js'; +import { isMacintosh, isNative } from '../../../base/common/platform.js'; /** * Sidebar part specifically for agent sessions workbench. @@ -156,11 +152,6 @@ export class SidebarPart extends AbstractPaneCompositePart { prepend(titleArea, $('div.titlebar-drag-region')); } - // Session toggle widget (right side of title area) - if (titleArea) { - this.createSessionsToggle(titleArea); - } - // 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. @@ -187,27 +178,6 @@ 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('aria-label', localize('hideSidebar', "Hide Side Bar")); - append(widget, $(ThemeIcon.asCSSSelector(Codicon.tasklist))); - - // Toggle sidebar on click - widgetDisposables.add(addDisposableListener(widget, EventType.CLICK, (e) => { - e.preventDefault(); - e.stopPropagation(); - this.layoutService.setPartHidden(true, Parts.SIDEBAR_PART); - })); - } - private createFooter(parent: HTMLElement): void { const footer = append(parent, $('.sidebar-footer.sidebar-action-list')); this.footerContainer = footer; diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index 6136099f682..9dd003889bd 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -86,42 +86,27 @@ opacity: 0.7; } -/* Provider label (shown for untitled sessions) */ -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-provider { - display: flex; - align-items: center; - flex-shrink: 0; - opacity: 0.7; +/* Sidebar toggle unread badge */ +.action-item.sidebar-toggle-action { + position: relative; } -/* 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: 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 .codicon { - font-size: 16px; -} - -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-count-label { - font-size: 12px; +.action-item.sidebar-toggle-action .sidebar-toggle-badge { + position: absolute; + top: 0; + right: -4px; + min-width: 14px; + height: 14px; + line-height: 14px; + padding: 0 3px; + box-sizing: border-box; + font-size: 9px; + font-weight: 600; font-variant-numeric: tabular-nums; - line-height: 16px; + text-align: center; + border-radius: 4px; + background-color: var(--vscode-activityBarBadge-background); + color: var(--vscode-activityBarBadge-foreground); + pointer-events: none; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index fb5d3a5f359..4090f931d81 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import './media/sessionsTitleBarWidget.css'; -import { $, addDisposableListener, EventType, getActiveWindow, reset } from '../../../../base/browser/dom.js'; - -import { Separator } from '../../../../base/common/actions.js'; +import { $, addDisposableListener, append, EventType, getActiveWindow, reset } from '../../../../base/browser/dom.js'; +import { IAction, Separator } from '../../../../base/common/actions.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { localize } from '../../../../nls.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ActionViewItem, BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IMenuService, MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -29,7 +28,6 @@ import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextke import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; import { ISessionsProvidersService } from './sessionsProvidersService.js'; import { SessionStatus } from '../common/sessionData.js'; -import { Codicon } from '../../../../base/common/codicons.js'; import { SHOW_SESSIONS_PICKER_COMMAND_ID } from './sessionsActions.js'; import { IsSessionArchivedContext, IsSessionPinnedContext, IsSessionReadContext, SessionItemContextMenuId } from './views/sessionsList.js'; @@ -67,7 +65,6 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ICommandService private readonly commandService: ICommandService, ) { super(undefined, action, options); @@ -131,9 +128,8 @@ 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 ?? ''}|${unreadCount}`; + const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}`; // Skip re-render if state hasn't changed if (this._lastRenderState === renderState) { @@ -198,37 +194,6 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { this._container.appendChild(sessionPill); - // Session count widget (to the left of the pill) — toggles sidebar - 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")); - } - // Hide when sidebar is visible (only shown when sidebar is hidden) - const updateVisibility = () => { - countWidget.style.display = this.layoutService.isVisible(Parts.SIDEBAR_PART) ? 'none' : ''; - }; - updateVisibility(); - this._dynamicDisposables.add(this.layoutService.onDidChangePartVisibility(e => { - if (e.partId === Parts.SIDEBAR_PART) { - updateVisibility(); - } - })); - this._dynamicDisposables.add(addDisposableListener(countWidget, EventType.CLICK, (e) => { - e.preventDefault(); - e.stopPropagation(); - this.layoutService.setPartHidden(false, Parts.SIDEBAR_PART); - })); - this._container.insertBefore(countWidget, sessionPill); - // Hover this._dynamicDisposables.add(this.hoverService.setupManagedHover( getDefaultHoverDelegate('mouse'), @@ -285,16 +250,6 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { return undefined; } - private _countUnreadSessions(): number { - let unread = 0; - for (const session of this.sessionsManagementService.getSessions()) { - if (!session.isArchived.get() && session.status.get() === SessionStatus.Completed && !session.isRead.get()) { - unread++; - } - } - return unread; - } - private _showContextMenu(e: MouseEvent): void { const sessionData = this.sessionsManagementService.activeSession.get(); if (!sessionData) { @@ -324,6 +279,71 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { } } +/** + * Custom action view item for the sidebar toggle button. + * Renders the tasklist icon with an unread session count badge. + */ +class SidebarToggleActionViewItem extends ActionViewItem { + + private _countBadge: HTMLElement | undefined; + + constructor( + context: unknown, + action: IAction, + options: IBaseActionViewItemOptions | undefined, + @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + ) { + super(context, action, { ...options, icon: true, label: false }); + } + + override render(container: HTMLElement): void { + super.render(container); + + container.classList.add('sidebar-toggle-action'); + + // Add badge element for unread session count + this._countBadge = append(container, $('span.sidebar-toggle-badge')); + this._updateBadge(); + + // Update badge when sessions change + this._register(this.sessionsManagementService.onDidChangeSessions(() => this._updateBadge())); + + // Update badge when sidebar visibility changes + this._register(this.layoutService.onDidChangePartVisibility(e => { + if (e.partId === Parts.SIDEBAR_PART) { + this._updateBadge(); + } + })); + } + + private _updateBadge(): void { + if (!this._countBadge) { + return; + } + + const unreadCount = this._countUnreadSessions(); + const sidebarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); + + if (unreadCount > 0 && !sidebarVisible) { + this._countBadge.textContent = `${unreadCount}`; + this._countBadge.style.display = ''; + } else { + this._countBadge.style.display = 'none'; + } + } + + private _countUnreadSessions(): number { + let unread = 0; + for (const session of this.sessionsManagementService.getSessions()) { + if (!session.isArchived.get() && session.status.get() === SessionStatus.Completed && !session.isRead.get()) { + unread++; + } + } + return unread; + } +} + /** * Provides custom rendering for the sessions title bar widget * in the command center. Uses IActionViewItemService to render a custom widget @@ -364,5 +384,10 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben } return instantiationService.createInstance(SessionsTitleBarWidget, action, options); }, undefined)); + + // Register custom view item for sidebar toggle with unread badge + this._register(actionViewItemService.register(Menus.TitleBarLeftLayout, 'workbench.action.agentToggleSidebarVisibility', (action, options) => { + return instantiationService.createInstance(SidebarToggleActionViewItem, undefined, action, options); + }, undefined)); } }