Path: blob/main/src/vs/sessions/browser/parts/titlebarPart.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/titlebar/media/titlebarpart.css';6import './media/titlebarpart.css';7import { MultiWindowParts, Part } from '../../../workbench/browser/part.js';8import { ITitleService } from '../../../workbench/services/title/browser/titleService.js';9import { getZoomFactor, isWCOEnabled, getWCOTitlebarAreaRect, isFullscreen, onDidChangeFullscreen } from '../../../base/browser/browser.js';10import { hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, TitlebarStyle, getTitleBarStyle, getWindowControlsStyle, WindowControlsStyle } from '../../../platform/window/common/window.js';11import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';12import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';13import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';14import { DisposableStore } from '../../../base/common/lifecycle.js';15import { IThemeService } from '../../../platform/theme/common/themeService.js';16import { agentsPanelForeground } from '../../common/theme.js';17import { isMacintosh, isWeb, isNative, platformLocale } from '../../../base/common/platform.js';18import { EventType, EventHelper, append, $, addDisposableListener, prepend, getWindow, getWindowId, getContentWidth } from '../../../base/browser/dom.js';19import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';20import { Emitter, Event } from '../../../base/common/event.js';21import { IStorageService } from '../../../platform/storage/common/storage.js';22import { Parts, IWorkbenchLayoutService } from '../../../workbench/services/layout/browser/layoutService.js';2324import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';25import { IHostService } from '../../../workbench/services/host/browser/host.js';26import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js';27import { IEditorGroupsContainer } from '../../../workbench/services/editor/common/editorGroupsService.js';28import { CodeWindow, mainWindow } from '../../../base/browser/window.js';29import { safeIntl } from '../../../base/common/date.js';30import { ITitlebarPart, ITitleProperties, ITitleVariable, IAuxiliaryTitlebarPart } from '../../../workbench/browser/parts/titlebar/titlebarPart.js';31import { Menus } from '../menus.js';3233/**34* Simplified agent sessions titlebar part.35*36* Three sections driven entirely by menus:37* - **Left**: `Menus.TitleBarLeft` toolbar38* - **Center**: `Menus.CommandCenter` toolbar (renders session picker via IActionViewItemService)39* - **Right**: `Menus.TitleBarRight` toolbar (includes account submenu)40*41* No menubar, no editor actions, no layout controls, no WindowTitle dependency.42*/43export class TitlebarPart extends Part implements ITitlebarPart {4445//#region IView4647readonly minimumWidth: number = 0;48readonly maximumWidth: number = Number.POSITIVE_INFINITY;4950get minimumHeight(): number {51const wcoEnabled = isWeb && isWCOEnabled();52let value = DEFAULT_CUSTOM_TITLEBAR_HEIGHT;53if (wcoEnabled) {54value = Math.max(value, getWCOTitlebarAreaRect(getWindow(this.element))?.height ?? 0);55}5657return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1);58}5960get maximumHeight(): number { return this.minimumHeight; }6162//#endregion6364//#region Events6566private readonly _onMenubarVisibilityChange = this._register(new Emitter<boolean>());67readonly onMenubarVisibilityChange = this._onMenubarVisibilityChange.event;6869private readonly _onWillDispose = this._register(new Emitter<void>());70readonly onWillDispose = this._onWillDispose.event;7172//#endregion7374private rootContainer!: HTMLElement;75private windowControlsContainer: HTMLElement | undefined;7677private leftContent!: HTMLElement;78private leftToolbarContainer!: HTMLElement;79private centerContent!: HTMLElement;80private rightContent!: HTMLElement;8182get leftContainer(): HTMLElement { return this.leftContent; }83get rightContainer(): HTMLElement { return this.rightContent; }84get rightWindowControlsContainer(): HTMLElement | undefined { return this.windowControlsContainer; }8586private sideBarPartResizeObserver: ResizeObserver | undefined;87private leftToolbarContentWidth: number = 0;88private lastSideBarWidth: number = 0;89private leftSpacerWidth: number = 0;9091private readonly titleBarStyle: TitlebarStyle;92private isInactive: boolean = false;9394constructor(95id: string,96targetWindow: CodeWindow,97@IContextMenuService private readonly contextMenuService: IContextMenuService,98@IConfigurationService protected readonly configurationService: IConfigurationService,99@IInstantiationService protected readonly instantiationService: IInstantiationService,100@IThemeService themeService: IThemeService,101@IStorageService storageService: IStorageService,102@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,103@IContextKeyService private readonly contextKeyService: IContextKeyService,104@IHostService private readonly hostService: IHostService,105) {106super(id, { hasTitle: false }, themeService, storageService, layoutService);107108this.titleBarStyle = getTitleBarStyle(this.configurationService);109110this.registerListeners(getWindowId(targetWindow));111}112113private registerListeners(targetWindowId: number): void {114this._register(this.hostService.onDidChangeFocus(focused => focused ? this.onFocus() : this.onBlur()));115this._register(this.hostService.onDidChangeActiveWindow(windowId => windowId === targetWindowId ? this.onFocus() : this.onBlur()));116}117118private onBlur(): void {119this.isInactive = true;120this.updateStyles();121}122123private onFocus(): void {124this.isInactive = false;125this.updateStyles();126}127128updateProperties(_properties: ITitleProperties): void {129// No window title to update in simplified titlebar130}131132registerVariables(_variables: ITitleVariable[]): void {133// No window title variables in simplified titlebar134}135136updateOptions(_options: { compact: boolean }): void {137// No compact mode support in agent sessions titlebar138}139140protected override createContentArea(parent: HTMLElement): HTMLElement {141this.element = parent;142this.rootContainer = append(parent, $('.titlebar-container.sessions-titlebar-container.has-center'));143144// Draggable region145prepend(this.rootContainer, $('div.titlebar-drag-region'));146147this.leftContent = append(this.rootContainer, $('.titlebar-left'));148this.centerContent = append(this.rootContainer, $('.titlebar-center'));149this.rightContent = append(this.rootContainer, $('.titlebar-right'));150151// Window Controls Container (must be before left toolbar for correct ordering)152if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) {153let primaryWindowControlsLocation = isMacintosh ? 'left' : 'right';154if (isMacintosh && isNative) {155const localeInfo = safeIntl.Locale(platformLocale).value;156const textInfo = (localeInfo as { textInfo?: { direction?: string } }).textInfo;157if (textInfo?.direction === 'rtl') {158primaryWindowControlsLocation = 'right';159}160}161162if (isMacintosh && isNative && primaryWindowControlsLocation === 'left') {163// macOS native: traffic lights are rendered by the OS at the top-left corner.164// Add a fixed-width spacer to push content past the traffic lights.165const spacer = append(this.leftContent, $('div.window-controls-container'));166167// Hide spacer in fullscreen (traffic lights are not shown)168const updateSpacerVisibility = () => {169const fullscreen = isFullscreen(mainWindow);170spacer.style.display = fullscreen ? 'none' : '';171this.leftSpacerWidth = fullscreen ? 0 : 70;172};173updateSpacerVisibility();174spacer.style.width = `${this.leftSpacerWidth}px`;175spacer.style.flexShrink = '0';176this._register(onDidChangeFullscreen(windowId => {177if (windowId === getWindowId(mainWindow)) {178updateSpacerVisibility();179this.updateLeftContentWidth();180}181}));182} else if (getWindowControlsStyle(this.configurationService) === WindowControlsStyle.HIDDEN) {183// controls explicitly disabled184} else {185this.windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container'));186if (isWeb) {187append(primaryWindowControlsLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container'));188}189190if (isWCOEnabled()) {191this.windowControlsContainer.classList.add('wco-enabled');192}193}194}195196// Left toolbar (driven by Menus.TitleBarLeft, rendered after window controls via CSS order)197this.leftToolbarContainer = append(this.leftContent, $('div.left-toolbar-container'));198const leftToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.leftToolbarContainer, Menus.TitleBarLeftLayout, {199contextMenu: Menus.TitleBarContext,200telemetrySource: 'titlePart.left',201hiddenItemStrategy: HiddenItemStrategy.NoHide,202toolbarOptions: { primaryGroup: () => true },203}));204this.leftToolbarContentWidth = getContentWidth(this.leftToolbarContainer);205this.updateLeftContentWidth();206this._register(leftToolbar.onDidChangeMenuItems(() => {207this.leftToolbarContentWidth = getContentWidth(this.leftToolbarContainer);208this.updateLeftContentWidth();209}));210211// Center toolbar - command center (renders session picker via IActionViewItemService)212// Uses .window-title > .command-center nesting to match default workbench CSS selectors213const windowTitle = append(this.centerContent, $('div.window-title'));214const centerToolbarContainer = append(windowTitle, $('div.command-center'));215this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, centerToolbarContainer, Menus.CommandCenter, {216contextMenu: Menus.TitleBarContext,217hiddenItemStrategy: HiddenItemStrategy.NoHide,218telemetrySource: 'commandCenter',219toolbarOptions: { primaryGroup: () => true },220}));221222// Right toolbar (driven by Menus.TitleBarRightLayout - includes layout actions)223const rightToolbarContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-right-layout-container'));224this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRightLayout, {225contextMenu: Menus.TitleBarContext,226hiddenItemStrategy: HiddenItemStrategy.NoHide,227telemetrySource: 'titlePart.right',228toolbarOptions: { primaryGroup: () => true },229}));230231// Session title actions toolbar (before right toolbar)232const sessionActionsContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-session-actions-container'));233this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, sessionActionsContainer, Menus.TitleBarSessionMenu, {234contextMenu: Menus.TitleBarContext,235hiddenItemStrategy: HiddenItemStrategy.NoHide,236telemetrySource: 'titlePart.sessionActions',237toolbarOptions: { primaryGroup: () => true },238}));239240// Context menu on the titlebar241this._register(addDisposableListener(this.rootContainer, EventType.CONTEXT_MENU, e => {242EventHelper.stop(e);243this.onContextMenu(e);244}));245246this.updateStyles();247248return this.element;249}250251override updateStyles(): void {252super.updateStyles();253254if (this.element) {255this.element.classList.toggle('inactive', this.isInactive);256257// Titlebar is transparent — it inherits the sidebar/gradient background via CSS.258// Only set foreground color for text/icon contrast.259this.element.style.backgroundColor = '';260261const titleForeground = this.getColor(agentsPanelForeground);262this.element.style.color = titleForeground || '';263}264}265266private onContextMenu(e: MouseEvent): void {267const event = new StandardMouseEvent(getWindow(this.element), e);268this.contextMenuService.showContextMenu({269getAnchor: () => event,270menuId: Menus.TitleBarContext,271contextKeyService: this.contextKeyService,272domForShadowRoot: isMacintosh && isNative ? event.target : undefined273});274}275276get hasZoomableElements(): boolean {277return true; // sessions titlebar always has command center and toolbar actions278}279280get preventZoom(): boolean {281// Prevent zooming behavior if any of the following conditions are met:282// 1. Shrinking below the window control size (zoom < 1)283// 2. No custom items are present in the title bar284return getZoomFactor(getWindow(this.element)) < 1 || !this.hasZoomableElements;285}286287override layout(width: number, height: number): void {288this.updateLayout();289super.layoutContents(width, height);290this.installSideBarPartResizeObserver();291}292293private installSideBarPartResizeObserver(): void {294if (this.sideBarPartResizeObserver) {295return;296}297298const sideBarContainer = this.layoutService.getContainer(getWindow(this.element), Parts.SIDEBAR_PART);299if (!sideBarContainer) {300return;301}302303this.sideBarPartResizeObserver = new ResizeObserver(entries => {304this.lastSideBarWidth = entries[0].contentRect.width;305this.updateLeftContentWidth();306});307this.sideBarPartResizeObserver.observe(sideBarContainer);308this._register({ dispose: () => this.sideBarPartResizeObserver?.disconnect() });309}310311private getLeftContentWidth(): number {312if (this.leftToolbarContentWidth === 0) {313this.leftToolbarContentWidth = getContentWidth(this.leftToolbarContainer);314}315return this.leftToolbarContentWidth + this.leftSpacerWidth;316}317318private updateLeftContentWidth(): void {319this.leftContent.style.width = `${Math.max(this.getLeftContentWidth(), this.lastSideBarWidth)}px`;320}321322private updateLayout(): void {323if (!hasCustomTitlebar(this.configurationService, this.titleBarStyle)) {324return;325}326327const zoomFactor = getZoomFactor(getWindow(this.element));328this.element.style.setProperty('--zoom-factor', zoomFactor.toString());329this.rootContainer.classList.toggle('counter-zoom', this.preventZoom);330}331332focus(): void {333// eslint-disable-next-line no-restricted-syntax334(this.element.querySelector('[tabindex]:not([tabindex="-1"])') as HTMLElement | null)?.focus();335}336337toJSON(): object {338return { type: Parts.TITLEBAR_PART };339}340341override dispose(): void {342this._onWillDispose.fire();343super.dispose();344}345}346347/**348* Main agent sessions titlebar part (for the main window).349*/350export class MainTitlebarPart extends TitlebarPart {351352constructor(353@IContextMenuService contextMenuService: IContextMenuService,354@IConfigurationService configurationService: IConfigurationService,355@IInstantiationService instantiationService: IInstantiationService,356@IThemeService themeService: IThemeService,357@IStorageService storageService: IStorageService,358@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,359@IContextKeyService contextKeyService: IContextKeyService,360@IHostService hostService: IHostService,361) {362super(Parts.TITLEBAR_PART, mainWindow, contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService);363}364}365366/**367* Auxiliary agent sessions titlebar part (for auxiliary windows).368*/369export class AuxiliaryTitlebarPart extends TitlebarPart implements IAuxiliaryTitlebarPart {370371private static COUNTER = 1;372373get height() { return this.minimumHeight; }374375constructor(376readonly container: HTMLElement,377private readonly mainTitlebar: TitlebarPart,378@IContextMenuService contextMenuService: IContextMenuService,379@IConfigurationService configurationService: IConfigurationService,380@IInstantiationService instantiationService: IInstantiationService,381@IThemeService themeService: IThemeService,382@IStorageService storageService: IStorageService,383@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,384@IContextKeyService contextKeyService: IContextKeyService,385@IHostService hostService: IHostService,386) {387const id = AuxiliaryTitlebarPart.COUNTER++;388super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService);389}390391override get preventZoom(): boolean {392// Prevent zooming behavior if any of the following conditions are met:393// 1. Shrinking below the window control size (zoom < 1)394// 2. No custom items are present in the main title bar395// The auxiliary title bar never contains any zoomable items itself,396// but we want to match the behavior of the main title bar.397return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;398}399}400401/**402* Agent Sessions title service - manages the titlebar parts.403*/404export class TitleService extends MultiWindowParts<TitlebarPart> implements ITitleService {405406declare _serviceBrand: undefined;407408readonly mainPart: TitlebarPart;409410constructor(411@IInstantiationService protected readonly instantiationService: IInstantiationService,412@IStorageService storageService: IStorageService,413@IThemeService themeService: IThemeService414) {415super('workbench.agentSessionsTitleService', themeService, storageService);416417this.mainPart = this._register(this.createMainTitlebarPart());418this.onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange;419this._register(this.registerPart(this.mainPart));420}421422protected createMainTitlebarPart(): TitlebarPart {423return this.instantiationService.createInstance(MainTitlebarPart);424}425426//#region Auxiliary Titlebar Parts427428createAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): IAuxiliaryTitlebarPart {429const titlebarPartContainer = $('.part.titlebar', { role: 'none' });430titlebarPartContainer.style.position = 'relative';431container.insertBefore(titlebarPartContainer, container.firstChild);432433const disposables = new DisposableStore();434435const titlebarPart = this.doCreateAuxiliaryTitlebarPart(titlebarPartContainer, editorGroupsContainer, instantiationService);436disposables.add(this.registerPart(titlebarPart));437438disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`));439titlebarPart.create(titlebarPartContainer);440441Event.once(titlebarPart.onWillDispose)(() => disposables.dispose());442443return titlebarPart;444}445446protected doCreateAuxiliaryTitlebarPart(container: HTMLElement, _editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): TitlebarPart & IAuxiliaryTitlebarPart {447return instantiationService.createInstance(AuxiliaryTitlebarPart, container, this.mainPart);448}449450//#endregion451452//#region Service Implementation453454readonly onMenubarVisibilityChange: Event<boolean>;455456updateProperties(properties: ITitleProperties): void {457for (const part of this.parts) {458part.updateProperties(properties);459}460}461462registerVariables(variables: ITitleVariable[]): void {463for (const part of this.parts) {464part.registerVariables(variables);465}466}467468//#endregion469}470471472