diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-claims.ts b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-claims.ts deleted file mode 100644 index 7e145c75560..00000000000 --- a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-claims.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' -import { collectLeafIdsInOrder } from './terminal-layout-leaf-ids' - -export type TerminalLayoutLeafClaims = { - root: TerminalPaneLayoutNode | null - activeLeafId?: string | null - ptyIdsByLeafId?: Record -} - -/** - * Leaf ids a rootless layout actually binds. The persisted map types its values as a plain - * string, so an empty one survives the schema and is not a binding: counting it would let a - * layout look like it holds a leaf no session is attached to. - */ -function boundLeafIds(layout: TerminalLayoutLeafClaims): string[] { - return Object.entries(layout.ptyIdsByLeafId ?? {}) - .filter(([, ptyId]) => Boolean(ptyId)) - .map(([leafId]) => leafId) -} - -/** - * Leaf ids this layout holds, read generously: its tree, or — for a rootless layout, which binds - * its sole pane off-tree — every leaf it binds. Use this to ask "does some pane already hold - * this?", where over-counting only costs a reveal that adopts instead of minting. - * - * A binding whose leaf has left a rooted tree reattaches nothing, so it is excluded either way. - */ -export function collectOwnedLeafIds(layout: TerminalLayoutLeafClaims): Set { - return new Set(layout.root ? collectLeafIdsInOrder(layout.root) : boundLeafIds(layout)) -} - -/** - * Leaf ids this layout may take from another tab, read narrowly. Same as the owned set for a - * rooted layout, but a rootless one proves only its sole off-tree pane, or the one its - * `activeLeafId` names: a never-pruned map holds more than it owns, and claiming those evicts - * the live row that really owns them (#13098). Mirrors the `owned`/`claimable` split in - * `terminal-session-row-hydration.ts`, which is the guard that caught #13060. - */ -export function collectClaimableLeafIds(layout: TerminalLayoutLeafClaims): Set { - if (layout.root) { - return new Set(collectLeafIdsInOrder(layout.root)) - } - const leafIds = boundLeafIds(layout) - const provenLeafId = leafIds.length === 1 ? leafIds[0] : layout.activeLeafId - return new Set(leafIds.filter((leafId) => leafId === provenLeafId)) -} diff --git a/src/renderer/src/lib/terminal-pty-pane-owner.ts b/src/renderer/src/lib/terminal-pty-pane-owner.ts index a766195b833..537694f97e6 100644 --- a/src/renderer/src/lib/terminal-pty-pane-owner.ts +++ b/src/renderer/src/lib/terminal-pty-pane-owner.ts @@ -1,4 +1,5 @@ -import { collectOwnedLeafIds } from '@/components/terminal-pane/terminal-layout-leaf-claims' +import { collectLeafIdsInOrder } from '@/components/terminal-pane/terminal-layout-leaf-ids' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import type { AppState } from '@/store/types' /** No `tabsByWorktree`: ownership is tab-keyed, so no worktree key participates. */ @@ -22,6 +23,26 @@ export type TerminalPtyPaneOwnerOptions = { preferTabId?: string } +/** + * Leaf ids this layout holds: its tree, or — for a rootless layout, which binds its sole pane + * off-tree — every leaf it actually binds. A binding whose leaf has left a rooted tree reattaches + * nothing, so it must not outrank a live pane (#13098). + * + * Why the truthiness check: the persisted map types its values as a plain string, so an empty one + * survives the schema and is not a binding. Counting it would name a leaf no session is attached + * to, and a reveal that adopted that tab would show nothing. + */ +function collectOwnedLeafIds(layout: TerminalLayoutSnapshot): Set { + if (layout.root) { + return new Set(collectLeafIdsInOrder(layout.root)) + } + return new Set( + Object.entries(layout.ptyIdsByLeafId ?? {}) + .filter(([, ptyId]) => Boolean(ptyId)) + .map(([leafId]) => leafId) + ) +} + /** The leaf a tab's layout binds to `ptyId`, or null when no leaf it owns holds that binding. */ function findLayoutBoundLeafId( state: TerminalPtyPaneOwnerState, diff --git a/src/renderer/src/lib/terminal-reveal-leaf-owner-lookup.test.ts b/src/renderer/src/lib/terminal-reveal-leaf-owner-lookup.test.ts index dbf9ae677f5..8c1d338781f 100644 --- a/src/renderer/src/lib/terminal-reveal-leaf-owner-lookup.test.ts +++ b/src/renderer/src/lib/terminal-reveal-leaf-owner-lookup.test.ts @@ -1,6 +1,6 @@ // Which tab owns a leaf id decides whether a reveal adopts a pane or mints a second one // (STA-7961). Two layouts can name the same leaf: one that still mounts it, and one left -// holding the id by a detach or by the hydration self-heal. +// holding the id by a detach. import { describe, expect, it } from 'vitest' import { findTerminalTabIdBindingLeafId } from './terminal-reveal-tab-adoption' import type { AppState } from '@/store/types' @@ -47,7 +47,7 @@ describe('findTerminalTabIdBindingLeafId', () => { }) it('prefers the tab that binds the leaf over one that only carries it unbound', () => { - // The hydration self-heal unbinds the losing single-leaf tab but leaves the id in its tree. + // A pane keeps its leaf in the tree after its PTY exits, so the id outlives the binding. const bound = layout(leaf(SHARED_LEAF_ID), { [SHARED_LEAF_ID]: 'pty-a' }) const unbound = layout(leaf(SHARED_LEAF_ID)) diff --git a/src/renderer/src/lib/terminal-reveal-tab-adoption.ts b/src/renderer/src/lib/terminal-reveal-tab-adoption.ts index 754371e885b..b431ba3f564 100644 --- a/src/renderer/src/lib/terminal-reveal-tab-adoption.ts +++ b/src/renderer/src/lib/terminal-reveal-tab-adoption.ts @@ -15,8 +15,8 @@ export type TerminalRevealTabAdoption = | { kind: 'mint' } /** - * The tab whose layout owns a leaf id. Bound-and-in-tree beats in-tree-unbound, because the - * hydration self-heal leaves a losing single-leaf tab carrying its leaf with no session to adopt. + * The tab whose layout owns a leaf id. Bound-and-in-tree beats in-tree-unbound, because a pane + * keeps its leaf after its PTY exits or is cleared, and such a tab has no session to adopt. * Every layout is scanned, including ones whose row is gone: a leaf id is a pane identity for its * lifetime, and re-minting one an orphan layout still holds is how the STA-7961 pair was created. */