mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
Merge pull request #306272 from microsoft/mrleemurray/sessions-toggle-update
Sessions: refactor toggle action and unread badge
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user