fix: prevent window re-maximize when opening browser tab (#666)

This commit is contained in:
Jinwoo Hong 2026-04-15 02:20:43 -04:00 committed by GitHub
parent 66456acf8d
commit 81144f29db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 166 additions and 4 deletions

View file

@ -24,10 +24,30 @@ afterEach(() => {
})
describe('configureDevUserDataPath', () => {
it('uses an explicit dev userData override when provided', async () => {
const { app } = await import('electron')
const { configureDevUserDataPath } = await import('./configure-process')
const originalOverride = process.env.ORCA_DEV_USER_DATA_PATH
process.env.ORCA_DEV_USER_DATA_PATH = '/tmp/orca-dev-repro'
try {
configureDevUserDataPath(true)
} finally {
if (originalOverride === undefined) {
delete process.env.ORCA_DEV_USER_DATA_PATH
} else {
process.env.ORCA_DEV_USER_DATA_PATH = originalOverride
}
}
expect(app.setPath).toHaveBeenCalledWith('userData', '/tmp/orca-dev-repro')
})
it('moves dev runs onto an orca-dev userData path', async () => {
const { app } = await import('electron')
const { configureDevUserDataPath } = await import('./configure-process')
delete process.env.ORCA_DEV_USER_DATA_PATH
configureDevUserDataPath(true)
// Why: production code uses path.join(app.getPath('appData'), 'orca-dev')
@ -159,3 +179,18 @@ describe('installDevParentWatchdog', () => {
expect(setIntervalSpy).not.toHaveBeenCalled()
})
})
describe('enableMainProcessGpuFeatures', () => {
it('appends Orca GPU flags by default', async () => {
const { app } = await import('electron')
const { enableMainProcessGpuFeatures } = await import('./configure-process')
enableMainProcessGpuFeatures()
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
'enable-features',
'Vulkan,UseSkiaGraphite'
)
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('enable-unsafe-webgpu')
})
})

View file

@ -66,6 +66,14 @@ export function configureDevUserDataPath(isDev: boolean): void {
if (!isDev) {
return
}
const overrideUserDataPath = process.env.ORCA_DEV_USER_DATA_PATH
if (overrideUserDataPath) {
// Why: automated Electron repros need an isolated profile so persisted
// tabs/worktrees from the developer's normal `orca-dev` session do not
// change startup behavior and hide or create window-management bugs.
app.setPath('userData', overrideUserDataPath)
return
}
// Why: development runs share the same machine as packaged Orca, and both
// publish runtime bootstrap files under userData. Without a dev-only path,
// `pnpm dev` can overwrite the packaged app's runtime pointer and make the

View file

@ -12,6 +12,9 @@ vi.mock('electron', () => ({
BrowserWindow: browserWindowMock,
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
nativeTheme: { shouldUseDarkColors: false },
screen: {
getPrimaryDisplay: () => ({ workAreaSize: { width: 1440, height: 900 } })
},
shell: { openExternal: openExternalMock }
}))
@ -44,6 +47,7 @@ describe('createMainWindow', () => {
isMock.dev = false
vi.mocked(ipcMain.on).mockReset()
vi.mocked(ipcMain.removeListener).mockReset()
vi.useRealTimers()
})
it('enables renderer sandboxing and opens external links safely', () => {
@ -87,6 +91,14 @@ describe('createMainWindow', () => {
webPreferences: expect.objectContaining({ sandbox: true })
})
)
const browserWindowOptions = browserWindowMock.mock.calls[0]?.[0]
if (process.platform === 'darwin') {
expect(browserWindowOptions).toMatchObject({
titleBarStyle: 'hiddenInset'
})
} else {
expect(browserWindowOptions.titleBarStyle).toBeUndefined()
}
expect(windowHandlers.windowOpen({ url: 'https://example.com' })).toEqual({ action: 'deny' })
expect(windowHandlers.windowOpen({ url: 'localhost:3000' })).toEqual({ action: 'deny' })
@ -487,4 +499,49 @@ describe('createMainWindow', () => {
expect(browserWindowInstance.setWindowButtonPosition).not.toHaveBeenCalled()
})
it('ignores duplicate ready-to-show events after startup maximize has already run', () => {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {
on: vi.fn(),
setZoomLevel: vi.fn(),
setBackgroundThrottling: vi.fn(),
invalidate: vi.fn(),
setWindowOpenHandler: vi.fn(),
send: vi.fn()
}
const browserWindowInstance = {
webContents,
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
isDestroyed: vi.fn(() => false),
isMaximized: vi.fn(() => false),
isFullScreen: vi.fn(() => false),
getSize: vi.fn(() => [1200, 800]),
setSize: vi.fn(),
setWindowButtonPosition: vi.fn(),
maximize: vi.fn(),
show: vi.fn(),
loadFile: vi.fn(),
loadURL: vi.fn()
}
browserWindowMock.mockImplementation(function () {
return browserWindowInstance
})
createMainWindow({
getUI: () =>
({
windowMaximized: true
}) as never,
updateUI: vi.fn()
} as never)
windowHandlers['ready-to-show']()
windowHandlers['ready-to-show']()
expect(browserWindowInstance.maximize).toHaveBeenCalledTimes(1)
expect(browserWindowInstance.show).toHaveBeenCalledTimes(1)
})
})

View file

@ -1,4 +1,4 @@
import { BrowserWindow, ipcMain, nativeTheme, shell } from 'electron'
import { BrowserWindow, ipcMain, nativeTheme, screen, shell } from 'electron'
import { join } from 'path'
import { is } from '@electron-toolkit/utils'
import icon from '../../../resources/icon.png?asset'
@ -61,9 +61,24 @@ export function createMainWindow(
store: Store | null,
opts?: CreateMainWindowOptions
): BrowserWindow {
const savedBounds = store?.getUI().windowBounds
const savedMaximized = store?.getUI().windowMaximized ?? false
// Why: on first launch (no saved bounds), fill the primary display work area
// so the window feels spacious without calling maximize(). Saved bounds still
// win on subsequent launches.
const defaultBounds = (() => {
try {
const { width, height } = screen.getPrimaryDisplay().workAreaSize
return { width, height }
} catch {
return { width: 1200, height: 800 }
}
})()
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
width: savedBounds?.width ?? defaultBounds.width,
height: savedBounds?.height ?? defaultBounds.height,
...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),
minWidth: 600,
minHeight: 400,
show: false,
@ -114,11 +129,53 @@ export function createMainWindow(
}
})
// Why: on macOS + Electron 41, creating a webview guest process can re-emit
// ready-to-show on the same BrowserWindow. Without a one-shot guard the
// handler re-runs maximize() from the persisted savedMaximized flag, snapping
// the window back to full-screen after the user already resized it (#591).
let handledInitialReadyToShow = false
mainWindow.on('ready-to-show', () => {
mainWindow.maximize()
if (handledInitialReadyToShow) {
return
}
handledInitialReadyToShow = true
if (savedMaximized) {
mainWindow.maximize()
}
mainWindow.show()
})
// Why: persist window bounds so the app restores to the user's last
// position/size instead of maximizing on every launch. Debounce to avoid
// hammering the persistence layer during continuous resize drags.
let boundsTimer: ReturnType<typeof setTimeout> | null = null
const saveBounds = (): void => {
if (boundsTimer) {
clearTimeout(boundsTimer)
}
boundsTimer = setTimeout(() => {
boundsTimer = null
if (mainWindow.isDestroyed() || mainWindow.isFullScreen()) {
return
}
const isMaximized = mainWindow.isMaximized()
store?.updateUI({ windowMaximized: isMaximized })
if (!isMaximized) {
store?.updateUI({ windowBounds: mainWindow.getBounds() })
}
}, 500)
}
mainWindow.on('resize', saveBounds)
mainWindow.on('move', saveBounds)
mainWindow.on('maximize', () => {
store?.updateUI({ windowMaximized: true })
})
mainWindow.on('unmaximize', () => {
store?.updateUI({ windowMaximized: false, windowBounds: mainWindow.getBounds() })
})
mainWindow.on('enter-full-screen', () => {
mainWindow.webContents.send('window:fullscreen-changed', true)
})

View file

@ -575,6 +575,11 @@ export type PersistedUIState = {
/** URL to navigate to when a new browser tab is opened. Null means blank tab.
* Phase 3 will expand this to a full BrowserSessionProfile per workspace. */
browserDefaultUrl?: string | null
/** Saved window bounds so the app restores to the user's last position/size
* instead of maximizing on every launch. */
windowBounds?: { x: number; y: number; width: number; height: number } | null
/** Whether the window was maximized when it was last closed. */
windowMaximized?: boolean
}
// ─── Persistence shape ──────────────────────────────────────────────