diff --git a/package.json b/package.json index 2dd9706e681..3fd332f5fe1 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "test:e2e:workspace-session-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-quit-relaunch-session.spec.ts tests/e2e/golden-terminal-file-link.spec.ts tests/e2e/golden-worktree-create-switch.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "test:e2e:multi-client-navigation": "node config/scripts/run-multi-client-navigation-e2e.mjs", "test:e2e:floating-mobile-emulator": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/floating-mobile-emulator-tab.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", - "test:e2e:terminal-rendering-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts tests/e2e/terminal-webgl-atlas-budget.spec.ts tests/e2e/terminal-padding-geometry-golden.spec.ts --grep @terminal-rendering-golden --config tests/playwright.config.ts --project electron-headless --workers=1", + "test:e2e:terminal-rendering-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts tests/e2e/terminal-webgl-atlas-budget.spec.ts --grep @terminal-rendering-golden --config tests/playwright.config.ts --project electron-headless --workers=1", "test:e2e:source-control-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-file-open-edit-save.spec.ts tests/e2e/golden-source-control-commit.spec.ts tests/e2e/golden-source-control-open-diff.spec.ts --grep @golden --config tests/playwright.config.ts --project electron-headless --workers=1", "test:e2e:posix-profile-index-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-posix-fresh-startup.spec.ts tests/e2e/golden-posix-profile-index-fsync.spec.ts --grep @posix-profile-index-golden --config tests/playwright.config.ts --project electron-headless --workers=1", "test:e2e:windows-fresh-startup-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-windows-fresh-startup.spec.ts --grep @windows-fresh-startup-golden --config tests/playwright.config.ts --project electron-headless --workers=1", diff --git a/src/main/ghostty/mapper-extended.test.ts b/src/main/ghostty/mapper-extended.test.ts index 9584ac75017..402e9253f21 100644 --- a/src/main/ghostty/mapper-extended.test.ts +++ b/src/main/ghostty/mapper-extended.test.ts @@ -90,9 +90,9 @@ describe('mapGhosttyToOrca — window-padding', () => { expect(result.unsupportedKeys).toEqual([]) }) - it('rounds odd-sum dual-value padding to the integer used for fitting', () => { + it('preserves odd-sum dual-value padding as a fractional average', () => { const result = mapGhosttyToOrca({ 'window-padding-x': '1,2' }) - expect(result.diff).toEqual({ terminalPaddingX: 2 }) + expect(result.diff).toEqual({ terminalPaddingX: 1.5 }) expect(result.unsupportedKeys).toEqual([]) }) diff --git a/src/main/ghostty/numeric-config-values.ts b/src/main/ghostty/numeric-config-values.ts index 9a20f040aee..f8a7ce7b468 100644 --- a/src/main/ghostty/numeric-config-values.ts +++ b/src/main/ghostty/numeric-config-values.ts @@ -1,5 +1,3 @@ -import { normalizeTerminalPadding } from '../../shared/terminal-padding-settings' - // Why: `Number("1e10")` succeeds and passes `Number.isInteger`, so a Ghostty // config with `window-padding-x = 1e9` would sail through the mapper and land // an absurd value in the store. Restrict to plain decimal integers. @@ -12,8 +10,9 @@ export const parseStrictInt = (v: string): number | null => { return Number.isFinite(num) ? num : null } -// Why: Orca stores one integer per axis, so collapse paired paddings to the -// nearest value that xterm's integer-based fit calculation will also use. +// Why: Ghostty accepts "top,bottom" / "left,right" pairs for window paddings, +// but Orca stores a single value per axis — average the pair so the total +// padding along the axis stays the same. export const parsePaddingValue = (v: string): number | null => { const parts = v.split(',') if (parts.length > 2) { @@ -27,5 +26,5 @@ export const parsePaddingValue = (v: string): number | null => { } nums.push(num) } - return normalizeTerminalPadding(nums.reduce((sum, num) => sum + num, 0) / nums.length) + return nums.reduce((sum, num) => sum + num, 0) / nums.length } diff --git a/src/renderer/src/assets/terminal-container-geometry.test.ts b/src/renderer/src/assets/terminal-container-geometry.test.ts index 28eb858fcdd..db079c9095f 100644 --- a/src/renderer/src/assets/terminal-container-geometry.test.ts +++ b/src/renderer/src/assets/terminal-container-geometry.test.ts @@ -5,7 +5,9 @@ const terminalCss = fs.readFileSync(new URL('./terminal.css', import.meta.url), describe('terminal container geometry', () => { it('keeps the hidden link tooltip out of the fitted terminal height', () => { - expect(terminalCss).toMatch(/\.xterm-container\s*{[^}]*height:\s*100%;/s) + expect(terminalCss).toMatch( + /\.xterm-container\s*{[^}]*height:\s*calc\(100% - var\(--pane-padding-y, 4px\)\);/s + ) expect(terminalCss).toMatch( /\.pane\[data-has-title\] \.xterm-container\s*{[^}]*height:\s*calc\(100% - var\(--orca-pane-title-height\)\);/s ) @@ -13,12 +15,4 @@ describe('terminal container geometry', () => { /\.pane-link-tooltip\s*{[^}]*height:\s*var\(--orca-terminal-link-tooltip-height\);/s ) }) - - it('insets the xterm grid on all four edges', () => { - expect(terminalCss).toMatch( - /\.xterm-container \.xterm\s*{[^}]*padding:\s*var\(--pane-padding-y, 4px\) var\(--pane-padding-x, 4px\);/s - ) - expect(terminalCss).toMatch(/\.xterm-container\s*{[^}]*width:\s*100%;/s) - expect(terminalCss).not.toMatch(/\.xterm-container\s*{[^}]*margin-left:/s) - }) }) diff --git a/src/renderer/src/assets/terminal.css b/src/renderer/src/assets/terminal.css index 16b3e36ec5e..ee5e0af01f5 100644 --- a/src/renderer/src/assets/terminal.css +++ b/src/renderer/src/assets/terminal.css @@ -25,11 +25,9 @@ outline: none; } -.xterm-container .xterm { - box-sizing: border-box; +.pane-manager-root .xterm { height: 100%; width: 100%; - padding: var(--pane-padding-y, 4px) var(--pane-padding-x, 4px); } .pane-manager-root .xterm-viewport { @@ -179,7 +177,7 @@ .pane[data-terminal-attention] .xterm-container::after { content: ''; position: absolute; - inset: 0; + inset: 0 4px 4px 0; z-index: 5; pointer-events: none; border-radius: 2px; @@ -500,13 +498,16 @@ border-color: var(--ring); } -/* The container fills the pane; xterm owns the four-edge grid inset so its fit - calculation subtracts the configured padding before choosing cols/rows. */ +/* Baseline xterm-container layout — padding is driven by CSS custom properties + so Ghostty-imported window-padding-x/y can be applied without inline styles. + The fallback 4px preserves the legacy default. */ .xterm-container { box-sizing: border-box; position: relative; - width: 100%; - height: 100%; + width: calc(100% - var(--pane-padding-x, 4px)); + height: calc(100% - var(--pane-padding-y, 4px)); + margin-top: var(--pane-padding-y, 4px); + margin-left: var(--pane-padding-x, 4px); } /* When a pane has a title, shift the terminal content down to make room. diff --git a/src/renderer/src/components/settings/TerminalWindowSection.padding.test.tsx b/src/renderer/src/components/settings/TerminalWindowSection.padding.test.tsx deleted file mode 100644 index 5f54fb38a9e..00000000000 --- a/src/renderer/src/components/settings/TerminalWindowSection.padding.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// @vitest-environment happy-dom - -import { act, type ReactNode } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/global-settings-types' -import { getDefaultSettings } from '../../../../shared/constants' -import { TerminalWindowSection } from './TerminalWindowSection' - -type NumberFieldProps = { - label: string - value: number - onChange: (value: number) => void -} - -const numberFields = vi.hoisted(() => new Map()) - -vi.mock('@/i18n/i18n', () => ({ - translate: (_key: string, defaultValue: string) => defaultValue -})) - -vi.mock('./SettingsFormControls', () => ({ - ColorField: function ColorField() { - return null - }, - NumberField: function NumberField(props: NumberFieldProps) { - numberFields.set(props.label, props) - return null - } -})) - -vi.mock('./SearchableSetting', () => ({ - SearchableSetting: function SearchableSetting({ children }: { children?: ReactNode }) { - return children ?? null - } -})) - -vi.mock('../ui/button', () => ({ Button: () => null })) -vi.mock('../ui/label', () => ({ Label: () => null })) -vi.mock('../ui/switch', () => ({ Switch: () => null })) - -describe('TerminalWindowSection padding', () => { - let container: HTMLDivElement - let root: Root - - beforeEach(() => { - globalThis.IS_REACT_ACT_ENVIRONMENT = true - numberFields.clear() - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - }) - - afterEach(() => { - act(() => root.unmount()) - document.body.replaceChildren() - }) - - it('displays and persists the same integer padding used by terminal fitting', () => { - const updateSettings = vi.fn() - const settings = { - ...getDefaultSettings('/tmp'), - terminalPaddingX: 1.5, - terminalPaddingY: 2.5 - } as GlobalSettings - - act(() => - root.render() - ) - - const horizontal = numberFields.get('Horizontal Padding') - const vertical = numberFields.get('Vertical Padding') - expect(horizontal?.value).toBe(2) - expect(vertical?.value).toBe(3) - - act(() => horizontal?.onChange(3.5)) - act(() => vertical?.onChange(4.5)) - expect(updateSettings).toHaveBeenNthCalledWith(1, { terminalPaddingX: 4 }) - expect(updateSettings).toHaveBeenNthCalledWith(2, { terminalPaddingY: 5 }) - }) -}) diff --git a/src/renderer/src/components/settings/TerminalWindowSection.tsx b/src/renderer/src/components/settings/TerminalWindowSection.tsx index e9efb2c4396..863d7fa11b0 100644 --- a/src/renderer/src/components/settings/TerminalWindowSection.tsx +++ b/src/renderer/src/components/settings/TerminalWindowSection.tsx @@ -1,6 +1,5 @@ import { useRef, useState } from 'react' import { RotateCw } from 'lucide-react' -import { normalizeTerminalPadding } from '../../../../shared/terminal-padding-settings' import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '../ui/button' import { Label } from '../ui/label' @@ -186,15 +185,13 @@ export function TerminalWindowSection({ 'Horizontal Padding' )} description="" - value={normalizeTerminalPadding(settings.terminalPaddingX ?? 4)} + value={settings.terminalPaddingX ?? 4} defaultValue={4} min={0} max={512} step={1} suffix="px" - onChange={(value) => - updateSettings({ terminalPaddingX: normalizeTerminalPadding(value) }) - } + onChange={(value) => updateSettings({ terminalPaddingX: Math.max(0, value) })} /> @@ -215,15 +212,13 @@ export function TerminalWindowSection({ 'Vertical Padding' )} description="" - value={normalizeTerminalPadding(settings.terminalPaddingY ?? 4)} + value={settings.terminalPaddingY ?? 4} defaultValue={4} min={0} max={512} step={1} suffix="px" - onChange={(value) => - updateSettings({ terminalPaddingY: normalizeTerminalPadding(value) }) - } + onChange={(value) => updateSettings({ terminalPaddingY: Math.max(0, value) })} /> diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts index 2bee7878d0b..6f7bec8592b 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts @@ -3,7 +3,6 @@ import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager' import { getDefaultSettings } from '../../../../shared/constants' import { applyTerminalAppearance, - composeRendererTerminalTheme, hexToRgba, publishTerminalViewAttributesAtAppStart } from './terminal-appearance' @@ -217,54 +216,6 @@ describe('applyTerminalAppearance theme assignment', () => { expect(pane.terminal.options.theme?.background).toBe('#102030') }) - it('lets the pane paint a fractional background exactly once', () => { - const pane = makePane(1) - const settings = getDefaultSettings('/tmp') - const manager = makeManager([pane]) - - applyTerminalAppearance( - manager, - { ...settings, terminalBackgroundOpacity: 0.5 }, - true, - new Map(), - new Map(), - 'false', - new Map(), - new Map() - ) - - expect(pane.terminal.options.theme?.background).toMatch(/^rgba\(.+, 0\)$/) - expect(pane.terminal.options.allowTransparency).toBe(true) - expect(manager.setPaneStyleOptions).toHaveBeenCalledWith( - expect.objectContaining({ paneBackground: expect.stringMatching(/^rgba\(.+, 0\.5\)$/) }) - ) - }) - - it('stamps four-edge padding before fitting', () => { - const pane = makePane(1) - pane.fitAddon.proposeDimensions = vi.fn(() => ({ cols: 79, rows: 23 })) - const manager = makeManager([pane]) - const settings = getDefaultSettings('/tmp') - - applyTerminalAppearance( - manager, - { ...settings, terminalPaddingX: 11, terminalPaddingY: 7 }, - true, - new Map(), - new Map(), - 'false', - new Map(), - new Map() - ) - - expect(manager.setPaneStyleOptions).toHaveBeenCalledWith( - expect.objectContaining({ paddingX: 11, paddingY: 7 }) - ) - expect(vi.mocked(manager.setPaneStyleOptions).mock.invocationCallOrder[0]).toBeLessThan( - vi.mocked(pane.fitAddon.proposeDimensions).mock.invocationCallOrder[0]! - ) - }) - // #7934: contrast correction rescues invisible white text on light backgrounds but over-corrects on dark; // gate by the composed theme's background luminance (either theme slot can hold either kind of theme). it('keeps xterm contrast correction on light themes', () => { @@ -411,23 +362,6 @@ describe('applyTerminalAppearance theme assignment', () => { }) }) -describe('composeRendererTerminalTheme', () => { - it('preserves background RGB while removing fractional renderer alpha', () => { - const theme = { background: 'rgba(16, 32, 48, 0.5)', foreground: '#ffffff' } - - expect(composeRendererTerminalTheme(theme, 0.5)).toEqual({ - ...theme, - background: 'rgba(16, 32, 48, 0)' - }) - }) - - it('keeps the opaque fast path unchanged', () => { - const theme = { background: '#102030' } - - expect(composeRendererTerminalTheme(theme, 1)).toBe(theme) - }) -}) - describe('publishTerminalViewAttributesAtAppStart', () => { // Hidden-at-launch PTYs query OSC 10/11 before any pane mounts; publish with no pane manager (terminal-query-authority.md). it('publishes composed attributes without any pane mount and dedupes repeats', () => { diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.ts index c7a1156500f..bb61a50fbbc 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.ts @@ -29,7 +29,6 @@ import { publishTerminalViewAttributes } from './terminal-view-attributes-publis import { normalizeTerminalLineHeight } from '../../../../shared/terminal-line-height-settings' import { maybePushMode2031Flip } from './terminal-mode-2031-replies' import { resolveTerminalMinimumContrastRatio } from '@/lib/terminal-contrast-correction' -import { normalizeTerminalPadding } from '../../../../shared/terminal-padding-settings' export function hexToRgba(hex: string, alpha: number): string { let clean = hex.replace('#', '') @@ -133,22 +132,6 @@ function composedTerminalThemesEqual(a: ITheme | undefined, b: ITheme): boolean return extA.length === extB.length && extA.every((value, i) => value === extB[i]) } -export function composeRendererTerminalTheme( - theme: ITheme | null, - backgroundOpacity: number | undefined -): ITheme | null { - if (!theme?.background || backgroundOpacity === undefined || backgroundOpacity >= 1) { - return theme - } - const transparentBackground = theme.background.replace( - /^rgba\(([^,]+),\s*([^,]+),\s*([^,]+),\s*[^)]+\)$/, - 'rgba($1, $2, $3, 0)' - ) - return transparentBackground === theme.background - ? theme - : { ...theme, background: transparentBackground } -} - export function applyTerminalAppearance( manager: PaneManager, settings: GlobalSettings, @@ -166,7 +149,6 @@ export function applyTerminalAppearance( // Publish composed appearance to main's hidden-PTY query responder — the only point it exists; deduped in the publisher. publishTerminalViewAttributes(theme, appearance.mode, settings) const paneBackground = theme?.background ?? '#000000' - const rendererTheme = composeRendererTerminalTheme(theme, settings.terminalBackgroundOpacity) const terminalFontWeights = resolveTerminalFontWeights( settings.terminalFontWeight, @@ -177,23 +159,10 @@ export function applyTerminalAppearance( settings.terminalFontFamily ) - // Why before fit: FitAddon reads live .xterm padding when proposing cols/rows. - manager.setPaneStyleOptions({ - splitBackground: paneBackground, - paneBackground, - inactivePaneOpacity: paneStyles.inactivePaneOpacity, - activePaneOpacity: paneStyles.activePaneOpacity, - opacityTransitionMs: paneStyles.opacityTransitionMs, - dividerThicknessPx: paneStyles.dividerThicknessPx, - focusFollowsMouse: paneStyles.focusFollowsMouse, - paddingX: normalizeTerminalPadding(settings.terminalPaddingX ?? 4), - paddingY: normalizeTerminalPadding(settings.terminalPaddingY ?? 4) - }) - for (const pane of manager.getPanes()) { // Why value-gated: writing options.theme rebuilds the palette, discarding TUI OSC 4/10/11/12 mutations; skip on no-op change. - if (rendererTheme && !composedTerminalThemesEqual(pane.terminal.options.theme, rendererTheme)) { - pane.terminal.options.theme = rendererTheme + if (theme && !composedTerminalThemesEqual(pane.terminal.options.theme, theme)) { + pane.terminal.options.theme = theme } // Gate off the configured theme background; the live OSC-11 background is deliberately preserved by the // theme write above, so a TUI that repaints its background at runtime won't re-gate (known limitation). @@ -258,4 +227,16 @@ export function applyTerminalAppearance( safeFit(pane) } } + + manager.setPaneStyleOptions({ + splitBackground: paneBackground, + paneBackground, + inactivePaneOpacity: paneStyles.inactivePaneOpacity, + activePaneOpacity: paneStyles.activePaneOpacity, + opacityTransitionMs: paneStyles.opacityTransitionMs, + dividerThicknessPx: paneStyles.dividerThicknessPx, + focusFollowsMouse: paneStyles.focusFollowsMouse, + paddingX: settings.terminalPaddingX, + paddingY: settings.terminalPaddingY + }) } diff --git a/src/shared/terminal-padding-settings.test.ts b/src/shared/terminal-padding-settings.test.ts deleted file mode 100644 index ee7ac432f46..00000000000 --- a/src/shared/terminal-padding-settings.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { normalizeTerminalPadding } from './terminal-padding-settings' - -describe('normalizeTerminalPadding', () => { - it('rounds fractional padding to the nearest fitted pixel', () => { - expect(normalizeTerminalPadding(1.5)).toBe(2) - expect(normalizeTerminalPadding(2.49)).toBe(2) - }) - - it('keeps padding inside the settings range', () => { - expect(normalizeTerminalPadding(-1)).toBe(0) - expect(normalizeTerminalPadding(513)).toBe(512) - }) -}) diff --git a/src/shared/terminal-padding-settings.ts b/src/shared/terminal-padding-settings.ts deleted file mode 100644 index 1f18a46973f..00000000000 --- a/src/shared/terminal-padding-settings.ts +++ /dev/null @@ -1,6 +0,0 @@ -const MIN_TERMINAL_PADDING = 0 -const MAX_TERMINAL_PADDING = 512 - -export function normalizeTerminalPadding(value: number): number { - return Math.min(MAX_TERMINAL_PADDING, Math.max(MIN_TERMINAL_PADDING, Math.round(value))) -} diff --git a/tests/e2e/terminal-padding-geometry-golden.spec.ts b/tests/e2e/terminal-padding-geometry-golden.spec.ts deleted file mode 100644 index 89b744f73b9..00000000000 --- a/tests/e2e/terminal-padding-geometry-golden.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import type { Page } from '@stablyai/playwright-test' -import { test, expect } from './helpers/orca-app' -import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' -import { waitForActiveTerminalManager } from './helpers/terminal' - -type TerminalGeometry = { - cellWidth: number - cellHeight: number - cols: number - rows: number - scrollbarWidth: number - pane: { top: number; right: number; bottom: number; left: number; width: number; height: number } - terminal: { top: number; right: number; bottom: number; left: number } - screen: { - top: number - right: number - bottom: number - left: number - width: number - height: number - } -} - -const FIRST_PADDING = { x: 11, y: 7 } -const SECOND_PADDING = { x: 17, y: 13 } -const TARGET_COLS = 70 -const TARGET_ROWS = 20 -const MIN_REMAINDER = 2 - -async function readTerminalGeometry(page: Page): Promise { - return page.evaluate(() => { - const terminalElement = document.querySelector('.pane .xterm') - const paneElement = terminalElement?.closest('.pane') - const screenElement = terminalElement?.querySelector('.xterm-screen') - const manager = [...(window.__paneManagers?.values() ?? [])].find((candidate) => - candidate.getPanes().some((pane) => pane.container === paneElement) - ) - const pane = manager?.getPanes().find((candidate) => candidate.container === paneElement) - const cell = pane?.terminal.dimensions?.css.cell - if (!terminalElement || !paneElement || !screenElement || !pane || !cell) { - throw new Error('Active terminal geometry is unavailable') - } - const paneRect = paneElement.getBoundingClientRect() - const terminalRect = terminalElement.getBoundingClientRect() - const screenRect = screenElement.getBoundingClientRect() - return { - cellWidth: cell.width, - cellHeight: cell.height, - cols: pane.terminal.cols, - rows: pane.terminal.rows, - scrollbarWidth: pane.terminal.options.scrollbar?.width ?? 14, - pane: { - top: paneRect.top, - right: paneRect.right, - bottom: paneRect.bottom, - left: paneRect.left, - width: paneRect.width, - height: paneRect.height - }, - terminal: { - top: terminalRect.top, - right: terminalRect.right, - bottom: terminalRect.bottom, - left: terminalRect.left - }, - screen: { - top: screenRect.top, - right: screenRect.right, - bottom: screenRect.bottom, - left: screenRect.left, - width: screenRect.width, - height: screenRect.height - } - } - }) -} - -function findNonDivisibleSize(cellSize: number, cells: number, chrome: number): number { - const floor = Math.ceil(cellSize * cells + chrome) - for (let size = floor; size < floor + Math.ceil(cellSize); size += 1) { - const available = size - chrome - const remainder = available - Math.floor(available / cellSize) * cellSize - if (remainder > MIN_REMAINDER && remainder < cellSize - MIN_REMAINDER) { - return size - } - } - throw new Error(`Could not force a non-divisible ${cellSize}px cell geometry`) -} - -async function applyPaddingAndPaneSize( - page: Page, - padding: { x: number; y: number }, - size: { width: number; height: number } -): Promise { - await page.evaluate( - async ({ nextPadding, nextSize }) => { - const store = window.__store - const terminal = document.querySelector('.pane .xterm') - const pane = terminal?.closest('.pane') - if (!store || !pane) { - throw new Error('Terminal settings surface is unavailable') - } - await store.getState().updateSettings({ - terminalPaddingX: nextPadding.x, - terminalPaddingY: nextPadding.y - }) - pane.style.setProperty('width', `${nextSize.width}px`, 'important') - pane.style.setProperty('height', `${nextSize.height}px`, 'important') - }, - { nextPadding: padding, nextSize: size } - ) - - await page.waitForFunction( - ({ expectedPadding, expectedSize }) => { - const store = window.__store - const terminal = document.querySelector('.pane .xterm') - const pane = terminal?.closest('.pane') - if (!store || !pane) { - return false - } - const settings = store.getState().settings - const rect = pane.getBoundingClientRect() - return ( - settings?.terminalPaddingX === expectedPadding.x && - settings?.terminalPaddingY === expectedPadding.y && - Math.abs(rect.width - expectedSize.width) < 0.5 && - Math.abs(rect.height - expectedSize.height) < 0.5 - ) - }, - { expectedPadding: padding, expectedSize: size } - ) - - await page.evaluate(() => { - for (const manager of window.__paneManagers?.values() ?? []) { - for (const pane of manager.getPanes()) { - pane.fitAddon.fit() - } - } - }) - await page.waitForTimeout(100) -} - -function expectClose(actual: number, expected: number): void { - expect(Math.abs(actual - expected)).toBeLessThan(0.75) -} - -function assertFourEdgeGeometry( - geometry: TerminalGeometry, - padding: { x: number; y: number } -): { horizontalRemainder: number; verticalRemainder: number } { - const verticalRemainder = geometry.pane.height - padding.y * 2 - geometry.screen.height - - // The old layout only exposed its trailing gap when cell fitting left a remainder. - expect(verticalRemainder).toBeGreaterThan(MIN_REMAINDER) - expect(verticalRemainder).toBeLessThan(geometry.cellHeight) - - expect({ - left: geometry.terminal.left - geometry.pane.left, - top: geometry.terminal.top - geometry.pane.top, - right: geometry.pane.right - geometry.terminal.right, - bottom: geometry.pane.bottom - geometry.terminal.bottom - }).toEqual({ left: 0, top: 0, right: 0, bottom: 0 }) - - const horizontalRemainder = - geometry.pane.width - padding.x * 2 - geometry.scrollbarWidth - geometry.screen.width - - expect(horizontalRemainder).toBeGreaterThan(0) - expect(horizontalRemainder).toBeLessThan(geometry.cellWidth) - - expectClose(geometry.screen.left - geometry.pane.left, padding.x) - expectClose(geometry.screen.top - geometry.pane.top, padding.y) - expectClose( - geometry.pane.right - geometry.screen.right, - padding.x + geometry.scrollbarWidth + horizontalRemainder - ) - expectClose(geometry.pane.bottom - geometry.screen.bottom, padding.y + verticalRemainder) - - return { horizontalRemainder, verticalRemainder } -} - -test.describe('terminal padding geometry', () => { - test.beforeEach(async ({ orcaPage }) => { - await waitForSessionReady(orcaPage) - await waitForActiveWorktree(orcaPage) - await ensureTerminalVisible(orcaPage) - await waitForActiveTerminalManager(orcaPage) - }) - - // #13252: the old start-edge margin left the grid flush right/bottom. The - // #14583 re-land was reverted, so this guard checks geometry, not its paint mechanism. - test('keeps configured padding on all four edges with a forced cell remainder @terminal-rendering-golden', async ({ - orcaPage - }) => { - const baseline = await readTerminalGeometry(orcaPage) - const firstSize = { - width: findNonDivisibleSize( - baseline.cellWidth, - TARGET_COLS, - FIRST_PADDING.x * 2 + baseline.scrollbarWidth - ), - height: findNonDivisibleSize(baseline.cellHeight, TARGET_ROWS, FIRST_PADDING.y * 2) - } - - await applyPaddingAndPaneSize(orcaPage, FIRST_PADDING, firstSize) - const first = await readTerminalGeometry(orcaPage) - const firstRemainders = assertFourEdgeGeometry(first, FIRST_PADDING) - - // Keep usable dimensions constant while changing both setting values. - const secondSize = { - width: firstSize.width + (SECOND_PADDING.x - FIRST_PADDING.x) * 2, - height: firstSize.height + (SECOND_PADDING.y - FIRST_PADDING.y) * 2 - } - await applyPaddingAndPaneSize(orcaPage, SECOND_PADDING, secondSize) - const second = await readTerminalGeometry(orcaPage) - const secondRemainders = assertFourEdgeGeometry(second, SECOND_PADDING) - - expect(second.cols).toBe(first.cols) - expect(second.rows).toBe(first.rows) - expectClose(secondRemainders.horizontalRemainder, firstRemainders.horizontalRemainder) - expectClose(secondRemainders.verticalRemainder, firstRemainders.verticalRemainder) - }) -})