From 7578cd33871def97ea1ae57b1e83a6b0ec408cbc Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:25:14 -0700 Subject: [PATCH] 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 (cherry picked from commit 94a64cf40fb9d187086645f9c9fbed9c5f6d1665) --- .../BrowserPaneOverlayLayer.tsx | 4 +- .../browser-workspace-pane.tsx | 9 ++- .../browser-page-mount-admission.ts | 63 +++++++++++++++++++ .../slices/browser/browser-close-actions.ts | 6 ++ .../browser/browser-page-create-actions.ts | 8 +++ .../slices/browser/browser-tab-actions.ts | 4 ++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.ts diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.tsx index 3aee63c0af7..b15c8420ff0 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.tsx @@ -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 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( diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.tsx index b50a6aa1692..e473f442a90 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.tsx @@ -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) diff --git a/src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.ts b/src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.ts new file mode 100644 index 00000000000..3c2b1c8a706 --- /dev/null +++ b/src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.ts @@ -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() +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) +} diff --git a/src/renderer/src/store/slices/browser/browser-close-actions.ts b/src/renderer/src/store/slices/browser/browser-close-actions.ts index 3aa77f9e46c..ad979dae145 100644 --- a/src/renderer/src/store/slices/browser/browser-close-actions.ts +++ b/src/renderer/src/store/slices/browser/browser-close-actions.ts @@ -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( diff --git a/src/renderer/src/store/slices/browser/browser-page-create-actions.ts b/src/renderer/src/store/slices/browser/browser-page-create-actions.ts index 7b15902ddee..d5562b3e59a 100644 --- a/src/renderer/src/store/slices/browser/browser-page-create-actions.ts +++ b/src/renderer/src/store/slices/browser/browser-page-create-actions.ts @@ -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 }[] = [] diff --git a/src/renderer/src/store/slices/browser/browser-tab-actions.ts b/src/renderer/src/store/slices/browser/browser-tab-actions.ts index e388716e405..95ae0e7c7a7 100644 --- a/src/renderer/src/store/slices/browser/browser-tab-actions.ts +++ b/src/renderer/src/store/slices/browser/browser-tab-actions.ts @@ -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