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/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 b7d2d4e78c4..05a2b89d867 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,51 +96,6 @@ color: inherit; } -.agent-sessions-workbench .collapsed-sidebar-segment.collapsed-sidebar-indicator-active .codicon { - 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 ---- */ .agent-sessions-workbench .collapsed-auxbar-insertions { 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..1b68c782f84 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,38 @@ 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 75eb74869dd..2b03d46e25e 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -33,13 +33,19 @@ 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 { 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 { countUnreadSessions } from '../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { localize } from '../../../nls.js'; /** * Sidebar part specifically for agent sessions workbench. @@ -151,6 +157,11 @@ 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. @@ -177,6 +188,49 @@ 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))); + 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 = (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(() => { + try { + const svc = this.instantiationService.invokeFunction(accessor => accessor.get(IAgentSessionsService)); + updateBadge(svc); + widgetDisposables.add(svc.model.onDidChangeSessions(() => updateBadge(svc))); + } catch { + // Service not yet available + badge.style.display = 'none'; + } + }, 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 2436595fd59..db3ae9fda11 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 { 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 collapsedSidebarWidget: CollapsedSidebarWidget | undefined; private collapsedAuxiliaryBarWidget: CollapsedAuxiliaryBarWidget | undefined; private readonly partVisibility: IPartVisibilityState = { @@ -385,14 +384,8 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { // Layout this.layout(); - // Collapsed Sidebar Widget (shown when sidebar is hidden) - const titlebarPart = this.getPart(Parts.TITLEBAR_PART) as TitlebarPart; - this.collapsedSidebarWidget = this._register(instantiationService.createInstance(CollapsedSidebarWidget, titlebarPart.leftContainer)); - if (!this.partVisibility.sidebar) { - this.collapsedSidebarWidget.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 collapsed sidebar widget - if (hidden) { - this.collapsedSidebarWidget?.show(); - } else { - this.collapsedSidebarWidget?.hide(); - } - // 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 6a5ea2b3e82..567da1a2e66 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: 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; + 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..d568036ca54 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, 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'; 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,37 @@ 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'), @@ -305,6 +339,10 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { return basename(uri); } + private _countUnreadSessions(): number { + return countUnreadSessions(this.agentSessionsService.model.sessions); + } + private _showContextMenu(e: MouseEvent): void { const activeSession = this.activeSessionService.getActiveSession(); if (!activeSession) { 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;