From 69700466e91156fcf44b6b2ef2b72ecfb9904c56 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 15 May 2026 11:57:22 -0700 Subject: [PATCH] fix: constrain floating terminal to titlebar (#1955) --- .../floating-terminal-panel-bounds.test.ts | 61 +++++++++++++++++++ .../floating-terminal-panel-bounds.ts | 15 ++--- 2 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts diff --git a/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts b/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts new file mode 100644 index 000000000..8cce47e3f --- /dev/null +++ b/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + TITLEBAR_SAFE_TOP, + clampFloatingTerminalBounds, + getDefaultFloatingTerminalBounds, + getMaximizedFloatingTerminalBounds +} from './floating-terminal-panel-bounds' + +function stubViewport(width: number, height: number, userAgent: string): void { + vi.stubGlobal('window', { innerWidth: width, innerHeight: height }) + vi.stubGlobal('navigator', { userAgent }) +} + +describe('floating terminal panel bounds', () => { + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('lets dragged panels touch the macOS titlebar without going above it', () => { + stubViewport(1200, 800, 'Macintosh') + + const bounds = clampFloatingTerminalBounds({ + left: 32, + top: 8, + width: 640, + height: 360 + }) + + expect(bounds.top).toBe(TITLEBAR_SAFE_TOP) + }) + + it('keeps dragged panels below the renderer titlebar on other platforms', () => { + stubViewport(1200, 800, 'Windows NT') + + const bounds = clampFloatingTerminalBounds({ + left: 32, + top: 8, + width: 640, + height: 360 + }) + + expect(bounds.top).toBe(TITLEBAR_SAFE_TOP) + }) + + it('maximizes below the renderer titlebar on non-mac platforms', () => { + stubViewport(1200, 800, 'Windows NT') + + expect(getMaximizedFloatingTerminalBounds()).toEqual( + expect.objectContaining({ + top: TITLEBAR_SAFE_TOP, + height: 800 - TITLEBAR_SAFE_TOP - 36 + }) + ) + }) + + it('defaults at or below the titlebar on compact macOS windows', () => { + stubViewport(760, 420, 'Macintosh') + + expect(getDefaultFloatingTerminalBounds().top).toBeGreaterThanOrEqual(TITLEBAR_SAFE_TOP) + }) +}) diff --git a/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.ts b/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.ts index c9892cba0..f083abe8f 100644 --- a/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.ts +++ b/src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.ts @@ -4,7 +4,7 @@ export const MIN_PANEL_WIDTH = 420 export const MIN_PANEL_HEIGHT = 280 export const MAXIMIZED_MARGIN = 12 export const MAXIMIZED_BOTTOM_GAP = 36 -export const MAC_TITLEBAR_SAFE_TOP = 44 +export const TITLEBAR_SAFE_TOP = 36 const DEFAULT_RIGHT_GAP = 24 const DEFAULT_BOTTOM_GAP = 84 @@ -18,11 +18,14 @@ export type FloatingTerminalPanelBounds = { export function getDefaultFloatingTerminalBounds(): FloatingTerminalPanelBounds { const viewportWidth = typeof window === 'undefined' ? 1200 : window.innerWidth const viewportHeight = typeof window === 'undefined' ? 800 : window.innerHeight + // Why: the floating panel may touch the renderer titlebar, but must not + // overlap it or the native window controls above it. + const safeTop = TITLEBAR_SAFE_TOP const width = Math.min(DEFAULT_PANEL_WIDTH, Math.max(MIN_PANEL_WIDTH, viewportWidth - 48)) const height = Math.min(DEFAULT_PANEL_HEIGHT, Math.max(MIN_PANEL_HEIGHT, viewportHeight - 96)) return { left: Math.max(16, viewportWidth - width - DEFAULT_RIGHT_GAP), - top: Math.max(40, viewportHeight - height - DEFAULT_BOTTOM_GAP), + top: Math.max(safeTop, viewportHeight - height - DEFAULT_BOTTOM_GAP), width, height } @@ -35,20 +38,18 @@ export function clampFloatingTerminalBounds( typeof window === 'undefined' ? bounds.left + bounds.width : window.innerWidth const viewportHeight = typeof window === 'undefined' ? bounds.top + bounds.height : window.innerHeight + const safeTop = TITLEBAR_SAFE_TOP return { ...bounds, left: Math.min(Math.max(8, bounds.left), Math.max(8, viewportWidth - 80)), - top: Math.min(Math.max(8, bounds.top), Math.max(8, viewportHeight - 80)) + top: Math.min(Math.max(safeTop, bounds.top), Math.max(safeTop, viewportHeight - 80)) } } export function getMaximizedFloatingTerminalBounds(): FloatingTerminalPanelBounds { const viewportWidth = typeof window === 'undefined' ? 1200 : window.innerWidth const viewportHeight = typeof window === 'undefined' ? 800 : window.innerHeight - const top = - typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') - ? MAC_TITLEBAR_SAFE_TOP - : MAXIMIZED_MARGIN + const top = TITLEBAR_SAFE_TOP return { left: MAXIMIZED_MARGIN, top,