feat: enhance editor and chat UI with new styles and layout adjustments

This commit is contained in:
mrleemurray
2026-09-03 13:43:35 +01:00
parent 4043ece7e8
commit 7ff71ee94e
13 changed files with 155 additions and 20 deletions
@@ -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",
@@ -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;
}
@@ -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 {
@@ -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 });
}
}
@@ -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;
@@ -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<BrowserEditorInput>() { };
const opened: { editor: unknown; options: IEditorOptions | undefined }[] = [];
instantiationService.stub(IBrowserViewWorkbenchService, new class extends mock<IBrowserViewWorkbenchService>() {
override getOrCreateLazy(): BrowserEditorInput {
return browserInput;
}
});
instantiationService.stub(IEditorService, new class extends mock<IEditorService>() {
override async openEditor(...args: unknown[]): Promise<undefined> {
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;
@@ -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) }),
@@ -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<readonly IChat[]>;
@@ -158,7 +159,7 @@ function createHarness(disposables: Pick<DisposableStore, 'add'>, options?: { re
container.appendChild(bar.element);
const tabs = Array.from(bar.element.querySelectorAll<HTMLElement>('.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<HTMLElement>('.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',
});
});
@@ -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,
@@ -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 {
@@ -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 {
@@ -13,6 +13,7 @@
}
.search-editor .query-container {
--search-editor-query-layout-offset: 28px;
margin: 0px 12px 12px 19px;
padding-top: 6px;
}
@@ -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<SearchEditorViewState>
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);
}
}