Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/browser/parts/titlebarPart.ts
13395 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import '../../../workbench/browser/parts/titlebar/media/titlebarpart.css';
7
import './media/titlebarpart.css';
8
import { MultiWindowParts, Part } from '../../../workbench/browser/part.js';
9
import { ITitleService } from '../../../workbench/services/title/browser/titleService.js';
10
import { getZoomFactor, isWCOEnabled, getWCOTitlebarAreaRect, isFullscreen, onDidChangeFullscreen } from '../../../base/browser/browser.js';
11
import { hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, TitlebarStyle, getTitleBarStyle, getWindowControlsStyle, WindowControlsStyle } from '../../../platform/window/common/window.js';
12
import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';
13
import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
14
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
15
import { DisposableStore } from '../../../base/common/lifecycle.js';
16
import { IThemeService } from '../../../platform/theme/common/themeService.js';
17
import { agentsPanelForeground } from '../../common/theme.js';
18
import { isMacintosh, isWeb, isNative, platformLocale } from '../../../base/common/platform.js';
19
import { EventType, EventHelper, append, $, addDisposableListener, prepend, getWindow, getWindowId, getContentWidth } from '../../../base/browser/dom.js';
20
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
21
import { Emitter, Event } from '../../../base/common/event.js';
22
import { IStorageService } from '../../../platform/storage/common/storage.js';
23
import { Parts, IWorkbenchLayoutService } from '../../../workbench/services/layout/browser/layoutService.js';
24
25
import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
26
import { IHostService } from '../../../workbench/services/host/browser/host.js';
27
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js';
28
import { IEditorGroupsContainer } from '../../../workbench/services/editor/common/editorGroupsService.js';
29
import { CodeWindow, mainWindow } from '../../../base/browser/window.js';
30
import { safeIntl } from '../../../base/common/date.js';
31
import { ITitlebarPart, ITitleProperties, ITitleVariable, IAuxiliaryTitlebarPart } from '../../../workbench/browser/parts/titlebar/titlebarPart.js';
32
import { Menus } from '../menus.js';
33
34
/**
35
* Simplified agent sessions titlebar part.
36
*
37
* Three sections driven entirely by menus:
38
* - **Left**: `Menus.TitleBarLeft` toolbar
39
* - **Center**: `Menus.CommandCenter` toolbar (renders session picker via IActionViewItemService)
40
* - **Right**: `Menus.TitleBarRight` toolbar (includes account submenu)
41
*
42
* No menubar, no editor actions, no layout controls, no WindowTitle dependency.
43
*/
44
export class TitlebarPart extends Part implements ITitlebarPart {
45
46
//#region IView
47
48
readonly minimumWidth: number = 0;
49
readonly maximumWidth: number = Number.POSITIVE_INFINITY;
50
51
get minimumHeight(): number {
52
const wcoEnabled = isWeb && isWCOEnabled();
53
let value = DEFAULT_CUSTOM_TITLEBAR_HEIGHT;
54
if (wcoEnabled) {
55
value = Math.max(value, getWCOTitlebarAreaRect(getWindow(this.element))?.height ?? 0);
56
}
57
58
return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1);
59
}
60
61
get maximumHeight(): number { return this.minimumHeight; }
62
63
//#endregion
64
65
//#region Events
66
67
private readonly _onMenubarVisibilityChange = this._register(new Emitter<boolean>());
68
readonly onMenubarVisibilityChange = this._onMenubarVisibilityChange.event;
69
70
private readonly _onWillDispose = this._register(new Emitter<void>());
71
readonly onWillDispose = this._onWillDispose.event;
72
73
//#endregion
74
75
private rootContainer!: HTMLElement;
76
private windowControlsContainer: HTMLElement | undefined;
77
78
private leftContent!: HTMLElement;
79
private leftToolbarContainer!: HTMLElement;
80
private centerContent!: HTMLElement;
81
private rightContent!: HTMLElement;
82
83
get leftContainer(): HTMLElement { return this.leftContent; }
84
get rightContainer(): HTMLElement { return this.rightContent; }
85
get rightWindowControlsContainer(): HTMLElement | undefined { return this.windowControlsContainer; }
86
87
private sideBarPartResizeObserver: ResizeObserver | undefined;
88
private leftToolbarContentWidth: number = 0;
89
private lastSideBarWidth: number = 0;
90
private leftSpacerWidth: number = 0;
91
92
private readonly titleBarStyle: TitlebarStyle;
93
private isInactive: boolean = false;
94
95
constructor(
96
id: string,
97
targetWindow: CodeWindow,
98
@IContextMenuService private readonly contextMenuService: IContextMenuService,
99
@IConfigurationService protected readonly configurationService: IConfigurationService,
100
@IInstantiationService protected readonly instantiationService: IInstantiationService,
101
@IThemeService themeService: IThemeService,
102
@IStorageService storageService: IStorageService,
103
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
104
@IContextKeyService private readonly contextKeyService: IContextKeyService,
105
@IHostService private readonly hostService: IHostService,
106
) {
107
super(id, { hasTitle: false }, themeService, storageService, layoutService);
108
109
this.titleBarStyle = getTitleBarStyle(this.configurationService);
110
111
this.registerListeners(getWindowId(targetWindow));
112
}
113
114
private registerListeners(targetWindowId: number): void {
115
this._register(this.hostService.onDidChangeFocus(focused => focused ? this.onFocus() : this.onBlur()));
116
this._register(this.hostService.onDidChangeActiveWindow(windowId => windowId === targetWindowId ? this.onFocus() : this.onBlur()));
117
}
118
119
private onBlur(): void {
120
this.isInactive = true;
121
this.updateStyles();
122
}
123
124
private onFocus(): void {
125
this.isInactive = false;
126
this.updateStyles();
127
}
128
129
updateProperties(_properties: ITitleProperties): void {
130
// No window title to update in simplified titlebar
131
}
132
133
registerVariables(_variables: ITitleVariable[]): void {
134
// No window title variables in simplified titlebar
135
}
136
137
updateOptions(_options: { compact: boolean }): void {
138
// No compact mode support in agent sessions titlebar
139
}
140
141
protected override createContentArea(parent: HTMLElement): HTMLElement {
142
this.element = parent;
143
this.rootContainer = append(parent, $('.titlebar-container.sessions-titlebar-container.has-center'));
144
145
// Draggable region
146
prepend(this.rootContainer, $('div.titlebar-drag-region'));
147
148
this.leftContent = append(this.rootContainer, $('.titlebar-left'));
149
this.centerContent = append(this.rootContainer, $('.titlebar-center'));
150
this.rightContent = append(this.rootContainer, $('.titlebar-right'));
151
152
// Window Controls Container (must be before left toolbar for correct ordering)
153
if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) {
154
let primaryWindowControlsLocation = isMacintosh ? 'left' : 'right';
155
if (isMacintosh && isNative) {
156
const localeInfo = safeIntl.Locale(platformLocale).value;
157
const textInfo = (localeInfo as { textInfo?: { direction?: string } }).textInfo;
158
if (textInfo?.direction === 'rtl') {
159
primaryWindowControlsLocation = 'right';
160
}
161
}
162
163
if (isMacintosh && isNative && primaryWindowControlsLocation === 'left') {
164
// macOS native: traffic lights are rendered by the OS at the top-left corner.
165
// Add a fixed-width spacer to push content past the traffic lights.
166
const spacer = append(this.leftContent, $('div.window-controls-container'));
167
168
// Hide spacer in fullscreen (traffic lights are not shown)
169
const updateSpacerVisibility = () => {
170
const fullscreen = isFullscreen(mainWindow);
171
spacer.style.display = fullscreen ? 'none' : '';
172
this.leftSpacerWidth = fullscreen ? 0 : 70;
173
};
174
updateSpacerVisibility();
175
spacer.style.width = `${this.leftSpacerWidth}px`;
176
spacer.style.flexShrink = '0';
177
this._register(onDidChangeFullscreen(windowId => {
178
if (windowId === getWindowId(mainWindow)) {
179
updateSpacerVisibility();
180
this.updateLeftContentWidth();
181
}
182
}));
183
} else if (getWindowControlsStyle(this.configurationService) === WindowControlsStyle.HIDDEN) {
184
// controls explicitly disabled
185
} else {
186
this.windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container'));
187
if (isWeb) {
188
append(primaryWindowControlsLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container'));
189
}
190
191
if (isWCOEnabled()) {
192
this.windowControlsContainer.classList.add('wco-enabled');
193
}
194
}
195
}
196
197
// Left toolbar (driven by Menus.TitleBarLeft, rendered after window controls via CSS order)
198
this.leftToolbarContainer = append(this.leftContent, $('div.left-toolbar-container'));
199
const leftToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.leftToolbarContainer, Menus.TitleBarLeftLayout, {
200
contextMenu: Menus.TitleBarContext,
201
telemetrySource: 'titlePart.left',
202
hiddenItemStrategy: HiddenItemStrategy.NoHide,
203
toolbarOptions: { primaryGroup: () => true },
204
}));
205
this.leftToolbarContentWidth = getContentWidth(this.leftToolbarContainer);
206
this.updateLeftContentWidth();
207
this._register(leftToolbar.onDidChangeMenuItems(() => {
208
this.leftToolbarContentWidth = getContentWidth(this.leftToolbarContainer);
209
this.updateLeftContentWidth();
210
}));
211
212
// Center toolbar - command center (renders session picker via IActionViewItemService)
213
// Uses .window-title > .command-center nesting to match default workbench CSS selectors
214
const windowTitle = append(this.centerContent, $('div.window-title'));
215
const centerToolbarContainer = append(windowTitle, $('div.command-center'));
216
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, centerToolbarContainer, Menus.CommandCenter, {
217
contextMenu: Menus.TitleBarContext,
218
hiddenItemStrategy: HiddenItemStrategy.NoHide,
219
telemetrySource: 'commandCenter',
220
toolbarOptions: { primaryGroup: () => true },
221
}));
222
223
// Right toolbar (driven by Menus.TitleBarRightLayout - includes layout actions)
224
const rightToolbarContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-right-layout-container'));
225
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRightLayout, {
226
contextMenu: Menus.TitleBarContext,
227
hiddenItemStrategy: HiddenItemStrategy.NoHide,
228
telemetrySource: 'titlePart.right',
229
toolbarOptions: { primaryGroup: () => true },
230
}));
231
232
// Session title actions toolbar (before right toolbar)
233
const sessionActionsContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-session-actions-container'));
234
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, sessionActionsContainer, Menus.TitleBarSessionMenu, {
235
contextMenu: Menus.TitleBarContext,
236
hiddenItemStrategy: HiddenItemStrategy.NoHide,
237
telemetrySource: 'titlePart.sessionActions',
238
toolbarOptions: { primaryGroup: () => true },
239
}));
240
241
// Context menu on the titlebar
242
this._register(addDisposableListener(this.rootContainer, EventType.CONTEXT_MENU, e => {
243
EventHelper.stop(e);
244
this.onContextMenu(e);
245
}));
246
247
this.updateStyles();
248
249
return this.element;
250
}
251
252
override updateStyles(): void {
253
super.updateStyles();
254
255
if (this.element) {
256
this.element.classList.toggle('inactive', this.isInactive);
257
258
// Titlebar is transparent — it inherits the sidebar/gradient background via CSS.
259
// Only set foreground color for text/icon contrast.
260
this.element.style.backgroundColor = '';
261
262
const titleForeground = this.getColor(agentsPanelForeground);
263
this.element.style.color = titleForeground || '';
264
}
265
}
266
267
private onContextMenu(e: MouseEvent): void {
268
const event = new StandardMouseEvent(getWindow(this.element), e);
269
this.contextMenuService.showContextMenu({
270
getAnchor: () => event,
271
menuId: Menus.TitleBarContext,
272
contextKeyService: this.contextKeyService,
273
domForShadowRoot: isMacintosh && isNative ? event.target : undefined
274
});
275
}
276
277
get hasZoomableElements(): boolean {
278
return true; // sessions titlebar always has command center and toolbar actions
279
}
280
281
get preventZoom(): boolean {
282
// Prevent zooming behavior if any of the following conditions are met:
283
// 1. Shrinking below the window control size (zoom < 1)
284
// 2. No custom items are present in the title bar
285
return getZoomFactor(getWindow(this.element)) < 1 || !this.hasZoomableElements;
286
}
287
288
override layout(width: number, height: number): void {
289
this.updateLayout();
290
super.layoutContents(width, height);
291
this.installSideBarPartResizeObserver();
292
}
293
294
private installSideBarPartResizeObserver(): void {
295
if (this.sideBarPartResizeObserver) {
296
return;
297
}
298
299
const sideBarContainer = this.layoutService.getContainer(getWindow(this.element), Parts.SIDEBAR_PART);
300
if (!sideBarContainer) {
301
return;
302
}
303
304
this.sideBarPartResizeObserver = new ResizeObserver(entries => {
305
this.lastSideBarWidth = entries[0].contentRect.width;
306
this.updateLeftContentWidth();
307
});
308
this.sideBarPartResizeObserver.observe(sideBarContainer);
309
this._register({ dispose: () => this.sideBarPartResizeObserver?.disconnect() });
310
}
311
312
private getLeftContentWidth(): number {
313
if (this.leftToolbarContentWidth === 0) {
314
this.leftToolbarContentWidth = getContentWidth(this.leftToolbarContainer);
315
}
316
return this.leftToolbarContentWidth + this.leftSpacerWidth;
317
}
318
319
private updateLeftContentWidth(): void {
320
this.leftContent.style.width = `${Math.max(this.getLeftContentWidth(), this.lastSideBarWidth)}px`;
321
}
322
323
private updateLayout(): void {
324
if (!hasCustomTitlebar(this.configurationService, this.titleBarStyle)) {
325
return;
326
}
327
328
const zoomFactor = getZoomFactor(getWindow(this.element));
329
this.element.style.setProperty('--zoom-factor', zoomFactor.toString());
330
this.rootContainer.classList.toggle('counter-zoom', this.preventZoom);
331
}
332
333
focus(): void {
334
// eslint-disable-next-line no-restricted-syntax
335
(this.element.querySelector('[tabindex]:not([tabindex="-1"])') as HTMLElement | null)?.focus();
336
}
337
338
toJSON(): object {
339
return { type: Parts.TITLEBAR_PART };
340
}
341
342
override dispose(): void {
343
this._onWillDispose.fire();
344
super.dispose();
345
}
346
}
347
348
/**
349
* Main agent sessions titlebar part (for the main window).
350
*/
351
export class MainTitlebarPart extends TitlebarPart {
352
353
constructor(
354
@IContextMenuService contextMenuService: IContextMenuService,
355
@IConfigurationService configurationService: IConfigurationService,
356
@IInstantiationService instantiationService: IInstantiationService,
357
@IThemeService themeService: IThemeService,
358
@IStorageService storageService: IStorageService,
359
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
360
@IContextKeyService contextKeyService: IContextKeyService,
361
@IHostService hostService: IHostService,
362
) {
363
super(Parts.TITLEBAR_PART, mainWindow, contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService);
364
}
365
}
366
367
/**
368
* Auxiliary agent sessions titlebar part (for auxiliary windows).
369
*/
370
export class AuxiliaryTitlebarPart extends TitlebarPart implements IAuxiliaryTitlebarPart {
371
372
private static COUNTER = 1;
373
374
get height() { return this.minimumHeight; }
375
376
constructor(
377
readonly container: HTMLElement,
378
private readonly mainTitlebar: TitlebarPart,
379
@IContextMenuService contextMenuService: IContextMenuService,
380
@IConfigurationService configurationService: IConfigurationService,
381
@IInstantiationService instantiationService: IInstantiationService,
382
@IThemeService themeService: IThemeService,
383
@IStorageService storageService: IStorageService,
384
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
385
@IContextKeyService contextKeyService: IContextKeyService,
386
@IHostService hostService: IHostService,
387
) {
388
const id = AuxiliaryTitlebarPart.COUNTER++;
389
super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService);
390
}
391
392
override get preventZoom(): boolean {
393
// Prevent zooming behavior if any of the following conditions are met:
394
// 1. Shrinking below the window control size (zoom < 1)
395
// 2. No custom items are present in the main title bar
396
// The auxiliary title bar never contains any zoomable items itself,
397
// but we want to match the behavior of the main title bar.
398
return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;
399
}
400
}
401
402
/**
403
* Agent Sessions title service - manages the titlebar parts.
404
*/
405
export class TitleService extends MultiWindowParts<TitlebarPart> implements ITitleService {
406
407
declare _serviceBrand: undefined;
408
409
readonly mainPart: TitlebarPart;
410
411
constructor(
412
@IInstantiationService protected readonly instantiationService: IInstantiationService,
413
@IStorageService storageService: IStorageService,
414
@IThemeService themeService: IThemeService
415
) {
416
super('workbench.agentSessionsTitleService', themeService, storageService);
417
418
this.mainPart = this._register(this.createMainTitlebarPart());
419
this.onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange;
420
this._register(this.registerPart(this.mainPart));
421
}
422
423
protected createMainTitlebarPart(): TitlebarPart {
424
return this.instantiationService.createInstance(MainTitlebarPart);
425
}
426
427
//#region Auxiliary Titlebar Parts
428
429
createAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): IAuxiliaryTitlebarPart {
430
const titlebarPartContainer = $('.part.titlebar', { role: 'none' });
431
titlebarPartContainer.style.position = 'relative';
432
container.insertBefore(titlebarPartContainer, container.firstChild);
433
434
const disposables = new DisposableStore();
435
436
const titlebarPart = this.doCreateAuxiliaryTitlebarPart(titlebarPartContainer, editorGroupsContainer, instantiationService);
437
disposables.add(this.registerPart(titlebarPart));
438
439
disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`));
440
titlebarPart.create(titlebarPartContainer);
441
442
Event.once(titlebarPart.onWillDispose)(() => disposables.dispose());
443
444
return titlebarPart;
445
}
446
447
protected doCreateAuxiliaryTitlebarPart(container: HTMLElement, _editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): TitlebarPart & IAuxiliaryTitlebarPart {
448
return instantiationService.createInstance(AuxiliaryTitlebarPart, container, this.mainPart);
449
}
450
451
//#endregion
452
453
//#region Service Implementation
454
455
readonly onMenubarVisibilityChange: Event<boolean>;
456
457
updateProperties(properties: ITitleProperties): void {
458
for (const part of this.parts) {
459
part.updateProperties(properties);
460
}
461
}
462
463
registerVariables(variables: ITitleVariable[]): void {
464
for (const part of this.parts) {
465
part.registerVariables(variables);
466
}
467
}
468
469
//#endregion
470
}
471
472