fix(browser): load newly opened background tabs (#19633)

* fix(browser): admit newly opened background pages

* fix(browser): scope background page mount admission

---------

Co-authored-by: Merge Sim <sim@local>
(cherry picked from commit 94a64cf40f)
This commit is contained in:
Brennan Benson
2026-09-08 16:58:44 -07:00
committed by Merge Sim
parent a883e17116
commit 7578cd3387
6 changed files with 92 additions and 2 deletions
@@ -15,6 +15,7 @@ import {
useClientHostedBrowserRows
} from '@/lib/pane-manager/client-hosted-browser-row-state'
import { ClientHostedBrowserHostRowPane } from '../client-hosted-browser-host-row-pane'
import { useAnyBrowserPageMountAdmission } from '../host-guest/browser-page-mount-admission'
// Why: Electron <webview> destroys its guest on DOM reparent, so BrowserPanes render at worktree level and moving a tab between groups only swaps the overlay's CSS position-anchor.
@@ -60,7 +61,8 @@ const BrowserOverlaySlot = memo(function BrowserOverlaySlot({
? browserTab.pageIds
: [browserTab.activePageId ?? browserTab.id]
const needsGuestPaint = useBrowserGuestPaintRetention(browserPageIds)
const isPaintable = isActive || needsGuestPaint
const isMountAdmitted = useAnyBrowserPageMountAdmission(browserPageIds)
const isPaintable = isActive || needsGuestPaint || isMountAdmitted
// Why: CSS anchor positioning pins the overlay to its owning group's body — a tab move only swaps positionAnchor, no measurement/state.
// Orphan branch (no anchorName) stays display:none until the tab is reassigned or destroyed.
const style: React.CSSProperties = useMemo(
@@ -22,6 +22,10 @@ import { WorkspaceDocPagePane } from '../workspace-doc/workspace-doc-page-pane'
import { DeferredBrowserContent } from './DeferredBrowserContent'
import { isBrowserPagePanePaintable } from '../host-guest/browser-page-paintability'
import { SshRoutedBrowserPageGate } from './ssh-routed-browser-page-gate'
import {
isBrowserPageMountAdmitted,
useAnyBrowserPageMountAdmission
} from '../host-guest/browser-page-mount-admission'
export default function BrowserPane({
browserTab,
@@ -71,6 +75,7 @@ export default function BrowserPane({
() => localBrowserPages.map((page) => page.id),
[localBrowserPages]
)
const hasAdmittedPage = useAnyBrowserPageMountAdmission(localBrowserPageIds)
const pageDriver = useBrowserDriverForPage(activeBrowserPageId)
// Why: a runtime-backed page is streamed, never locally driven, so its driver must read idle.
const activeBrowserDriver = runtimeEnvironmentActive ? IDLE_BROWSER_DRIVER : pageDriver
@@ -166,7 +171,9 @@ export default function BrowserPane({
key={page.id}
retainMounted={isWorktreeActive}
mountEligible={isBrowserPagePanePaintable({
isActive: isActive && page.id === activeBrowserPageId,
isActive:
(isActive && page.id === activeBrowserPageId) ||
(hasAdmittedPage && isBrowserPageMountAdmitted(page.id)),
isAutomationVisible: automationVisiblePageIds.has(page.id),
isMobileDriven: mobileDrivenPageIds.has(page.id),
hasRemoteViewer: remotelyViewedPageIds.has(page.id)
@@ -0,0 +1,63 @@
import { useSyncExternalStore } from 'react'
// Newly requested pages must start a guest even when opened in the background. Restored pages are
// deliberately absent so worktree restoration can remain lazy.
const admittedPageIds = new Set<string>()
const listeners = new Set<() => void>()
let version = 0
export function isBrowserPageMountAdmitted(pageId: string): boolean {
return admittedPageIds.has(pageId)
}
function emit(): void {
version += 1
for (const listener of listeners) {
listener()
}
}
export function admitBrowserPageMount(pageId: string): void {
if (admittedPageIds.has(pageId)) {
return
}
admittedPageIds.add(pageId)
emit()
}
export function releaseBrowserPageMount(pageId: string): void {
if (!admittedPageIds.delete(pageId)) {
return
}
emit()
}
export function useBrowserPageMountAdmission(pageId: string): boolean {
useSyncExternalStore(
(listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
() => {
void version
return isBrowserPageMountAdmitted(pageId)
},
() => false
)
return isBrowserPageMountAdmitted(pageId)
}
export function useAnyBrowserPageMountAdmission(pageIds: readonly string[]): boolean {
useSyncExternalStore(
(listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
() => {
void version
return pageIds.some(isBrowserPageMountAdmitted)
},
() => false
)
return pageIds.some(isBrowserPageMountAdmitted)
}
@@ -13,6 +13,7 @@ import { closeRemoteBrowserPageInOwningEnvironment } from './browser-remote-clos
import { releaseDocPreviewGrant } from '@/lib/doc-preview-grants'
import { destroyWorkspaceWebviews } from '../browser-webview-cleanup'
import { omitRecordKeys } from '../worktrees/teardown/record-key-omission'
import { releaseBrowserPageMount } from '@/components/browser-pane/host-guest/browser-page-mount-admission'
export function createBrowserCloseActions(
set: BrowserSliceSet,
@@ -28,6 +29,7 @@ export function createBrowserCloseActions(
// grant is the only authority the preview scheme honors — a closed document must stop being
// readable, and it must stop being readable even if the reducer bails out below.
let docPageIdsToRelease: string[] = []
let closedPagesForMountRelease: string[] = []
let activeBrowserWorktreeIdToNotify: string | null = null
set((s) => {
let owningWorktreeId: string | null = null
@@ -49,6 +51,7 @@ export function createBrowserCloseActions(
}
const closedPages = s.browserPagesByWorkspace[tabId] ?? []
closedPagesForMountRelease = closedPages.map((page) => page.id)
const nextBrowserPagesByWorkspace = { ...s.browserPagesByWorkspace }
delete nextBrowserPagesByWorkspace[tabId]
const nextBrowserAnnotationsByPageId = { ...s.browserAnnotationsByPageId }
@@ -179,6 +182,9 @@ export function createBrowserCloseActions(
for (const docPageId of docPageIdsToRelease) {
releaseDocPreviewGrant(docPageId)
}
for (const pageId of closedPagesForMountRelease) {
releaseBrowserPageMount(pageId)
}
for (const tabs of Object.values(get().unifiedTabsByWorktree)) {
const workspaceItem = tabs.find(
@@ -13,6 +13,10 @@ import {
import { ORCA_BROWSER_BLANK_URL } from '../../../../../shared/constants'
import { closeRemoteBrowserPageInOwningEnvironment } from './browser-remote-close'
import { releaseDocPreviewGrant } from '@/lib/doc-preview-grants'
import {
admitBrowserPageMount,
releaseBrowserPageMount
} from '@/components/browser-pane/host-guest/browser-page-mount-admission'
export function createBrowserPageCreateActions(
set: BrowserSliceSet,
@@ -33,6 +37,9 @@ export function createBrowserPageCreateActions(
undefined,
options?.docLocation
)
if (!options?.browserRuntimeEnvironmentId && !options?.docLocation) {
admitBrowserPageMount(page.id)
}
set((s) => {
const pages = s.browserPagesByWorkspace[workspaceId] ?? []
@@ -94,6 +101,7 @@ export function createBrowserPageCreateActions(
},
closeBrowserPage: (pageId) => {
releaseBrowserPageMount(pageId)
let closedWorkspaceIdForLabel: string | null = null
let docPageIdToRelease: string | null = null
const remotePagesToClose: { worktreeId: string; handle: RemoteBrowserPageHandle }[] = []
@@ -17,6 +17,7 @@ import {
} from '../browser-page-records'
import { getBrowserSessionProfileHostId } from './browser-host-state'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { admitBrowserPageMount } from '@/components/browser-pane/host-guest/browser-page-mount-admission'
export function createBrowserTabActions(
set: BrowserSliceSet,
@@ -48,6 +49,9 @@ export function createBrowserTabActions(
browserPageId,
options?.docLocation
)
if (!options?.browserRuntimeEnvironmentId && !options?.docLocation) {
admitBrowserPageMount(page.id)
}
// Why: with no explicit profile, inherit the user's default so a Settings preference applies to new tabs.
const sessionProfileId =
options?.sessionProfileId !== undefined