From 22cfba5654472eb52c217ee392e7de890b8b52bc Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:09:42 -0700 Subject: [PATCH] =?UTF-8?q?fix(ssh):=20union=20unifiedTabs=20per=20tab=20?= =?UTF-8?q?=E2=80=94=20the=20fold=20hydration=20actually=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review established that only the unifiedTabs fold is load-bearing: hydration iterates unifiedTabs for membership and never reads tabsByWorktree, and adoptGrouplessTabs mints a group, activeGroupId and layout for any tab lacking one. So my earlier rationale for the group folds was wrong — short-circuiting tabGroups, tabGroupLayouts and activeGroupIdByWorktree leaves the E2E green. The real defect was granularity. The tabsByWorktree loop unions per tab; this one replaced whole-record per worktree. Because ssh: is a LIVE plane, once local had any record every later partition-created tab was folded into tabsByWorktree — satisfying persistPtyBinding, so a Store-level oracle stays green — stamped as hoisted, and never reached unifiedTabs. Permanently invisible in the tab bar, for exactly the cohort this migration serves. Now unions per tab. KNOWN COVERAGE GAP, stated rather than papered over: no test pins this. The E2E covers only the first upgrade, where local has no unified record. I attempted a unit test and could not construct a partition fixture whose unifiedTabs survives the load parse — the unified Tab shape differs from TerminalTab — so I removed it rather than leave a red or vacuous test behind. The three dead group loops are also left in place rather than removed blind; both belong in the follow-up. --- src/main/persistence.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 278ac90a0ab..5ed4298fd59 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -2970,11 +2970,31 @@ export function hoistSshPartitionsIntoLocalSession( local.tabGroupLayouts = { ...local.tabGroupLayouts, [worktreeId]: structuredClone(layout) } changed = true } + // Per TAB, matching the tabsByWorktree loop above. This is the load-bearing fold: hydration + // iterates `unifiedTabs` for membership and never reads `tabsByWorktree`, so a tab whose + // unified record does not travel is invisible in the tab bar no matter what else is hoisted. + // + // Whole-record-per-worktree was wrong because this plane is LIVE: once local has any record + // for the worktree, every tab the CLI or a headless surface later adds to the partition would + // be folded into tabsByWorktree — satisfying persistPtyBinding, so a Store-level oracle stays + // green — stamped as hoisted, and never reach unifiedTabs. Permanently invisible, for exactly + // the cohort this migration exists to serve. for (const [worktreeId, tabs] of Object.entries(partition.unifiedTabs ?? {})) { - if (local.unifiedTabs?.[worktreeId]) { + const localTabs = local.unifiedTabs?.[worktreeId] + if (!localTabs) { + local.unifiedTabs = { ...local.unifiedTabs, [worktreeId]: structuredClone(tabs) } + changed = true continue } - local.unifiedTabs = { ...local.unifiedTabs, [worktreeId]: structuredClone(tabs) } + const known = new Set(localTabs.map((tab) => tab.id)) + const additions = (tabs ?? []).filter((tab) => !known.has(tab.id)) + if (additions.length === 0) { + continue + } + local.unifiedTabs = { + ...local.unifiedTabs, + [worktreeId]: [...localTabs, ...structuredClone(additions)] + } changed = true } for (const [worktreeId, groupId] of Object.entries(partition.activeGroupIdByWorktree ?? {})) {