mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
Defer inactive browser tabs while retaining their viewport slots
Restore worktrees and tabs on demand instead of mounting the full tree. Only render active pages and those required by automation, mobile drivers, or remote viewers. Inactive panes stay deferred with persistent viewport slots so their webview guests survive chrome unmounts, reducing memory overhead when opening workspaces with many tabs.
This commit is contained in:
+32
-3
@@ -177,13 +177,13 @@ describe('BrowserPaneOverlayLayer', () => {
|
||||
expect(view.container.querySelectorAll('[data-browser-overlay-tab-id]')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps inactive browser panes mounted for a visible worktree', () => {
|
||||
it('defers inactive browser panes while retaining their viewport slots', () => {
|
||||
const markup = renderOverlay({ isWorktreeActive: true })
|
||||
|
||||
expect(markup).toContain('data-browser-pane-id="browser-a"')
|
||||
expect(markup).toContain('data-browser-pane-active="true"')
|
||||
expect(markup).toContain('data-browser-pane-id="browser-b"')
|
||||
expect(markup).toContain('data-browser-pane-active="false"')
|
||||
expect(markup).not.toContain('data-browser-pane-id="browser-b"')
|
||||
expect(markup).toContain('data-browser-overlay-tab-id="browser-b"')
|
||||
})
|
||||
|
||||
it('marks the active browser pane focused when its own group holds focus', () => {
|
||||
@@ -195,6 +195,35 @@ describe('BrowserPaneOverlayLayer', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('restores 200 tabs on demand and preserves viewport roots across parking and selection', () => {
|
||||
const browsers = Array.from({ length: 200 }, (_, index) =>
|
||||
createBrowserTab(`browser-${index}`, [`page-${index}`])
|
||||
)
|
||||
const tabs = browsers.map((browser, index) =>
|
||||
createUnifiedBrowserTab(`tab-${index}`, browser.id, index)
|
||||
)
|
||||
mocks.state!.browserTabsByWorktree['wt-1'] = browsers
|
||||
mocks.state!.unifiedTabsByWorktree['wt-1'] = tabs
|
||||
const group = mocks.state!.groupsByWorktree['wt-1'][0]
|
||||
mocks.state!.groupsByWorktree['wt-1'] = [
|
||||
{ ...group, activeTabId: tabs[0].id, tabOrder: tabs.map((tab) => tab.id) }
|
||||
]
|
||||
const view = render(<BrowserPaneOverlayLayer worktreeId="wt-1" isWorktreeActive />)
|
||||
const slot = view.container.querySelector('[data-browser-overlay-tab-id="browser-0"]')!
|
||||
const viewport = slot.firstElementChild
|
||||
expect(view.container.querySelectorAll('[data-browser-pane-id]')).toHaveLength(1)
|
||||
expect(view.container.querySelectorAll('[data-browser-overlay-tab-id]')).toHaveLength(200)
|
||||
|
||||
view.rerender(<BrowserPaneOverlayLayer worktreeId="wt-1" isWorktreeActive={false} />)
|
||||
expect(view.container.querySelectorAll('[data-browser-pane-id]')).toHaveLength(0)
|
||||
mocks.state!.groupsByWorktree['wt-1'] = [{ ...group, activeTabId: tabs[199].id }]
|
||||
view.rerender(<BrowserPaneOverlayLayer worktreeId="wt-1" isWorktreeActive />)
|
||||
expect(view.container.querySelectorAll('[data-browser-pane-id]')).toHaveLength(1)
|
||||
expect(view.container.querySelector('[data-browser-pane-id="browser-199"]')).not.toBeNull()
|
||||
expect(slot.firstElementChild).toBe(viewport)
|
||||
expect(viewport!.isConnected).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps an active browser pane unfocused when another split holds focus (#11348)', () => {
|
||||
mocks.state = createState()
|
||||
mocks.state.groupsByWorktree = {
|
||||
|
||||
+4
-7
@@ -34,7 +34,6 @@ type BrowserOverlaySlotProps = {
|
||||
chromeShortcutScope: BrowserChromeShortcutScope
|
||||
// Why: overlay is a sibling of the group layout, so pane focus doesn't bubble to TabGroupPanel; re-sync it here or split-view clicks leave activeGroupIdByWorktree stale.
|
||||
onFocusOwningGroup: ((groupId: string) => void) | undefined
|
||||
isWorktreeActive: boolean
|
||||
}
|
||||
|
||||
// Why: memoize each slot so unrelated worktree mutations don't cascade a re-render into every BrowserPane subtree.
|
||||
@@ -43,8 +42,7 @@ const BrowserOverlaySlot = memo(function BrowserOverlaySlot({
|
||||
groupId,
|
||||
isActive,
|
||||
chromeShortcutScope,
|
||||
onFocusOwningGroup,
|
||||
isWorktreeActive
|
||||
onFocusOwningGroup
|
||||
}: BrowserOverlaySlotProps): React.JSX.Element {
|
||||
// Why: persistent page viewports (webview guests) live under this root so they survive BrowserPane chrome unmounts without reparenting.
|
||||
const setSlotViewportRef = useCallback(
|
||||
@@ -60,8 +58,8 @@ const BrowserOverlaySlot = memo(function BrowserOverlaySlot({
|
||||
: [browserTab.activePageId ?? browserTab.id]
|
||||
const needsGuestPaint = useBrowserGuestPaintRetention(browserPageIds)
|
||||
const isPaintable = isActive || needsGuestPaint
|
||||
// Why: hidden worktrees keep lightweight overlay slots, but park their webviews unless a remote controller or viewer needs the guest.
|
||||
const shouldMountPane = isWorktreeActive || needsGuestPaint
|
||||
// Why: restoring a workspace must not create guests for every inactive tab.
|
||||
const shouldMountPane = isActive || needsGuestPaint
|
||||
// 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(
|
||||
@@ -104,7 +102,7 @@ const BrowserOverlaySlot = memo(function BrowserOverlaySlot({
|
||||
onFocusCapture={handleFocus}
|
||||
>
|
||||
<div ref={setSlotViewportRef} className="absolute inset-0 flex min-h-0 flex-col" />
|
||||
{/* Why: hidden worktrees park the heavy pane subtree; visible ones keep stable slots so reparenting can't destroy the webview guest. */}
|
||||
{/* Persistent viewport slots keep live guests intact when inactive chrome unmounts. */}
|
||||
{shouldMountPane ? (
|
||||
<BrowserPane
|
||||
browserTab={browserTab}
|
||||
@@ -192,7 +190,6 @@ const BrowserPaneOverlayLayer = memo(function BrowserPaneOverlayLayer({
|
||||
isActive={isActive}
|
||||
chromeShortcutScope={chromeShortcutScope}
|
||||
onFocusOwningGroup={focusOwningGroup}
|
||||
isWorktreeActive={isWorktreeActive}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
+41
-6
@@ -149,14 +149,49 @@ describe('browser workspace pane retention props', () => {
|
||||
hydrateBrowserRemoteViewerPages([])
|
||||
})
|
||||
|
||||
it('loads only the selected page out of 200 after mounting and remounting', () => {
|
||||
const pages = Array.from({ length: 200 }, (_, index) => createPage(`page-${index}`))
|
||||
mocks.state!.browserPagesByWorkspace[WORKSPACE_ID] = pages
|
||||
const workspace = { ...createWorkspace(), activePageId: pages[0].id }
|
||||
const view = render(<BrowserPane browserTab={workspace} isActive />)
|
||||
const renderedIds = (): (string | null)[] =>
|
||||
[...view.container.querySelectorAll('[data-browser-page-id]')].map((node) =>
|
||||
node.getAttribute('data-browser-page-id')
|
||||
)
|
||||
expect(renderedIds()).toEqual(['page-0'])
|
||||
|
||||
view.rerender(<BrowserPane browserTab={{ ...workspace, activePageId: 'page-199' }} isActive />)
|
||||
expect(renderedIds()).toEqual(['page-199'])
|
||||
view.rerender(<BrowserPane browserTab={workspace} isActive={false} />)
|
||||
expect(renderedIds()).toEqual([])
|
||||
view.rerender(<BrowserPane key="restored" browserTab={workspace} isActive />)
|
||||
expect(renderedIds()).toEqual(['page-0'])
|
||||
})
|
||||
|
||||
it.each(['automation', 'mobile', 'viewer'])('loads an inactive page for %s only', (consumer) => {
|
||||
const token = consumer === 'automation' ? acquireBrowserAutomationVisibility('page-b') : null
|
||||
if (consumer === 'mobile') {
|
||||
hydrateBrowserDrivers([
|
||||
{ browserPageId: 'page-b', driver: { kind: 'mobile', clientId: 'phone-1' } }
|
||||
])
|
||||
}
|
||||
if (consumer === 'viewer') {
|
||||
hydrateBrowserRemoteViewerPages(['page-b'])
|
||||
}
|
||||
try {
|
||||
const view = render(<BrowserPane browserTab={createWorkspace()} isActive={false} />)
|
||||
expect(view.container.querySelector('[data-browser-page-id="page-a"]')).toBeNull()
|
||||
expect(view.container.querySelector('[data-browser-page-id="page-b"]')).not.toBeNull()
|
||||
} finally {
|
||||
if (token) {
|
||||
releaseBrowserAutomationVisibility(token)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('threads all three retention terms to the page that owns them', () => {
|
||||
renderWorkspacePane()
|
||||
expect(propsFor('page-b')).toEqual({
|
||||
id: 'page-b',
|
||||
isAutomationVisible: false,
|
||||
isMobileDriven: false,
|
||||
isRemotelyViewed: false
|
||||
})
|
||||
expect(mocks.pageProps.some((props) => props.id === 'page-b')).toBe(false)
|
||||
|
||||
cleanup()
|
||||
const token = acquireBrowserAutomationVisibility('page-b')
|
||||
|
||||
+16
-3
@@ -55,13 +55,26 @@ export default function BrowserPane({
|
||||
const automationVisiblePageIds = useBrowserAutomationVisiblePageIds(browserPageIds)
|
||||
const mobileDrivenPageIds = useBrowserMobileDrivenPageIds(browserPageIds)
|
||||
const remotelyViewedPageIds = useBrowserRemotelyViewedPageIds(browserPageIds)
|
||||
// Why: inactive webviews must stay mounted in their original DOM parent; unmounting/reparenting loses form text and SPA state.
|
||||
// Why: dormant pages load on demand; live guests survive chrome unmounts in persistent viewports.
|
||||
const renderedBrowserPages = useMemo(
|
||||
() =>
|
||||
browserPages.filter(
|
||||
(page) => !getBrowserPageRuntimeEnvironmentId(page, activeRuntimeEnvironmentId)
|
||||
(page) =>
|
||||
!getBrowserPageRuntimeEnvironmentId(page, activeRuntimeEnvironmentId) &&
|
||||
((isActive && page.id === activeBrowserPageId) ||
|
||||
automationVisiblePageIds.has(page.id) ||
|
||||
mobileDrivenPageIds.has(page.id) ||
|
||||
remotelyViewedPageIds.has(page.id))
|
||||
),
|
||||
[browserPages, activeRuntimeEnvironmentId]
|
||||
[
|
||||
browserPages,
|
||||
activeRuntimeEnvironmentId,
|
||||
isActive,
|
||||
activeBrowserPageId,
|
||||
automationVisiblePageIds,
|
||||
mobileDrivenPageIds,
|
||||
remotelyViewedPageIds
|
||||
]
|
||||
)
|
||||
const renderedBrowserPageIds = useMemo(
|
||||
() => renderedBrowserPages.map((page) => page.id),
|
||||
|
||||
Reference in New Issue
Block a user