fix(layout): an SSH pane's terminal binds in its own session partition (dark, layout-owner build only)

Main keeps its own layout over a window save, so a binding only a save carries is lost. A window
or runtime spawn of an SSH pane bound in the local partition; it now binds where the pane's layout
lives.
This commit is contained in:
Jinwoo-H
2026-10-18 02:49:46 -04:00
parent d4b3a65d52
commit beb2275b45
6 changed files with 162 additions and 16 deletions
+3 -1
View File
@@ -3,6 +3,7 @@ import { closeStartupQueryAuthorityForPty } from '../provider/registry'
import { createTerminalSessionStateSaveFailureMessage } from '../../../../shared/terminal-session-state-save-failure'
import { recordCodexPaneAccountForSpawn } from '../host-env/codex-home'
import { persistAdmittedStablePaneBinding } from '../pane/stable-owner'
import { persistPaneBinding } from '../pane/pane-binding-partition'
import { swapReplacedPaneBinding } from '../pane/pane-owner-replacement'
import {
pendingByPaneKey,
@@ -25,6 +26,7 @@ export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise<P
owner: ctx.stablePaneOwner,
result: ctx.result,
worktreeId: args.worktreeId,
connectionId: args.connectionId,
startupCwd: ctx.cwd
})
} catch (error) {
@@ -64,7 +66,7 @@ export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise<P
const input = ctx.replacedPaneOwner
? swapReplacedPaneBinding(ctx.deps.store, binding, ctx.replacedPaneOwner)
: binding
const persisted = await ctx.deps.store.persistPtyBinding(input)
const persisted = await persistPaneBinding(ctx.deps.store, input, args.connectionId)
if (persisted === false) {
throw new Error('terminal_pane_owner_changed')
}
@@ -0,0 +1,17 @@
import { toSshExecutionHostId } from '../../../../shared/execution-host'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import type { Store } from '../../../persistence'
/**
* Switch point: main keeps its own layout over a window save, so an SSH pane binds in the
* partition its layout lives in, not in `local`.
*/
export function persistPaneBinding(
store: Pick<Store, 'persistPtyBinding'>,
binding: Parameters<Store['persistPtyBinding']>[0],
connectionId: string | null | undefined
): Promise<boolean> {
return ORCA_LAYOUT_OWNER_IS_MAIN && connectionId
? store.persistPtyBinding(binding, toSshExecutionHostId(connectionId))
: store.persistPtyBinding(binding)
}
@@ -4,6 +4,7 @@ import type { Store } from '../../../persistence'
import type { PersistPtyBindingArgs } from '../../../persistence/loading-store/pty-binding-persistence'
import type { OrcaRuntimeService } from '../../../runtime/orca-runtime'
import { resolveStablePaneOwner, type StablePaneOwner } from './stable-owner'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
/** The owner a replacing spawn stopped. Its binding stays until the replacement's bind swaps it. */
export type ReplacedPaneOwner = {
@@ -69,10 +70,9 @@ export function swapReplacedPaneBinding(
replaced: ReplacedPaneOwner
): () => PersistPtyBindingArgs | null {
return () => {
const bound =
store.getWorkspaceSession().terminalLayoutsByTabId?.[binding.tabId]?.ptyIdsByLeafId?.[
binding.leafId
]
const bound = store.getWorkspaceSession(
ORCA_LAYOUT_OWNER_IS_MAIN ? replaced.pane?.hostId : undefined
).terminalLayoutsByTabId?.[binding.tabId]?.ptyIdsByLeafId?.[binding.leafId]
// Unbound is ok: the renderer's pre-connect clear can withdraw it (split tab).
return bound === undefined || bound === replaced.ptyId ? binding : null
}
+16 -10
View File
@@ -20,6 +20,7 @@ import {
import { ptyIncarnationById } from '../provider/ownership-state'
import { releaseAbsentPaneOwner } from './absent-pane-owner'
import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span'
import { persistPaneBinding } from './pane-binding-partition'
export type StablePaneOwner = {
handle?: string
@@ -197,22 +198,27 @@ export async function persistAdmittedStablePaneBinding(args: {
owner: StablePaneOwner | null
result: PtySpawnResult
worktreeId: string | undefined
connectionId: string | null | undefined
startupCwd: string | undefined
}): Promise<boolean> {
const expectedBinding = stablePanePersistenceFence(args.owner)
if (!args.store || !args.owner || !args.worktreeId || !expectedBinding) {
return false
}
const persisted = await args.store.persistPtyBinding({
worktreeId: args.worktreeId,
tabId: args.owner.tabId,
leafId: args.owner.leafId,
ptyId: args.result.id,
...(args.result.incarnationId ? { incarnationId: args.result.incarnationId } : {}),
...(args.startupCwd ? { startupCwd: args.startupCwd } : {}),
expectedBinding,
origin: spawnCommitBindingOrigin(args.result)
})
const persisted = await persistPaneBinding(
args.store,
{
worktreeId: args.worktreeId,
tabId: args.owner.tabId,
leafId: args.owner.leafId,
ptyId: args.result.id,
...(args.result.incarnationId ? { incarnationId: args.result.incarnationId } : {}),
...(args.startupCwd ? { startupCwd: args.startupCwd } : {}),
expectedBinding,
origin: spawnCommitBindingOrigin(args.result)
},
args.connectionId
)
if (persisted === false) {
throw new Error('terminal_pane_owner_changed')
}
+3 -1
View File
@@ -23,6 +23,7 @@ import { seedResumeLaunchIdentityFromSpawn } from '../pane/resume-launch-identit
import { seedHeadlessTerminalFromSpawnResult } from '../pane/terminal-spawn-restore'
import { recordPtySpawnTelemetry } from '../pane/spawn-telemetry'
import { persistAdmittedStablePaneBinding } from '../pane/stable-owner'
import { persistPaneBinding } from '../pane/pane-binding-partition'
import {
isNativeWindowsLocalPtySpawn,
markNativeWindowsConptyPty
@@ -66,6 +67,7 @@ async function commitReservedRuntimePtySpawn(ctx: RuntimePtySpawnState) {
owner: ctx.stablePaneOwner,
result: ctx.result,
worktreeId: ctx.hostSessionBinding?.worktreeId,
connectionId: args.connectionId,
startupCwd: ctx.cwd
})
} catch (error) {
@@ -152,7 +154,7 @@ async function commitReservedRuntimePtySpawn(ctx: RuntimePtySpawnState) {
...(placement ? { placement } : {}),
origin: spawnCommitBindingOrigin(ctx.result, expectedSourceBinding)
}
const persisted = await store.persistPtyBinding(binding)
const persisted = await persistPaneBinding(store, binding, args.connectionId)
if (persisted === false) {
throw new Error('terminal_split_source_not_found')
}
@@ -0,0 +1,119 @@
import { expect, it, vi } from 'vitest'
import { admitRuntimeSpawnPane, fixture } from './profile-state-delayed-authority-fixture'
import { commitRuntimePtySpawn } from '../../ipc/pty/runtime/spawn-commit'
import { createRuntimePtySpawnState } from '../../ipc/pty/runtime/spawn-state'
import type { PtyRuntimeControllerDeps } from '../../ipc/pty/runtime/controller-deps'
import { OrcaRuntimeService } from '../../runtime/orca-runtime'
import { persistPtyIpcSpawnCommit } from '../../ipc/pty/ipc/spawn-commit-persist'
import { createPtyIpcSpawnState } from '../../ipc/pty/ipc/spawn-state'
import type { PtySpawnIpcDeps } from '../../ipc/pty/ipc/spawn-types'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: () => ({ nth_repo_added: 2 })
}))
vi.mock('../../ssh/ssh-config-parser', () => ({
loadUserSshConfig: () => ({ hosts: [] }),
sshConfigHostsToTargets: () => []
}))
const SSH_HOST_ID = 'ssh:ssh-1'
const pane = {
worktreeId: 'repo-ssh::/remote/wt',
tabId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
leafId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
}
function sshSession(ptyId: string | null): WorkspaceSessionState {
return {
...getDefaultWorkspaceSession(),
tabsByWorktree: {
[pane.worktreeId]: [
{
id: pane.tabId,
worktreeId: pane.worktreeId,
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
ptyId
}
]
},
terminalLayoutsByTabId: {
[pane.tabId]: {
root: { type: 'leaf', leafId: pane.leafId },
activeLeafId: pane.leafId,
expandedLeafId: null,
...(ptyId ? { ptyIdsByLeafId: { [pane.leafId]: ptyId } } : {})
}
}
}
}
function boundPtyId(session: WorkspaceSessionState): string | undefined {
return session.terminalLayoutsByTabId[pane.tabId]?.ptyIdsByLeafId?.[pane.leafId]
}
// Main keeps its own layout over a window save, so a binding only a save carries would be lost.
it.each([
{ spawn: 'fresh', sessionId: undefined },
{ spawn: 'reattach', sessionId: 'ssh:ssh-1@@remote-pty' }
])('binds a window-spawned SSH pane ($spawn) in its SSH partition', async ({ sessionId }) => {
const { store } = await fixture()
store.setWorkspaceSession(sshSession(null), SSH_HOST_ID)
const local = store.getWorkspaceSession('local')
const ptyId = 'ssh:ssh-1@@remote-pty'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the commit's persist step runs; its store is real.
const deps = { runtime: new OrcaRuntimeService(store), store } as PtySpawnIpcDeps
const ctx = createPtyIpcSpawnState(deps, {
...pane,
connectionId: 'ssh-1',
...(sessionId ? { sessionId } : {}),
cols: 80,
rows: 24
})
ctx.result = { id: ptyId, incarnationId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc' }
ctx.stablePaneOwner = null
ctx.validatedLeafId = pane.leafId
await persistPtyIpcSpawnCommit(ctx)
expect(boundPtyId(store.getWorkspaceSession(SSH_HOST_ID))).toBe(ptyId)
expect(store.getWorkspaceSession('local').tabsByWorktree).toEqual(local.tabsByWorktree)
})
it('binds a runtime-started SSH pane in the SSH partition it was admitted to', async () => {
const { store } = await fixture()
const runtime = new OrcaRuntimeService(store)
const ptyId = 'ssh:ssh-1@@runtime-pty'
const incarnationId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'
runtime.onPtySpawned(ptyId, incarnationId)
runtime.beginPtyRegistration(ptyId, incarnationId)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only commit runs; its runtime/store are real and preflight-only dependencies are unreachable.
const deps = {
runtime,
store,
options: {},
sendPtySpawnedToRenderer: vi.fn()
} as PtyRuntimeControllerDeps
const ctx = createRuntimePtySpawnState(deps, {
...pane,
connectionId: 'ssh-1',
cols: 80,
rows: 24
})
ctx.result = { id: ptyId, incarnationId }
ctx.stablePaneOwner = null
await admitRuntimeSpawnPane(store, pane, 'ssh-1')
ctx.hostSessionBinding = { store, ...pane }
ctx.metadataLeafId = pane.leafId
await commitRuntimePtySpawn(ctx)
expect(boundPtyId(store.getWorkspaceSession(SSH_HOST_ID))).toBe(ptyId)
})