diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts index f59d8b4500e..84004997022 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts @@ -7,6 +7,10 @@ import { reconcileParkedWatcherPtyIds, resolveParkedTerminalPaneCandidates } from './terminal-parked-watcher-reconciliation' +import { + readTerminalScrollIntentKeyRetention, + writeKeyedTerminalScrollIntent +} from '../../lib/pane-manager/terminal-scroll-intent-key-store' const TAB_ID = 'tab-1' const WORKTREE_ID = 'repo::/worktree' @@ -86,6 +90,23 @@ describe('paired parked-watcher reconciliation', () => { }) }) +it('releases captured scroll-intent keys when a parked tab is closed', () => { + writeKeyedTerminalScrollIntent(FIRST_LEAF_ID, { + kind: 'pinnedViewport', + bufferType: 'normal', + viewportY: 4, + baseY: 12, + revision: 1 + }) + captureParkedTerminalPaneCandidates(TAB_ID, WORKTREE_ID, [ + { ptyId: FIRST_PTY_ID, paneId: 1, leafId: FIRST_LEAF_ID, drivesTabTitle: true } + ]) + + expect(readTerminalScrollIntentKeyRetention().intents).toBe(1) + retireParkedTerminalTab(TAB_ID) + expect(readTerminalScrollIntentKeyRetention().intents).toBe(0) +}) + // Why: the sole-newborn parity flag is a fact about the captured PTY, so the // layout-fallback rescue must carry it only while the leaf still binds that PTY. describe('untouchedFreshSpawn carry through the layout-fallback rescue', () => { diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts index 7e305ac7fe3..70d2bc3e2e6 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts @@ -10,6 +10,7 @@ import { discardPreHandlerPtyState, hasPreHandlerPtyExit } from './pty-pre-handler-buffer' import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' +import { releaseTerminalScrollIntentKey } from '../../lib/pane-manager/terminal-scroll-intent-key-store' export type ParkedTerminalPaneCapture = { ptyId: string | null @@ -140,7 +141,16 @@ export function retireParkedTerminalTab(tabId: string): void { // Why: explicit tab retirement permanently invalidates both live parked // observers and unmounted-pane candidates; neither may reattach later. disposeParkedTabWatchers(tabId) - capturedPanesByTabId.delete(tabId) + const capture = capturedPanesByTabId.get(tabId) + if (capture) { + // Parked panes never run PaneManager's close teardown. Release their + // strong scroll-intent keys here or every closed parked tab leaks one per + // leaf for the renderer lifetime. + for (const pane of capture.panes) { + releaseTerminalScrollIntentKey(pane.leafId) + } + capturedPanesByTabId.delete(tabId) + } } /**