fix(browser): clean up closed page webviews

This commit is contained in:
Nikolatesla-lj
2026-05-02 04:39:13 +08:00
parent d4bbf473c1
commit 9e5fc43bbb
3 changed files with 99 additions and 12 deletions
+22 -12
View File
@@ -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
}