diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index 996acb9fdeb..bd8d5bbc1c1 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -13,22 +13,25 @@ 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 { static readonly ID = 'workbench.action.agentToggleSidebarVisibility'; - static readonly LABEL = localize('compositePart.hideSideBarLabel', "Hide Primary Side Bar"); constructor() { 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 +42,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', @@ -66,7 +75,6 @@ class ToggleSidebarVisibilityAction extends Action2 { class ToggleSecondarySidebarVisibilityAction extends Action2 { static readonly ID = 'workbench.action.agentToggleSecondarySidebarVisibility'; - static readonly LABEL = localize('compositePart.hideSecondarySideBarLabel', "Hide Secondary Side Bar"); constructor() { super({ 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..da144ac677e 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 */ +.agent-sessions-workbench .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; +.agent-sessions-workbench .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..5e0de629eea 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'; @@ -23,13 +22,12 @@ import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; import { ISessionsManagementService } from './sessionsManagementService.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, observableSignalFromEvent } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; 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,93 @@ 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._countBadge.setAttribute('aria-hidden', 'true'); + this._updateBadge(); + + // Single autorun that tracks all badge-relevant state: + // - session list changes (add/remove) via observableSignalFromEvent + // - individual session observable state (status, isRead, isArchived) + // - sidebar visibility changes + const sessionsChanged = observableSignalFromEvent(this, this.sessionsManagementService.onDidChangeSessions); + const sidebarVisibilityChanged = observableSignalFromEvent(this, handler => this.layoutService.onDidChangePartVisibility(e => { + if (e.partId === Parts.SIDEBAR_PART) { + handler(e); + } + })); + this._register(autorun(reader => { + sessionsChanged.read(reader); + sidebarVisibilityChanged.read(reader); + for (const session of this.sessionsManagementService.getSessions()) { + session.isArchived.read(reader); + session.status.read(reader); + session.isRead.read(reader); + } + 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'; + } + + // Update accessible label to include unread count for screen readers + if (this.label) { + const baseLabel = this.action.label || localize('toggleSidebarA11y', "Toggle Primary Side Bar"); + if (unreadCount > 0 && !sidebarVisible) { + this.label.setAttribute('aria-label', localize('toggleSidebarUnread', "{0}, {1} unread session(s)", baseLabel, unreadCount)); + } else { + this.label.setAttribute('aria-label', baseLabel); + } + } + } + + 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 +406,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)); } }