From 295530df2ea034d9d00f53e6da28019442d08ffa Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:16:31 -0700 Subject: [PATCH] fix(ssh): a migrated host's old session no longer brings closed tabs back on restart (#33702) * fix(ssh): a migrated host's old session no longer brings closed tabs back on restart (#33539) After an SSH host moved to a managed server, main hides its retained source repos but still listed its frozen session partition. The window adopted that partition at startup, and agent tabs the user had closed on the server came back on every restart. The partition census now hides that partition exactly when its repos are hidden; the stored rows stay for downgraded clients. * test(ssh): pin that an uncommitted move keeps its session listed --- src/main/ipc/session.ts | 3 ++- .../ssh/orcad-retained-source-lists.test.ts | 21 +++++++++++++++++++ src/main/ssh/orcad-retained-source-session.ts | 16 ++++++++++++++ src/main/ssh/orcad-retained-source.ts | 4 ++-- 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/main/ssh/orcad-retained-source-session.ts diff --git a/src/main/ipc/session.ts b/src/main/ipc/session.ts index cdde869a5e6..b0b8860c633 100644 --- a/src/main/ipc/session.ts +++ b/src/main/ipc/session.ts @@ -3,6 +3,7 @@ import type { Store } from '../persistence' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { parseTerminalSurfaceCloseTarget } from '../../shared/terminal-surface-close-target' import { isFrozenOrcadSourceSessionPartition } from '../ssh/orcad-retained-source' +import { visibleSessionHostIds } from '../ssh/orcad-retained-source-session' import { markAgentLaunchesClosedByUser } from '../agent-launch/agent-launch-pane-attachment' import { windowViewPatchForMain, windowViewSaveForMain } from '../persistence/window-save-view' import { reportWindowSaveDrift } from '../persistence/window-save-divergence' @@ -25,7 +26,7 @@ export function registerSessionHandlers(store: Store, runtime: OrcaRuntimeServic // Repo catalogs omit folder-only SSH hosts; enumerate session partitions directly (#12723). ipcMain.handle('session:list-host-ids', () => { - return store.getWorkspaceSessionHostIds() + return visibleSessionHostIds(store, store.getWorkspaceSessionHostIds()) }) // Main owns layout membership; merge the window's view and facts, then report layout drift. diff --git a/src/main/ssh/orcad-retained-source-lists.test.ts b/src/main/ssh/orcad-retained-source-lists.test.ts index bb931a7b38e..6d0f2821300 100644 --- a/src/main/ssh/orcad-retained-source-lists.test.ts +++ b/src/main/ssh/orcad-retained-source-lists.test.ts @@ -25,6 +25,7 @@ vi.mock('../../shared/app-environment', () => ({ const { isFrozenOrcadSourceSessionPartition, visibleProjectGroups, visibleProjectHostSetups } = await import('./orcad-retained-source') +const { visibleSessionHostIds } = await import('./orcad-retained-source-session') const FENCED: SshTarget = { id: 'ssh-box', @@ -108,6 +109,26 @@ describe('lists while a converted host keeps its source rows', () => { ]) }) + // #33539: adopting the hidden source's session at startup restored tabs closed on the server. + it('leaves the hidden host session out of the partition census until the host changes', () => { + const hostIds: ExecutionHostId[] = ['local', 'ssh:ssh-box', 'ssh:other', 'runtime:env-1'] + expect(visibleSessionHostIds(catalog([FENCED]), hostIds)).toEqual([ + 'local', + 'ssh:other', + 'runtime:env-1' + ]) + const changed = { ...FENCED, orcadFence: { environmentId: 'env-1', sourceChangedAt: 'x' } } + expect(visibleSessionHostIds(catalog([changed]), hostIds)).toEqual(hostIds) + // A fenced move that has not committed still shows its repos, so their session stays too. + const inFlight = mkdtempSync(join(tmpdir(), 'orcad-retained-lists-')) + try { + writeOrcadMigrationSourceCutover(inFlight, orcadMigrationCutoverFixture('m-1', FENCED.id)) + expect(visibleSessionHostIds(catalog([FENCED]), hostIds, () => inFlight)).toEqual(hostIds) + } finally { + rmSync(inFlight, { recursive: true, force: true }) + } + }) + it('shows them until the migration commits, while freezing the session from the fence on', () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orcad-retained-lists-')) try { diff --git a/src/main/ssh/orcad-retained-source-session.ts b/src/main/ssh/orcad-retained-source-session.ts new file mode 100644 index 00000000000..d9fdfbf33e6 --- /dev/null +++ b/src/main/ssh/orcad-retained-source-session.ts @@ -0,0 +1,16 @@ +import { parseExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' +import type { Store } from '../persistence' +import { appUserDataPath, hiddenRetainedSourceTargetIds } from './orcad-retained-source' + +/** A hidden source's session goes with its repos: a window adopting it at startup brings closed tabs back (#33539). */ +export function visibleSessionHostIds( + store: Pick, + hostIds: readonly ExecutionHostId[], + getUserDataPath = appUserDataPath +): ExecutionHostId[] { + const hidden = new Set(hiddenRetainedSourceTargetIds(getUserDataPath, store.getSshTargets())) + return hostIds.filter((hostId) => { + const parsed = parseExecutionHostId(hostId) + return parsed?.kind !== 'ssh' || !hidden.has(parsed.targetId) + }) +} diff --git a/src/main/ssh/orcad-retained-source.ts b/src/main/ssh/orcad-retained-source.ts index 9b3f29100b7..dc8c3354835 100644 --- a/src/main/ssh/orcad-retained-source.ts +++ b/src/main/ssh/orcad-retained-source.ts @@ -36,7 +36,7 @@ import { withoutWslMovedRepos } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' -const appUserDataPath = (): string => getAppEnvironment().getPath('userData') +export const appUserDataPath = (): string => getAppEnvironment().getPath('userData') type CatalogStore = Pick type TargetStore = Pick @@ -64,7 +64,7 @@ function isHiddenRetainedSourceTarget( } /** Delay journal reads until a host is fenced. */ -function hiddenRetainedSourceTargetIds( +export function hiddenRetainedSourceTargetIds( getUserDataPath: () => string, targets: readonly SshTarget[] ): string[] {