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 <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-16 17:18:44 -07:00
committed by GitHub
co-authored by Claude
parent 7d31ff6c15
commit db0e62f4b4
2 changed files with 53 additions and 0 deletions
@@ -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',
@@ -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. */