From 7ff71ee94eaafe8fabbe6a9ab29c2dde1222eb41 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 3 Sep 2026 10:07:44 +0100 Subject: [PATCH] feat: enhance editor and chat UI with new styles and layout adjustments --- .../lib/stylelint/vscode-known-variables.json | 2 + .../browser/parts/media/chatCompositeBar.css | 4 +- .../browser/parts/media/editorPart.css | 55 +++++++++++++++++++ .../contrib/editor/browser/addTabActions.ts | 2 +- .../editor/browser/media/editorHeader.css | 6 +- .../test/browser/editor.contribution.test.ts | 28 +++++++++- .../test/browser/editorHeader.fixture.ts | 7 ++- .../test/browser/chatCompositeBar.test.ts | 27 +++++++-- .../parts/editor/editorHeaderControl.ts | 10 +++- .../browser/parts/editor/editorTabsControl.ts | 4 +- .../contrib/modernUI/browser/media/tabs.css | 20 ++++++- .../browser/media/searchEditor.css | 1 + .../searchEditor/browser/searchEditor.ts | 9 ++- 13 files changed, 155 insertions(+), 20 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index abd33957465c..dd215360b803 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1092,6 +1092,7 @@ "--modern-ui-editor-tab-unfocused-hover-foreground", "--modern-ui-editor-tab-unfocused-inactive-background", "--modern-ui-editor-tab-unfocused-inactive-foreground", + "--modern-ui-editor-tabs-border", "--modern-ui-floating-card-border-color", "--modern-ui-floating-card-corner-bottom-left-border-image", "--modern-ui-floating-card-corner-bottom-right-border-image", @@ -1114,6 +1115,7 @@ "--modern-ui-shell-background", "--modern-ui-tab-active-background", "--modern-ui-tab-hover-background", + "--search-editor-query-layout-offset", "--scroll-shadow-surface", "--vscode-chat-list-background", "--vscode-chat-persistent-content-height", diff --git a/src/vs/sessions/browser/parts/media/chatCompositeBar.css b/src/vs/sessions/browser/parts/media/chatCompositeBar.css index c4f5d2bc3fb3..9d68823d196f 100644 --- a/src/vs/sessions/browser/parts/media/chatCompositeBar.css +++ b/src/vs/sessions/browser/parts/media/chatCompositeBar.css @@ -20,7 +20,7 @@ /* Tabs host: the chat tab strip, shown only when the session has multiple chats. */ .chat-composite-bar.session-chat-tabs-bar { - padding: 0 var(--vscode-spacing-size100); + padding: 0 var(--vscode-spacing-size20); box-sizing: border-box; container-type: inline-size; @@ -165,7 +165,7 @@ .chat-composite-bar-tabs-row { display: flex; align-items: center; - height: 35px; + height: var(--vscode-spacing-size320, 32px); box-sizing: border-box; overflow: hidden; } diff --git a/src/vs/sessions/browser/parts/media/editorPart.css b/src/vs/sessions/browser/parts/media/editorPart.css index 1d11337194c6..d87e702cc265 100644 --- a/src/vs/sessions/browser/parts/media/editorPart.css +++ b/src/vs/sessions/browser/parts/media/editorPart.css @@ -21,6 +21,61 @@ overflow: hidden; } +.agent-sessions-workbench.dock-detail-panel .part.editor .editor-group-container > .title.tabs { + --modern-ui-editor-tabs-border: var(--vscode-editorGroupHeader-tabsBorder, var(--vscode-editorGroup-border)); +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .browser-root > .browser-navbar { + box-sizing: border-box; + min-height: var(--vscode-spacing-size320); + padding: var(--vscode-spacing-size40); + gap: var(--vscode-spacing-size40); + border-bottom: var(--vscode-strokeThickness) solid var(--vscode-editorGroupHeader-tabsBorder, var(--vscode-editorGroup-border)); +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .editor-tabs-compact-height .browser-root > .browser-navbar { + min-height: var(--vscode-spacing-size280); + padding-block: var(--vscode-spacing-size20); +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .search-editor > .query-container { + --search-editor-query-layout-offset: var(--vscode-spacing-size80, 8px); + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + column-gap: var(--vscode-spacing-size40); + box-sizing: border-box; + min-height: var(--vscode-spacing-size320); + margin: 0; + padding: var(--vscode-spacing-size20) var(--vscode-spacing-size40); + border-bottom: var(--vscode-strokeThickness) solid var(--vscode-editorGroupHeader-tabsBorder, var(--vscode-editorGroup-border)); +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .editor-tabs-compact-height .search-editor > .query-container { + min-height: var(--vscode-spacing-size280); + padding-block: var(--vscode-spacing-sizeNone); +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .search-editor > .query-container > .search-widget { + min-width: 0; +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .search-editor > .query-container > .includes-excludes { + min-height: 0; +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .search-editor > .query-container > .includes-excludes:not(.expanded) > .expand { + position: static; + display: flex; + align-items: center; + justify-content: center; +} + +.agent-sessions-workbench.dock-detail-panel .part.editor .search-editor > .query-container > .includes-excludes.expanded { + grid-column: 1 / -1; + width: 100%; +} + /* Editor Layout Actions Toolbar */ .agent-sessions-workbench .part.editor > .content .editor-group-container > .title .editor-actions { diff --git a/src/vs/sessions/contrib/editor/browser/addTabActions.ts b/src/vs/sessions/contrib/editor/browser/addTabActions.ts index b0462f1d840f..6a11bc623607 100644 --- a/src/vs/sessions/contrib/editor/browser/addTabActions.ts +++ b/src/vs/sessions/contrib/editor/browser/addTabActions.ts @@ -122,7 +122,7 @@ export class NewBrowserTabAction extends Action2 { const editorService = accessor.get(IEditorService); const browserInput = browserViewWorkbenchService.getOrCreateLazy({ id: generateUuid() }); - await editorService.openEditor(browserInput); + await editorService.openEditor(browserInput, { pinned: true }); } } diff --git a/src/vs/sessions/contrib/editor/browser/media/editorHeader.css b/src/vs/sessions/contrib/editor/browser/media/editorHeader.css index 9c40d41d995b..1ee2b594d175 100644 --- a/src/vs/sessions/contrib/editor/browser/media/editorHeader.css +++ b/src/vs/sessions/contrib/editor/browser/media/editorHeader.css @@ -22,11 +22,15 @@ align-items: center; box-sizing: border-box; width: 100%; - min-height: 29px; + min-height: var(--vscode-spacing-size320); overflow: hidden; padding: var(--vscode-spacing-size20, 2px) var(--vscode-spacing-size40, 4px); } +.agent-sessions-workbench.dock-detail-panel .part.editor .editor-group-container.editor-tabs-compact-height .title > .editor-group-header { + min-height: var(--vscode-spacing-size280); +} + .agent-sessions-workbench.dock-detail-panel .part.editor .editor-group-header-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; diff --git a/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts b/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts index 11c8f1ebe22a..5de93f145845 100644 --- a/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts +++ b/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts @@ -27,6 +27,8 @@ import { IEditorService } from '../../../../../workbench/services/editor/common/ import { IEditorGroup, IEditorGroupsService } from '../../../../../workbench/services/editor/common/editorGroupsService.js'; import { generateColorThemeCSS } from '../../../../../workbench/services/themes/browser/colorThemeCss.js'; import { ColorThemeData } from '../../../../../workbench/services/themes/common/colorThemeData.js'; +import { BrowserEditorInput } from '../../../../../workbench/contrib/browserView/common/browserEditorInput.js'; +import { IBrowserViewWorkbenchService } from '../../../../../workbench/contrib/browserView/common/browserView.js'; import { TERMINAL_VIEW_ID } from '../../../../../workbench/contrib/terminal/common/terminal.js'; import { openNewSearchEditor } from '../../../../../workbench/contrib/searchEditor/browser/searchEditorActions.js'; import { IAgentWorkbenchLayoutService } from '../../../../browser/workbench.js'; @@ -34,7 +36,7 @@ import { ISessionWorkspace } from '../../../../services/sessions/common/session. import { IActiveSession } from '../../../../services/sessions/common/sessionsManagement.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; import { ISessionChangesService } from '../../../changes/browser/sessionChangesService.js'; -import { NewChangesTabAction, NewFileTabAction, NewSearchTabAction } from '../../browser/addTabActions.js'; +import { NewBrowserTabAction, NewChangesTabAction, NewFileTabAction, NewSearchTabAction } from '../../browser/addTabActions.js'; import { EmptyFileEditorInput, EmptyFileEditorSerializer } from '../../browser/emptyFileEditorInput.js'; import { EditorTabsVisibleContext, IsAuxiliaryWindowContext, IsSessionsWindowContext, IsTopRightEditorGroupContext } from '../../../../../workbench/common/contextkeys.js'; import { TestEnvironmentService } from '../../../../../workbench/test/browser/workbenchTestServices.js'; @@ -123,6 +125,30 @@ suite('Sessions - Editor Contribution', () => { })), [{ isEmptyFileEditor: true, resource: workspaceFolder.toString(), pinned: true, index: 7 }]); }); + test('new browser tab action opens a pinned browser editor', async () => { + const instantiationService = store.add(new TestInstantiationService()); + const browserInput = new class extends mock() { }; + const opened: { editor: unknown; options: IEditorOptions | undefined }[] = []; + instantiationService.stub(IBrowserViewWorkbenchService, new class extends mock() { + override getOrCreateLazy(): BrowserEditorInput { + return browserInput; + } + }); + instantiationService.stub(IEditorService, new class extends mock() { + override async openEditor(...args: unknown[]): Promise { + opened.push({ editor: args[0], options: args[1] as IEditorOptions | undefined }); + return undefined; + } + }); + + await new NewBrowserTabAction().run(instantiationService); + + assert.deepStrictEqual(opened.map(({ editor, options }) => ({ + isBrowserEditor: editor === browserInput, + pinned: options?.pinned, + })), [{ isBrowserEditor: true, pinned: true }]); + }); + test('Add Tab menu stays available in dock-only mode', () => { const getWhen = (action: NewFileTabAction | NewChangesTabAction | NewSearchTabAction): ContextKeyExpression => { const menu = action.desc.menu; diff --git a/src/vs/sessions/contrib/editor/test/browser/editorHeader.fixture.ts b/src/vs/sessions/contrib/editor/test/browser/editorHeader.fixture.ts index 721aca0ecdce..779ff7f2f9a8 100644 --- a/src/vs/sessions/contrib/editor/test/browser/editorHeader.fixture.ts +++ b/src/vs/sessions/contrib/editor/test/browser/editorHeader.fixture.ts @@ -80,12 +80,12 @@ MenuRegistry.appendMenuItem(addTabMenu, { group: 'navigation', }); -function renderHeader(ctx: ComponentFixtureContext, breadcrumbs: boolean, primaryAction: boolean, secondaryAction = false, layoutActions = false, showTabs: 'multiple' | 'single' | 'none' = 'multiple', addTab = false): void { +function renderHeader(ctx: ComponentFixtureContext, breadcrumbs: boolean, primaryAction: boolean, secondaryAction = false, layoutActions = false, showTabs: 'multiple' | 'single' | 'none' = 'multiple', addTab = false, tabHeight: 'default' | 'compact' = 'default'): void { ctx.container.classList.add('agent-sessions-workbench', 'dock-detail-panel'); renderEditorTabBarFixture(ctx, { modernUI: true, - partOptions: { showTabs }, + partOptions: { showTabs, tabHeight }, breadcrumbs: breadcrumbs ? { filePath: 'on', icons: true } : undefined, showHeader: true, headerMenuIds: { @@ -98,7 +98,8 @@ function renderHeader(ctx: ComponentFixtureContext, breadcrumbs: boolean, primar } export default defineThemedFixtureGroup({ path: 'sessions/editorHeader/' }, { - FullHeader: defineComponentFixture({ render: ctx => renderHeader(ctx, true, true, true, true) }), + FullHeader: defineComponentFixture({ render: ctx => renderHeader(ctx, true, true, true, true), additionalThemes: ['darkHighContrast'] }), + CompactFullHeader: defineComponentFixture({ render: ctx => renderHeader(ctx, true, true, true, true, 'multiple', false, 'compact'), additionalThemes: ['darkHighContrast'] }), BreadcrumbsAndAction: defineComponentFixture({ render: ctx => renderHeader(ctx, true, true) }), BreadcrumbsAndSecondaryAction: defineComponentFixture({ render: ctx => renderHeader(ctx, true, false, true) }), BreadcrumbsOnly: defineComponentFixture({ render: ctx => renderHeader(ctx, true, false) }), diff --git a/src/vs/sessions/test/browser/chatCompositeBar.test.ts b/src/vs/sessions/test/browser/chatCompositeBar.test.ts index 67cc2bbe1d07..ea43c0d47645 100644 --- a/src/vs/sessions/test/browser/chatCompositeBar.test.ts +++ b/src/vs/sessions/test/browser/chatCompositeBar.test.ts @@ -110,6 +110,7 @@ interface IChatCompositeBarHarness { readonly commandService: TestCommandService; readonly sessionsService: TestSessionsService; readonly bar: ChatCompositeBar; + readonly container: HTMLElement; readonly session: IActiveSession; readonly tabs: readonly HTMLElement[]; readonly chats: ISettableObservable; @@ -158,7 +159,7 @@ function createHarness(disposables: Pick, options?: { re container.appendChild(bar.element); const tabs = Array.from(bar.element.querySelectorAll('.chat-composite-bar-tab')); - return { store, instantiationService, commandService, sessionsService, bar, session, tabs, chats, activeChatResource, visible, showSessionActions }; + return { store, instantiationService, commandService, sessionsService, bar, container, session, tabs, chats, activeChatResource, visible, showSessionActions }; } suite('Sessions - ChatCompositeBar', () => { @@ -191,6 +192,24 @@ suite('Sessions - ChatCompositeBar', () => { assert.strictEqual(bar.element.querySelector('.chat-composite-bar-new-chat'), null); }); + test('matches the editor tab strip height', () => { + const { bar, container } = createHarness(disposables); + mainWindow.document.body.appendChild(container); + + try { + const tabsRow = bar.element.querySelector('.chat-composite-bar-tabs-row'); + assert.deepStrictEqual({ + barHeight: mainWindow.getComputedStyle(bar.element).height, + tabsRowHeight: tabsRow && mainWindow.getComputedStyle(tabsRow).height, + }, { + barHeight: '32px', + tabsRowHeight: '32px', + }); + } finally { + container.remove(); + } + }); + test('updates active, visibility, and session action state without rebuilding tabs', () => { const { activeChatResource, bar, showSessionActions, tabs, visible } = createHarness(disposables); const secondaryResource = tabs[1].dataset.chatResource!; @@ -236,8 +255,8 @@ suite('Sessions - ChatCompositeBar', () => { const observedHeights: number[] = []; disposables.add(bar.onDidChangeHeight(() => observedHeights.push(bar.height))); - resizeObserver.fire(35); - resizeObserver.fire(35); + resizeObserver.fire(32); + resizeObserver.fire(32); resizeObserver.fire(0); assert.deepStrictEqual({ @@ -246,7 +265,7 @@ suite('Sessions - ChatCompositeBar', () => { observedBox: resizeObserver.observedBox, }, { height: 0, - observedHeights: [35, 0], + observedHeights: [32, 0], observedBox: 'border-box', }); }); diff --git a/src/vs/workbench/browser/parts/editor/editorHeaderControl.ts b/src/vs/workbench/browser/parts/editor/editorHeaderControl.ts index b2179a51f5f7..26880bd64375 100644 --- a/src/vs/workbench/browser/parts/editor/editorHeaderControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorHeaderControl.ts @@ -14,7 +14,8 @@ import { IEditorGroupMenuIds, IEditorGroupsView, IEditorGroupView } from './edit export class EditorHeaderControl extends Disposable { - static readonly HEIGHT = 29; + static readonly DEFAULT_HEIGHT = 32; + static readonly COMPACT_HEIGHT = 28; private readonly headerContainer: HTMLElement | undefined; private readonly actionsContainer: HTMLElement | undefined; @@ -33,7 +34,10 @@ export class EditorHeaderControl extends Disposable { get height(): number { if (this.headerContainer) { - return this.visible ? EditorHeaderControl.HEIGHT : 0; + if (!this.visible) { + return 0; + } + return this.groupsView.partOptions.tabHeight === 'compact' ? EditorHeaderControl.COMPACT_HEIGHT : EditorHeaderControl.DEFAULT_HEIGHT; } return this.breadcrumbsControl?.isHidden() === false ? BreadcrumbsControl.HEIGHT : 0; } @@ -41,7 +45,7 @@ export class EditorHeaderControl extends Disposable { constructor( parent: HTMLElement, private readonly groupView: IEditorGroupView, - groupsView: IEditorGroupsView, + private readonly groupsView: IEditorGroupsView, private readonly menuIds: IEditorGroupMenuIds | undefined, showHeader: boolean, @IInstantiationService private readonly instantiationService: IInstantiationService, diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index c5487ffa306e..b5ae95312685 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -646,10 +646,12 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC } protected updateTabHeight(): void { + const isCompact = this.groupsView.partOptions.tabHeight === 'compact'; this.parent.style.setProperty('--editor-group-tab-height', `${this.tabHeight}px`); // Signal compact mode via a CSS class so the modern tab rules in tabs.css // can apply a proportionally smaller --editor-group-tab-height value. - this.parent.classList.toggle('compact-height', this.groupsView.partOptions.tabHeight === 'compact'); + this.parent.classList.toggle('compact-height', isCompact); + this.parent.parentElement?.classList.toggle('editor-tabs-compact-height', isCompact); } private updateTabActionSpaceReservation(): void { diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index 190b96849a8e..728f40291c75 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css @@ -46,6 +46,7 @@ --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-action-hover-background); --modern-ui-editor-tab-action-active-hover-background: var(--vscode-modernEditorTab-activeHoverActionBackground); --modern-ui-editor-tab-action-unfocused-active-hover-background: var(--modern-ui-editor-tab-action-active-hover-background); + --modern-ui-editor-tabs-border: transparent; } .modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs { @@ -188,11 +189,28 @@ /* Center tabs vertically and strip the bottom border so the pills float. */ .modern-ui-tabs .part.editor .tabs-and-actions-container { - --tabs-border-bottom-color: transparent !important; + --tabs-border-bottom-color: var(--modern-ui-editor-tabs-border) !important; align-items: center; padding: var(--vscode-spacing-size20) 0 0 var(--vscode-spacing-size20); } +.modern-ui-tabs .part.editor .title:not(.two-tab-bars) > .tabs-and-actions-container::after, +.modern-ui-tabs .part.editor .title.two-tab-bars > .tabs-and-actions-container:not(:first-child)::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + z-index: 9; + pointer-events: none; + width: 100%; + height: var(--vscode-strokeThickness); + background-color: var(--tabs-border-bottom-color); +} + +.modern-ui-tabs .part.editor .title.two-tab-bars > .tabs-and-actions-container:first-child.tabs-border-bottom::after { + display: none; +} + /* In two-tab-bars mode the border-block-width rules below own every vertical gutter, so the * container must not add its own top padding to either the pinned or the unpinned row. */ .modern-ui-tabs .part.editor .title.two-tab-bars > .tabs-and-actions-container { diff --git a/src/vs/workbench/contrib/searchEditor/browser/media/searchEditor.css b/src/vs/workbench/contrib/searchEditor/browser/media/searchEditor.css index 69a2d0c0dce2..968719314c75 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/media/searchEditor.css +++ b/src/vs/workbench/contrib/searchEditor/browser/media/searchEditor.css @@ -13,6 +13,7 @@ } .search-editor .query-container { + --search-editor-query-layout-offset: 28px; margin: 0px 12px 12px 19px; padding-top: 6px; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 1d4336a45d7c..db3bf69625c5 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -68,6 +68,7 @@ import { ISearchResult } from '../../search/browser/searchTreeModel/searchTreeCo const RESULT_LINE_REGEX = /^(\s+)(\d+)(: | )(\s*)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; +const DEFAULT_QUERY_EDITOR_LAYOUT_OFFSET = 28; type SearchEditorViewState = ICodeEditorViewState & { focused: 'input' | 'editor' }; @@ -677,10 +678,12 @@ export class SearchEditor extends AbstractTextCodeEditor private reLayout() { if (this.dimension) { - this.queryEditorWidget.setWidth(this.dimension.width - 28 /* container margin */); + const configuredOffset = Number.parseFloat(DOM.getWindow(this.queryEditorContainer).getComputedStyle(this.queryEditorContainer).getPropertyValue('--search-editor-query-layout-offset')); + const queryEditorWidth = this.dimension.width - (Number.isFinite(configuredOffset) ? configuredOffset : DEFAULT_QUERY_EDITOR_LAYOUT_OFFSET); + this.queryEditorWidget.setWidth(queryEditorWidth); this.searchResultEditor.layout({ height: this.dimension.height - DOM.getTotalHeight(this.queryEditorContainer), width: this.dimension.width }); - this.inputPatternExcludes.setWidth(this.dimension.width - 28 /* container margin */); - this.inputPatternIncludes.setWidth(this.dimension.width - 28 /* container margin */); + this.inputPatternExcludes.setWidth(queryEditorWidth); + this.inputPatternIncludes.setWidth(queryEditorWidth); } }