fix(terminal): preserve floating park watchers

This commit is contained in:
Brennan Benson
2026-07-30 21:21:36 -07:00
parent d6f5d24072
commit 200e712874
4 changed files with 53 additions and 4 deletions
+5 -2
View File
@@ -113,7 +113,8 @@ import {
disposeAllParkedTerminalWatchers,
pruneParkedTerminalWatchers,
shouldDeferParkedPtyExitTabClose,
syncParkedTerminalTabWatchers
syncParkedTerminalTabWatchers,
terminalWatcherLiveWorkspaceIds
} from './terminal-pane/terminal-parked-tab-watchers'
import { isMainTerminalSideEffectAuthorityForPty } from './terminal-pane/terminal-side-effect-facts-handler'
import { appendUniqueOpenFileIds } from './terminal/unsaved-close-queue'
@@ -1294,7 +1295,9 @@ function Terminal(): React.JSX.Element | null {
)
// Why: legacy (non-split) host owns watcher reconciliation; split mode's overlay layers own theirs, so only dispose worktrees with no overlay layer.
useEffect(() => {
pruneParkedTerminalWatchers(new Set(workspaceSurfaces.map((workspace) => workspace.id)))
pruneParkedTerminalWatchers(
terminalWatcherLiveWorkspaceIds(workspaceSurfaces.map((workspace) => workspace.id))
)
for (const workspace of workspaceSurfaces) {
if (
anyMountedWorktreeHasLayout &&
@@ -35,7 +35,8 @@ export {
disposeParkedTerminalWatchersForPtyIds,
disposeParkedTerminalWatchersForWorktree,
getParkedTerminalWatcherTabIds,
pruneParkedTerminalWatchers
pruneParkedTerminalWatchers,
terminalWatcherLiveWorkspaceIds
} from './terminal-parked-watcher-registry'
export type { ParkedTerminalPaneCapture } from './terminal-parked-watcher-registry'
export {
@@ -8,16 +8,24 @@ import {
hasPreHandlerPtyExit
} from './pty-pre-handler-buffer'
import {
capturedPanesByTabId,
parkedWatchersByTabId,
pruneParkedTerminalWatchers
pruneParkedTerminalWatchers,
terminalWatcherLiveWorkspaceIds
} from './terminal-parked-watcher-registry'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
const TAB_ID = 'removed-parked-tab'
const PTY_ID = 'removed-worktree@@parked-pty'
const FLOATING_TAB_ID = 'floating-parked-tab'
const FLOATING_PTY_ID = `${FLOATING_TERMINAL_WORKTREE_ID}@@parked-pty`
describe('terminal parked watcher registry removal', () => {
afterEach(() => {
parkedWatchersByTabId.delete(TAB_ID)
parkedWatchersByTabId.delete(FLOATING_TAB_ID)
capturedPanesByTabId.delete(TAB_ID)
capturedPanesByTabId.delete(FLOATING_TAB_ID)
clearPreHandlerPtyState(PTY_ID)
})
@@ -51,4 +59,35 @@ describe('terminal parked watcher registry removal', () => {
expect(exit).not.toHaveBeenCalled()
expect(delayedData).not.toHaveBeenCalled()
})
it('keeps the floating workspace while pruning a removed workspace', () => {
const removedDispose = vi.fn()
const floatingDispose = vi.fn()
parkedWatchersByTabId.set(TAB_ID, {
worktreeId: 'removed-worktree',
tabPtyId: PTY_ID,
paneIdByPtyId: new Map([[PTY_ID, 1]]),
disposersByPtyId: new Map([[PTY_ID, removedDispose]])
})
parkedWatchersByTabId.set(FLOATING_TAB_ID, {
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
tabPtyId: FLOATING_PTY_ID,
paneIdByPtyId: new Map([[FLOATING_PTY_ID, 1]]),
disposersByPtyId: new Map([[FLOATING_PTY_ID, floatingDispose]])
})
capturedPanesByTabId.set(TAB_ID, { worktreeId: 'removed-worktree', panes: [] })
capturedPanesByTabId.set(FLOATING_TAB_ID, {
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
panes: []
})
pruneParkedTerminalWatchers(terminalWatcherLiveWorkspaceIds([]))
expect(parkedWatchersByTabId.has(TAB_ID)).toBe(false)
expect(capturedPanesByTabId.has(TAB_ID)).toBe(false)
expect(removedDispose).toHaveBeenCalledOnce()
expect(parkedWatchersByTabId.has(FLOATING_TAB_ID)).toBe(true)
expect(capturedPanesByTabId.has(FLOATING_TAB_ID)).toBe(true)
expect(floatingDispose).not.toHaveBeenCalled()
})
})
@@ -8,6 +8,7 @@
* import cycle-free, mirroring how pty-dispatcher exports its handler maps.
*/
import { discardPreHandlerPtyState } from './pty-pre-handler-buffer'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
export type ParkedTerminalPaneCapture = {
ptyId: string | null
@@ -51,6 +52,11 @@ export function getParkedTerminalWatcherTabIds(): string[] {
return Array.from(parkedWatchersByTabId.keys())
}
// Why: the floating workspace is synthetic, so repo/folder surface lists never include it.
export function terminalWatcherLiveWorkspaceIds(workspaceIds: Iterable<string>): Set<string> {
return new Set([...workspaceIds, FLOATING_TERMINAL_WORKTREE_ID])
}
/**
* Whether this tab is parked right now — the reveal remount's own mount effect
* runs before the host effect that disposes the watcher (child effects first),