fix: publish relay-local SSH PTY IDs for stable workspace synchronization

This commit is contained in:
Neil
2026-09-06 10:05:57 -07:00
parent 91f6009301
commit 3e524c3117
3 changed files with 56 additions and 5 deletions
@@ -1,5 +1,8 @@
import { getDefaultWorkspaceSession } from './constants'
import { projectRemoteWorkspaceSshPtyOwner } from './remote-workspace-ssh-pty-owner'
import {
projectRemoteWorkspaceSshPtyOwner,
stripRemoteWorkspaceSshPtyOwners
} from './remote-workspace-ssh-pty-owner'
import type { RemoteWorkspaceSession, RemoteWorkspaceTerminalTab } from './remote-workspace-types'
import type { TerminalTab } from './terminal-tab-types'
import type { WorkspaceSessionState } from './workspace-session-state-types'
@@ -136,7 +139,7 @@ export function exportRemoteWorkspaceSession(
}
}
return {
return stripRemoteWorkspaceSshPtyOwners({
activeWorktreePath,
activeTabId,
tabsByWorktreePath,
@@ -159,7 +162,7 @@ export function exportRemoteWorkspaceSession(
: undefined,
lastVisitedAtByWorktreePath,
defaultTerminalTabsAppliedByWorktreePath
}
})
}
export function importRemoteWorkspaceSession(
@@ -37,6 +37,10 @@ it('imports a retained host session under the receiving client SSH target', () =
remoteSessionIdsByTabId: { tab: oldPtyId }
}
const snapshot = exportRemoteWorkspaceSession(oldSession, { isTargetWorktree: () => true })
// Previously published snapshots contain the publishing client's target IDs.
snapshot.tabsByWorktreePath['/srv/project'][0].ptyId = oldPtyId
snapshot.terminalLayoutsByTabId.tab.ptyIdsByLeafId = { leaf: oldPtyId }
snapshot.remoteSessionIdsByTabId = { tab: oldPtyId }
const imported = importRemoteWorkspaceSession(snapshot, {
executionHostId: 'ssh:new-client-target',
resolveWorktreeId: () => 'new-repo::/srv/project'
@@ -86,3 +90,35 @@ it('preserves legacy, runtime, and malformed IDs while rebasing encoded SSH owne
})
expect(snapshot.remoteSessionIdsByTabId.prior).toBe(toAppSshPtyId('old-client', 'pty2:leaf:4'))
})
it('publishes the same host session after either client imports it', () => {
const hostSession = {
activeWorktreePath: null,
activeTabId: null,
tabsByWorktreePath: {
'/srv/project': [
{
id: 'tab',
ptyId: toAppSshPtyId('client-a', 'pty-1'),
worktreePath: '/srv/project',
title: 'Shell',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {},
remoteSessionIdsByTabId: { tab: toAppSshPtyId('client-a', 'pty-1') }
}
const republish = (target: string) =>
exportRemoteWorkspaceSession(
importRemoteWorkspaceSession(hostSession, {
executionHostId: `ssh:${target}`,
resolveWorktreeId: () => `${target}::/srv/project`
}),
{ isTargetWorktree: () => true }
)
expect(republish('client-a')).toEqual(republish('client-b'))
})
+14 -2
View File
@@ -11,10 +11,22 @@ export function projectRemoteWorkspaceSshPtyOwner(
return session
}
// The authenticated host owns the snapshot; target IDs belong to each importing client.
const project = (id: string): string => {
return mapSshPtyIds(session, (id) => {
const parsed = parseAppSshPtyId(id)
return parsed ? toAppSshPtyId(host.targetId, parsed.relayPtyId) : id
}
})
}
export function stripRemoteWorkspaceSshPtyOwners(
session: RemoteWorkspaceSession
): RemoteWorkspaceSession {
return mapSshPtyIds(session, (id) => parseAppSshPtyId(id)?.relayPtyId ?? id)
}
function mapSshPtyIds(
session: RemoteWorkspaceSession,
project: (id: string) => string
): RemoteWorkspaceSession {
const projectIds = (ids: Record<string, string>): Record<string, string> =>
Object.fromEntries(Object.entries(ids).map(([key, id]) => [key, project(id)]))
return {