Path: blob/main/src/vs/sessions/browser/parts/sidebarPart.ts
13395 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import '../../../workbench/browser/parts/sidebar/media/sidebarpart.css';6import './media/sidebarPart.css';7import { IWorkbenchLayoutService, Parts, Position as SideBarPosition } from '../../../workbench/services/layout/browser/layoutService.js';8import { SidebarFocusContext, ActiveViewletContext } from '../../../workbench/common/contextkeys.js';9import { IStorageService } from '../../../platform/storage/common/storage.js';10import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';11import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';12import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';13import { IThemeService } from '../../../platform/theme/common/themeService.js';14import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_TITLE_BORDER, SIDE_BAR_FOREGROUND, SIDE_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_TOP_FOREGROUND, ACTIVITY_BAR_TOP_ACTIVE_BORDER, ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND, ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER } from '../../../workbench/common/theme.js';15import { agentsPanelForeground } from '../../common/theme.js';16import { INotificationService } from '../../../platform/notification/common/notification.js';17import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';18import { AnchorAlignment } from '../../../base/browser/ui/contextview/contextview.js';19import { IExtensionService } from '../../../workbench/services/extensions/common/extensions.js';20import { LayoutPriority } from '../../../base/browser/ui/grid/grid.js';21import { assertReturnsDefined } from '../../../base/common/types.js';22import { IViewDescriptorService, ViewContainerLocation } from '../../../workbench/common/views.js';23import { AbstractPaneCompositePart, CompositeBarPosition } from '../../../workbench/browser/parts/paneCompositePart.js';24import { ICompositeTitleLabel } from '../../../workbench/browser/parts/compositePart.js';25import { Part } from '../../../workbench/browser/part.js';26import { ActionsOrientation } from '../../../base/browser/ui/actionbar/actionbar.js';27import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js';28import { IPaneCompositeBarOptions } from '../../../workbench/browser/parts/paneCompositeBar.js';29import { IMenuService } from '../../../platform/actions/common/actions.js';30import { Separator } from '../../../base/common/actions.js';31import { IHoverService } from '../../../platform/hover/browser/hover.js';32import { Extensions } from '../../../workbench/browser/panecomposite.js';33import { Menus } from '../menus.js';34import { $, append, getWindowId, prepend } from '../../../base/browser/dom.js';35import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js';36import { isFullscreen, onDidChangeFullscreen } from '../../../base/browser/browser.js';37import { mainWindow } from '../../../base/browser/window.js';38import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';39import { hasNativeTitlebar, getTitleBarStyle } from '../../../platform/window/common/window.js';40import { isMacintosh, isNative, isWeb } from '../../../base/common/platform.js';4142/**43* Sidebar part specifically for agent sessions workbench.44* This is a simplified version of the SidebarPart for agent session contexts.45*/46export class SidebarPart extends AbstractPaneCompositePart {4748static readonly activeViewletSettingsKey = 'workbench.agentsession.sidebar.activeviewletid';49static readonly pinnedViewContainersKey = 'workbench.agentsession.pinnedViewlets2';50static readonly placeholderViewContainersKey = 'workbench.agentsession.placeholderViewlets';51static readonly viewContainersWorkspaceStateKey = 'workbench.agentsession.viewletsWorkspaceState';5253/** Visual margin values - sidebar is flush (no card appearance) */54static readonly MARGIN_TOP = 0;55static readonly MARGIN_BOTTOM = 0;56static readonly MARGIN_LEFT = 0;57private static readonly FOOTER_ITEM_HEIGHT = 26;58private static readonly FOOTER_ITEM_GAP = 4;59private static readonly FOOTER_VERTICAL_PADDING = 6;60private static readonly FOOTER_BOTTOM_MARGIN = 2;61private static readonly FOOTER_BORDER_TOP = 1;6263private footerContainer: HTMLElement | undefined;64private sideBarTitleArea: HTMLElement | undefined;65private footerToolbar: MenuWorkbenchToolBar | undefined;66private previousLayoutDimensions: { width: number; height: number; top: number; left: number } | undefined;6768//#region IView6970// On web the titlebar hosts an additional host filter combo alongside the71// sidebar toggle; use a wider minimum so those controls always fit within72// the sidebar's rendered area (below this the sidebar snaps closed).73readonly minimumWidth: number = isWeb ? 270 : 170;74readonly maximumWidth: number = Number.POSITIVE_INFINITY;75readonly minimumHeight: number = 0;76readonly maximumHeight: number = Number.POSITIVE_INFINITY;77override get snap(): boolean { return true; }7879readonly priority: LayoutPriority = LayoutPriority.Low;8081get preferredWidth(): number | undefined {82const viewlet = this.getActivePaneComposite();8384if (!viewlet) {85return undefined;86}8788const width = viewlet.getOptimalWidth();89if (typeof width !== 'number') {90return undefined;91}9293return Math.max(width, 300);94}9596//#endregion9798constructor(99@INotificationService notificationService: INotificationService,100@IStorageService storageService: IStorageService,101@IContextMenuService contextMenuService: IContextMenuService,102@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,103@IKeybindingService keybindingService: IKeybindingService,104@IHoverService hoverService: IHoverService,105@IInstantiationService instantiationService: IInstantiationService,106@IThemeService themeService: IThemeService,107@IViewDescriptorService viewDescriptorService: IViewDescriptorService,108@IContextKeyService contextKeyService: IContextKeyService,109@IExtensionService extensionService: IExtensionService,110@IMenuService menuService: IMenuService,111@IConfigurationService private readonly configurationService: IConfigurationService,112) {113super(114Parts.SIDEBAR_PART,115{ hasTitle: false, trailingSeparator: false, borderWidth: () => 0 },116SidebarPart.activeViewletSettingsKey,117ActiveViewletContext.bindTo(contextKeyService),118SidebarFocusContext.bindTo(contextKeyService),119'sideBar',120'viewlet',121SIDE_BAR_TITLE_FOREGROUND,122SIDE_BAR_TITLE_BORDER,123ViewContainerLocation.Sidebar,124Extensions.Viewlets,125Menus.SidebarTitle,126notificationService,127storageService,128contextMenuService,129layoutService,130keybindingService,131hoverService,132instantiationService,133themeService,134viewDescriptorService,135contextKeyService,136extensionService,137menuService,138);139}140141override create(parent: HTMLElement): void {142super.create(parent);143this.createFooter(parent);144}145146protected override createTitleArea(parent: HTMLElement): HTMLElement | undefined {147const titleArea = super.createTitleArea(parent);148this.sideBarTitleArea = titleArea;149150if (titleArea) {151// Add a drag region so the sidebar title area can be used to move the window,152// matching the titlebar's drag behavior.153prepend(titleArea, $('div.titlebar-drag-region'));154}155156// macOS native: the sidebar spans full height and the traffic lights157// overlay the top-left corner. Add a fixed-width spacer inside the158// title area to push content horizontally past the traffic lights.159if (titleArea && isMacintosh && isNative && !hasNativeTitlebar(this.configurationService, getTitleBarStyle(this.configurationService))) {160const spacer = $('div.window-controls-container');161spacer.style.width = '70px';162spacer.style.height = '100%';163spacer.style.flexShrink = '0';164spacer.style.order = '-1'; // match global-actions-left order so DOM order is respected165prepend(titleArea, spacer);166167// Hide spacer in fullscreen (traffic lights are not shown)168const updateSpacerVisibility = () => {169spacer.style.display = isFullscreen(mainWindow) ? 'none' : '';170};171updateSpacerVisibility();172this._register(onDidChangeFullscreen(windowId => {173if (windowId === getWindowId(mainWindow)) {174updateSpacerVisibility();175}176}));177}178179return titleArea;180}181182private createFooter(parent: HTMLElement): void {183const footer = append(parent, $('.sidebar-footer.sidebar-action-list'));184this.footerContainer = footer;185186this.footerToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, footer, Menus.SidebarFooter, {187hiddenItemStrategy: HiddenItemStrategy.NoHide,188toolbarOptions: { primaryGroup: () => true },189telemetrySource: 'sidebarFooter',190}));191192this._register(this.footerToolbar.onDidChangeMenuItems(() => {193if (this.previousLayoutDimensions) {194const { width, height, top, left } = this.previousLayoutDimensions;195this.layout(width, height, top, left);196}197}));198}199200private getFooterHeight(): number {201const actionCount = this.footerToolbar?.getItemsLength() ?? 0;202if (actionCount === 0) {203return 0;204}205206return SidebarPart.FOOTER_VERTICAL_PADDING * 2207+ (actionCount * SidebarPart.FOOTER_ITEM_HEIGHT)208+ ((actionCount - 1) * SidebarPart.FOOTER_ITEM_GAP)209+ SidebarPart.FOOTER_BOTTOM_MARGIN210+ SidebarPart.FOOTER_BORDER_TOP;211}212213private updateFooterVisibility(): void {214const footer = this.footerContainer;215if (!footer) {216return;217}218219footer.style.display = this.getFooterHeight() > 0 ? '' : 'none';220}221222override updateStyles(): void {223super.updateStyles();224225const container = assertReturnsDefined(this.getContainer());226227container.style.backgroundColor = 'transparent';228container.style.color = this.getColor(SIDE_BAR_FOREGROUND) || '';229container.style.outlineColor = this.getColor(SIDE_BAR_DRAG_AND_DROP_BACKGROUND) ?? '';230231// No right border in sessions sidebar232container.style.borderRightWidth = '';233container.style.borderRightStyle = '';234container.style.borderRightColor = '';235236if (this.sideBarTitleArea) {237this.sideBarTitleArea.style.backgroundColor = 'transparent';238this.sideBarTitleArea.style.color = this.getColor(agentsPanelForeground) || '';239}240}241242override layout(width: number, height: number, top: number, left: number): void {243this.previousLayoutDimensions = { width, height, top, left };244245if (!this.layoutService.isVisible(Parts.SIDEBAR_PART)) {246return;247}248249this.updateFooterVisibility();250const footerHeight = Math.min(height, this.getFooterHeight());251252// Layout content with reduced height to account for footer253super.layout(254width,255height - footerHeight,256top, left257);258259// Restore the full grid-allocated dimensions so that Part.relayout() works correctly.260Part.prototype.layout.call(this, width, height, top, left);261}262263protected override getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {264return this.layoutService.getSideBarPosition() === SideBarPosition.LEFT ? AnchorAlignment.LEFT : AnchorAlignment.RIGHT;265}266267protected override createTitleLabel(_parent: HTMLElement): ICompositeTitleLabel {268// No title label in agent sessions sidebar269return {270updateTitle: () => { },271updateStyles: () => { }272};273}274275protected getCompositeBarOptions(): IPaneCompositeBarOptions {276return {277partContainerClass: 'sidebar',278pinnedViewContainersKey: SidebarPart.pinnedViewContainersKey,279placeholderViewContainersKey: SidebarPart.placeholderViewContainersKey,280viewContainersWorkspaceStateKey: SidebarPart.viewContainersWorkspaceStateKey,281icon: false,282orientation: ActionsOrientation.HORIZONTAL,283recomputeSizes: true,284activityHoverOptions: {285position: () => this.getCompositeBarPosition() === CompositeBarPosition.BOTTOM ? HoverPosition.ABOVE : HoverPosition.BELOW,286},287fillExtraContextMenuActions: actions => {288if (this.getCompositeBarPosition() === CompositeBarPosition.TITLE) {289const viewsSubmenuAction = this.getViewsSubmenuAction();290if (viewsSubmenuAction) {291actions.push(new Separator());292actions.push(viewsSubmenuAction);293}294}295},296compositeSize: 0,297iconSize: 16,298overflowActionSize: 30,299colors: theme => ({300activeBackgroundColor: undefined,301inactiveBackgroundColor: undefined,302activeBorderBottomColor: theme.getColor(ACTIVITY_BAR_TOP_ACTIVE_BORDER),303activeForegroundColor: theme.getColor(ACTIVITY_BAR_TOP_FOREGROUND),304inactiveForegroundColor: theme.getColor(ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND),305badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND),306badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND),307dragAndDropBorder: theme.getColor(ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER)308}),309compact: true310};311}312313protected shouldShowCompositeBar(): boolean {314return false;315}316317protected getCompositeBarPosition(): CompositeBarPosition {318return CompositeBarPosition.TITLE;319}320321async focusActivityBar(): Promise<void> {322if (this.shouldShowCompositeBar()) {323this.focusCompositeBar();324}325}326327toJSON(): object {328return {329type: Parts.SIDEBAR_PART330};331}332}333334335