diff --git a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx index 78c8969c291..e23df8ae29e 100644 --- a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx @@ -1,5 +1,6 @@ // @vitest-environment happy-dom import { cleanup, render } from '@testing-library/react' +import { Suspense } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { BrowserTab as BrowserTabState, Tab, TabGroup } from '../../../../shared/types' @@ -94,6 +95,33 @@ describe('BrowserPaneOverlayLayer', () => { expect(view.container.querySelectorAll('[data-browser-overlay-tab-id]')).toHaveLength(2) }) + it('does not retain browser slots from an eligible render that never commits', () => { + const pending = new Promise(() => {}) + const BlockCommit = ({ blocked }: { blocked: boolean }): null => { + if (blocked) { + throw pending + } + return null + } + const renderBoundary = (mountEligible: boolean, blocked: boolean) => ( + }> + + + + ) + const view = render(renderBoundary(false, false)) + + view.rerender(renderBoundary(true, true)) + expect(view.container.querySelector('[data-suspended]')).not.toBeNull() + + view.rerender(renderBoundary(false, false)) + expect(view.container.querySelectorAll('[data-browser-overlay-tab-id]')).toHaveLength(0) + }) + it('keeps inactive browser panes mounted for a visible worktree', () => { const markup = renderOverlay({ isWorktreeActive: true }) diff --git a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx index f6ca1286648..67d6aa4da9e 100644 --- a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useMemo, useRef } from 'react' +import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react' import { registerBrowserOverlaySlotViewport } from './browser-page-viewport' import { useShallow } from 'zustand/react/shallow' import { useAppStore } from '../../store' @@ -203,11 +203,14 @@ export const RetainedBrowserPaneOverlayLayer = memo(function RetainedBrowserPane isWorktreeActive: boolean mountEligible: boolean }): React.JSX.Element | null { - const hasMountedRef = useRef(mountEligible) - if (mountEligible) { - hasMountedRef.current = true - } - if (!hasMountedRef.current) { + const [hasCommittedMount, setHasCommittedMount] = useState(false) + // Why: commit the latch with the persistent slot DOM so discarded renders cannot retain a guest host. + useLayoutEffect(() => { + if (mountEligible && !hasCommittedMount) { + setHasCommittedMount(true) + } + }, [hasCommittedMount, mountEligible]) + if (!mountEligible && !hasCommittedMount) { return null } return