diff --git a/src/shared/remote-workspace-session-projection.ts b/src/shared/remote-workspace-session-projection.ts index 8ed295266d5..608f1f0291b 100644 --- a/src/shared/remote-workspace-session-projection.ts +++ b/src/shared/remote-workspace-session-projection.ts @@ -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( diff --git a/src/shared/remote-workspace-session-ssh-owner.test.ts b/src/shared/remote-workspace-session-ssh-owner.test.ts index 3327a388683..4c0cd864101 100644 --- a/src/shared/remote-workspace-session-ssh-owner.test.ts +++ b/src/shared/remote-workspace-session-ssh-owner.test.ts @@ -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')) +}) diff --git a/src/shared/remote-workspace-ssh-pty-owner.ts b/src/shared/remote-workspace-ssh-pty-owner.ts index fc3365bcec7..a89d8275c5a 100644 --- a/src/shared/remote-workspace-ssh-pty-owner.ts +++ b/src/shared/remote-workspace-ssh-pty-owner.ts @@ -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): Record => Object.fromEntries(Object.entries(ids).map(([key, id]) => [key, project(id)])) return {