From db0e62f4b4f79d0be4280ca990cf7f792e1025d2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:18:44 -0700 Subject: [PATCH] fix(ssh): move the open workspace to its managed server after the cold-start scan (#29894) On cold start the conversion skips its own worktree fetch and waits for startup's scan, so the one rehome attempt found no server row and left the active workspace on the retired SSH owner. Retry the move once the startup scan completes, while the conversion is still current. Co-authored-by: Claude --- ...-managed-server-converted-catalogs.test.ts | 32 +++++++++++++++++++ .../ssh-managed-server-state-effects.ts | 21 ++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts b/src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts index aa0089b9588..86b00c67a67 100644 --- a/src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts +++ b/src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts @@ -176,6 +176,38 @@ describe('a host that just moved to its managed server', () => { expect(store.getState().activeWorktreeId).toBe(worktreeId) }) + it('moves the open workspace once the cold-start scan delivers the server rows', async () => { + store.setState({ + activeRepoId: 'repo-1', + activeWorktreeId: worktreeId, + activeWorkspaceExecutionHostId: 'ssh:ssh-1' + }) + applySshManagedServerTransition('ssh-1', undefined, { + kind: 'managed', + environmentId: 'env-4' + }) + await vi.waitFor(() => expect(store.getState().worktreesByRepo['repo-1']).toEqual([])) + // Startup's own scan has not run yet, so the server's row is not known. + expect(store.getState().activeWorkspaceExecutionHostId).toBe('ssh:ssh-1') + + store.setState({ + worktreesByRepo: { + 'repo-1': [ + makeWorktree({ + id: worktreeId, + repoId: 'repo-1', + path: '/root/repo', + hostId: 'runtime:env-4' + }) + ] + }, + startupWorktreeRefreshCompleted: true + }) + + expect(store.getState().activeWorkspaceExecutionHostId).toBe('runtime:env-4') + expect(store.getState().activeWorktreeId).toBe(worktreeId) + }) + it('leaves a workspace open on another host where it is', async () => { store.setState({ activeRepoId: 'repo-1', diff --git a/src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts b/src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts index 545274c4b07..d3f5b5b31bc 100644 --- a/src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts +++ b/src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts @@ -156,6 +156,27 @@ async function loadManagedServerCatalogs(targetId: string, load: CatalogLoad): P } useAppStore.setState((state) => withoutConvertedSshHostRows(state, targetId)) rehomeActiveWorkspace(targetId, environmentId) + if (!useAppStore.getState().startupWorktreeRefreshCompleted) { + rehomeAfterStartupScan(targetId, environmentId, isCurrent) + } +} + +/** Why: on cold start the server's worktree rows arrive with startup's scan, after the attempt above. */ +function rehomeAfterStartupScan( + targetId: string, + environmentId: string, + isCurrent: () => boolean +): void { + const unsubscribe = useAppStore.subscribe((state) => { + if (!isCurrent()) { + unsubscribe() + return + } + if (state.startupWorktreeRefreshCompleted) { + unsubscribe() + rehomeActiveWorkspace(targetId, environmentId) + } + }) } /** Why: an active workspace left on the SSH host keeps showing panes that dial the stopped relay. */