Fix browser pane stuck in HTML fullscreen (#6442) (#6451)

* Fix browser pane stuck in HTML fullscreen (#6442)

Guest content in a browser pane could enter HTML fullscreen but not
exit it: requestFullscreen() resized the host BrowserWindow into native
fullscreen, and exitFullscreen() had nothing to restore, leaving the
pane stuck.

Set the webview's disableHtmlFullscreenWindowResize preference so HTML
fullscreen is contained to the <webview> element. Exit then works and
fullscreenchange fires as guests expect.

* Apply browser guest fullscreen policy consistently

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Wolfie 2026-06-26 20:05:47 -07:00 committed by GitHub
parent 66b54c4f49
commit 1f968b92c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 94 additions and 0 deletions

View File

@ -1,6 +1,7 @@
import { randomUUID } from 'crypto'
import { BrowserWindow } from 'electron'
import { ORCA_BROWSER_PARTITION } from '../../shared/constants'
import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences'
import type { BrowserBackend, BrowserBackendCreateTab } from './browser-backend'
import type { BrowserManager } from './browser-manager'
import { browserSessionRegistry } from './browser-session-registry'
@ -35,6 +36,9 @@ export class OffscreenBrowserBackend implements BrowserBackend {
width: DEFAULT_VIEWPORT_WIDTH,
height: DEFAULT_VIEWPORT_HEIGHT,
webPreferences: {
// Why: offscreen pages are the SSH/headless browser backend; keep their
// HTML fullscreen behavior aligned with desktop <webview> guests.
...ORCA_BROWSER_GUEST_WEB_PREFERENCES,
partition,
sandbox: true,
contextIsolation: true,

View File

@ -0,0 +1,27 @@
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { describe, expect, it } from 'vitest'
const OFFSCREEN_BACKEND_SOURCE = resolve(__dirname, 'offscreen-browser-backend.ts')
function sourceBetween(source: string, start: string, end: string): string {
const startIndex = source.indexOf(start)
const endIndex = source.indexOf(end, startIndex + start.length)
expect(startIndex).toBeGreaterThanOrEqual(0)
expect(endIndex).toBeGreaterThan(startIndex)
return source.slice(startIndex, endIndex)
}
describe('OffscreenBrowserBackend web preferences', () => {
it('uses the shared browser guest fullscreen policy', () => {
const source = readFileSync(OFFSCREEN_BACKEND_SOURCE, 'utf8')
const webPreferencesBlock = sourceBetween(source, 'webPreferences: {', 'partition,')
expect(source).toContain(
"import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences'"
)
expect(webPreferencesBlock).toContain('...ORCA_BROWSER_GUEST_WEB_PREFERENCES')
})
})

View File

@ -225,6 +225,11 @@ describe('createMainWindow', () => {
{ src: 'data:text/html,' } as never
)
expect(allowBlankEvent.preventDefault).not.toHaveBeenCalled()
expect(allowBlankPrefs).toMatchObject({
disableHtmlFullscreenWindowResize: true,
partition: 'persist:orca-browser',
sandbox: true
})
const denyInlineHtmlEvent = { preventDefault: vi.fn() }
windowHandlers['will-attach-webview'](

View File

@ -20,6 +20,7 @@ import {
normalizeBrowserNavigationUrl,
normalizeExternalBrowserUrl
} from '../../shared/browser-url'
import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences'
import { isCrashReportReason } from '../../shared/crash-reporting'
import {
getWindowShortcutActionId,
@ -485,6 +486,9 @@ export function createMainWindow(
webPreferences.allowRunningInsecureContent = false
webPreferences.contextIsolation = true
webPreferences.sandbox = true
// Why: keep renderer-created webviews aligned with the browser guest policy
// even if the host markup omits or misspells a preference.
Object.assign(webPreferences, ORCA_BROWSER_GUEST_WEB_PREFERENCES)
// Why: preserve the registry-validated partition instead of forcing the
// legacy constant. This lets imported/isolated session profiles use their
// own cookie/storage partition while keeping all other hardening intact.

View File

@ -77,6 +77,7 @@ import {
browserViewportPresetToOverride,
getBrowserViewportPreset
} from '../../../../shared/browser-viewport-presets'
import { ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE } from '../../../../shared/browser-guest-web-preferences'
import { rememberLiveBrowserUrl } from './browser-runtime'
import {
destroyPersistentWebview,
@ -3612,6 +3613,16 @@ function BrowserPagePane({
webview = document.createElement('webview') as Electron.WebviewTag
webview.setAttribute('partition', webviewPartition)
webview.setAttribute('allowpopups', '')
// Why: keep HTML fullscreen contained to the <webview> element instead of
// letting guest content resize the host BrowserWindow into native macOS
// fullscreen. Without this, requestFullscreen() resizes the OS window but
// exitFullscreen() has nothing to restore, leaving the pane stuck
// fullscreen (issue #6442). Containing fullscreen to the element makes
// exit reliable and fires fullscreenchange as guests expect.
// Why: the key must be camelCase — Electron's <webview> webpreferences
// parser spreads keys verbatim into WebPreferences, so a lowercase key is
// silently ignored.
webview.setAttribute('webpreferences', ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE)
webview.style.display = 'flex'
webview.style.flex = '1'
webview.style.width = '100%'

View File

@ -0,0 +1,38 @@
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { describe, expect, it } from 'vitest'
const BROWSER_PANE_SOURCE = resolve(__dirname, 'BrowserPane.tsx')
function browserPaneSource(): string {
return readFileSync(BROWSER_PANE_SOURCE, 'utf8')
}
function sourceBetween(source: string, start: string, end: string): string {
const startIndex = source.indexOf(start)
const endIndex = source.indexOf(end, startIndex + start.length)
expect(startIndex).toBeGreaterThanOrEqual(0)
expect(endIndex).toBeGreaterThan(startIndex)
return source.slice(startIndex, endIndex)
}
describe('BrowserPane webview preferences', () => {
it('keeps HTML fullscreen contained inside the browser pane webview', () => {
const source = browserPaneSource()
const creationBlock = sourceBetween(
source,
"webview = document.createElement('webview')",
'registerPersistentWebview'
)
expect(source).toContain(
"import { ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE } from '../../../../shared/browser-guest-web-preferences'"
)
expect(creationBlock).toContain(
"webview.setAttribute('webpreferences', ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE)"
)
expect(creationBlock).not.toContain('disablehtmlfullscreenwindowresize')
})
})

View File

@ -0,0 +1,5 @@
export const ORCA_BROWSER_GUEST_WEB_PREFERENCES = {
disableHtmlFullscreenWindowResize: true
} as const
export const ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE = 'disableHtmlFullscreenWindowResize=true'