mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:49:36 +01:00
Merge pull request #304424 from microsoft/mrleemurray/systematic-teal-swift
Sessions: Refactor and enhance session status widget in titlebar
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user