feat: enhance sidebar functionality with toggle action and unread badge

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
mrleemurray
2026-03-30 10:26:39 +01:00
parent d96c52ea7d
commit 3bc76baebe
5 changed files with 108 additions and 154 deletions

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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));
}
}