mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 08:02:52 +00:00
fix(browser): clean up closed page webviews
This commit is contained in:
@@ -31,6 +31,7 @@ import EditorAutosaveController from './editor/EditorAutosaveController'
|
||||
import type { TabGroupLayoutNode } from '../../../shared/types'
|
||||
import BrowserPane, { destroyPersistentWebview } from './browser-pane/BrowserPane'
|
||||
import BrowserPaneOverlayLayer from './browser-pane/BrowserPaneOverlayLayer'
|
||||
import { collectBrowserWebviewIds } from './browser-pane/browser-webview-cleanup'
|
||||
import { handleSwitchTab, handleSwitchTerminalTab } from '../hooks/ipc-tab-switch'
|
||||
import TabGroupSplitLayout from './tab-group/TabGroupSplitLayout'
|
||||
import { shouldAutoCreateInitialTerminal } from './terminal/initial-terminal'
|
||||
@@ -904,29 +905,38 @@ function Terminal(): React.JSX.Element | null {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Why: removeWorktree cleans up browser tab state in the store but cannot
|
||||
// call destroyPersistentWebview (renderer-only DOM code). This subscriber
|
||||
// detects when browser tabs disappear from a worktree (e.g. worktree deleted)
|
||||
// and destroys orphaned webview elements to prevent memory leaks.
|
||||
const prevBrowserTabIdsRef = useRef<Set<string>>(new Set())
|
||||
// Why: browser page state can disappear through store-only paths (CLI tab
|
||||
// close, worktree deletion). The store cannot call destroyPersistentWebview
|
||||
// because that function owns renderer DOM nodes, so this subscriber tears down
|
||||
// webviews whose backing page records were removed.
|
||||
const prevBrowserWebviewIdsRef = useRef<Set<string>>(
|
||||
collectBrowserWebviewIds(
|
||||
useAppStore.getState().browserTabsByWorktree,
|
||||
useAppStore.getState().browserPagesByWorkspace
|
||||
)
|
||||
)
|
||||
useEffect(() => {
|
||||
let prevBrowserTabs = useAppStore.getState().browserTabsByWorktree
|
||||
let prevBrowserPages = useAppStore.getState().browserPagesByWorkspace
|
||||
return useAppStore.subscribe((state) => {
|
||||
if (state.browserTabsByWorktree === prevBrowserTabs) {
|
||||
if (
|
||||
state.browserTabsByWorktree === prevBrowserTabs &&
|
||||
state.browserPagesByWorkspace === prevBrowserPages
|
||||
) {
|
||||
return
|
||||
}
|
||||
prevBrowserTabs = state.browserTabsByWorktree
|
||||
const currentIds = new Set(
|
||||
Object.values(state.browserTabsByWorktree)
|
||||
.flat()
|
||||
.map((tab) => tab.id)
|
||||
prevBrowserPages = state.browserPagesByWorkspace
|
||||
const currentIds = collectBrowserWebviewIds(
|
||||
state.browserTabsByWorktree,
|
||||
state.browserPagesByWorkspace
|
||||
)
|
||||
for (const prevId of prevBrowserTabIdsRef.current) {
|
||||
for (const prevId of prevBrowserWebviewIdsRef.current) {
|
||||
if (!currentIds.has(prevId)) {
|
||||
destroyPersistentWebview(prevId)
|
||||
}
|
||||
}
|
||||
prevBrowserTabIdsRef.current = currentIds
|
||||
prevBrowserWebviewIdsRef.current = currentIds
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collectBrowserWebviewIds } from './browser-webview-cleanup'
|
||||
import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types'
|
||||
|
||||
function workspace(id: string): BrowserWorkspace {
|
||||
return {
|
||||
id,
|
||||
worktreeId: 'wt-1',
|
||||
label: id,
|
||||
sessionProfileId: null,
|
||||
pageIds: [],
|
||||
activePageId: null,
|
||||
url: 'about:blank',
|
||||
title: id,
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
function page(id: string, workspaceId: string): BrowserPage {
|
||||
return {
|
||||
id,
|
||||
workspaceId,
|
||||
worktreeId: 'wt-1',
|
||||
url: 'about:blank',
|
||||
title: id,
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
describe('collectBrowserWebviewIds', () => {
|
||||
it('tracks browser page ids because webviews are keyed by page id', () => {
|
||||
const ids = collectBrowserWebviewIds(
|
||||
{ 'wt-1': [workspace('workspace-1')] },
|
||||
{ 'workspace-1': [page('page-1', 'workspace-1'), page('page-2', 'workspace-1')] }
|
||||
)
|
||||
|
||||
expect([...ids].sort()).toEqual(['page-1', 'page-2'])
|
||||
})
|
||||
|
||||
it('keeps legacy workspace ids only when no page records exist', () => {
|
||||
const ids = collectBrowserWebviewIds({ 'wt-1': [workspace('legacy-workspace')] }, {})
|
||||
|
||||
expect([...ids]).toEqual(['legacy-workspace'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types'
|
||||
|
||||
export function collectBrowserWebviewIds(
|
||||
browserTabsByWorktree: Record<string, BrowserWorkspace[]>,
|
||||
browserPagesByWorkspace: Record<string, BrowserPage[]>
|
||||
): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
for (const pages of Object.values(browserPagesByWorkspace)) {
|
||||
for (const page of pages) {
|
||||
ids.add(page.id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const tabs of Object.values(browserTabsByWorktree)) {
|
||||
for (const tab of tabs) {
|
||||
if ((browserPagesByWorkspace[tab.id] ?? []).length === 0) {
|
||||
ids.add(tab.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
Reference in New Issue
Block a user