perf(terminal): prepaint parked SSH sessions (#12610)

* perf(terminal): prepaint parked SSH sessions

* fix(terminal): fence parked SSH prepaint
This commit is contained in:
Brennan Benson 2026-08-04 19:33:18 -07:00 committed by GitHub
parent 7287ca8ae2
commit b4dca4d12a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 345 additions and 22 deletions

View File

@ -20639,6 +20639,249 @@ describe('connectPanePty', () => {
expect(api.pty.signal).toHaveBeenCalledWith('leaf-session', 'SIGWINCH')
})
it('paints a parked SSH model before the remote connection settles', async () => {
const { connectPanePty } = await import('./pty-connection')
const sshPtyId = toAppSshPtyId('conn-1', 'relay-pty-1')
const sshConnect = createDeferred<SshConnectionState | null>()
const transport = createMockTransport()
transportFactoryQueue.push(transport)
vi.mocked(window.api.ssh.connect).mockReturnValue(sshConnect.promise)
vi.mocked(window.api.pty.getMainBufferSnapshot).mockResolvedValue({
data: 'PARKED-SSH-PAINTED-WITHOUT-NETWORK\r\n',
cols: 101,
rows: 31,
seq: 123,
source: 'headless'
})
await parkTabForReveal('tab-1', sshPtyId)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: sshPtyId }] },
ptyIdsByTabId: { 'tab-1': [sshPtyId] },
repos: [{ id: 'repo1', connectionId: 'conn-1' }],
sshConnectionStates: new Map([['conn-1', { status: 'disconnected' }]]),
deferredSshReconnectTargets: ['conn-1'],
deferredSshSessionIdsByTabId: { 'tab-1': sshPtyId }
}
const pane = createPane(1)
const { writes } = captureCallbackTerminalWrites(pane)
const binding = connectPanePty(
pane as never,
createManager(1) as never,
createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: sshPtyId }
}) as never
)
await flushAsyncTicks(20)
expect(window.api.ssh.connect).toHaveBeenCalledWith({ targetId: 'conn-1' })
expect(window.api.pty.getMainBufferSnapshot).toHaveBeenCalledOnce()
expect(writes.join('')).toContain('PARKED-SSH-PAINTED-WITHOUT-NETWORK')
expect(transport.connect).not.toHaveBeenCalled()
binding.dispose()
sshConnect.resolve({
targetId: 'conn-1',
status: 'connected',
error: null,
reconnectAttempt: 0
})
})
it('drops an in-flight parked SSH prepaint after its retry lease is replaced', async () => {
const { connectPanePty } = await import('./pty-connection')
const sshPtyId = toAppSshPtyId('conn-1', 'relay-pty-1')
const snapshot = createDeferred<{
data: string
cols: number
rows: number
seq: number
source: 'headless'
}>()
const sshConnect = createDeferred<SshConnectionState | null>()
const pendingRetry = {
attemptId: 'attempt-prepaint',
authority: {
targetId: 'conn-1',
providerEpoch: 'epoch-1',
connectionGeneration: 3
},
tabGeneration: 7,
startedAt: 1
}
transportFactoryQueue.push(createMockTransport())
vi.mocked(window.api.pty.getMainBufferSnapshot).mockReturnValue(snapshot.promise)
vi.mocked(window.api.ssh.connect).mockReturnValue(sshConnect.promise)
await parkTabForReveal('tab-1', sshPtyId)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: sshPtyId, generation: 7 }] },
ptyIdsByTabId: { 'tab-1': [sshPtyId] },
repos: [{ id: 'repo1', connectionId: 'conn-1' }],
sshConnectionStates: new Map([
[
'conn-1',
{
status: 'disconnected',
providerEpoch: 'epoch-1',
connectionGeneration: 3
}
]
]),
deferredSshReconnectTargets: ['conn-1'],
deferredSshSessionIdsByTabId: { 'tab-1': sshPtyId },
directSshPaneRetryByTabId: { 'tab-1': pendingRetry }
}
const pane = createPane(1)
const { writes } = captureCallbackTerminalWrites(pane)
const binding = connectPanePty(
pane as never,
createManager(1) as never,
createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: sshPtyId }
}) as never
)
await flushAsyncTicks(8)
expect(window.api.pty.getMainBufferSnapshot).toHaveBeenCalledOnce()
mockStoreState.sshConnectionStates = new Map([
[
'conn-1',
{
status: 'disconnected',
providerEpoch: 'epoch-1',
connectionGeneration: 4
}
]
])
mockStoreState.directSshPaneRetryByTabId = {
'tab-1': {
...pendingRetry,
attemptId: 'attempt-prepaint-new',
authority: { ...pendingRetry.authority, connectionGeneration: 4 }
}
}
snapshot.resolve({
data: 'OBSOLETE-LEASE-SNAPSHOT\r\n',
cols: 101,
rows: 31,
seq: 124,
source: 'headless'
})
await flushAsyncTicks(12)
expect(writes.join('')).not.toContain('OBSOLETE-LEASE-SNAPSHOT')
binding.dispose()
sshConnect.resolve(null)
await flushAsyncTicks(4)
})
it('does not prepaint a parked SSH snapshot owned by another connection', async () => {
const { connectPanePty } = await import('./pty-connection')
const foreignPtyId = toAppSshPtyId('conn-2', 'relay-pty-1')
const sshConnect = createDeferred<SshConnectionState | null>()
transportFactoryQueue.push(createMockTransport())
vi.mocked(window.api.ssh.connect).mockReturnValue(sshConnect.promise)
vi.mocked(window.api.pty.getMainBufferSnapshot).mockResolvedValue({
data: 'FOREIGN-CONNECTION-SNAPSHOT\r\n',
cols: 101,
rows: 31,
seq: 125,
source: 'headless'
})
await parkTabForReveal('tab-1', foreignPtyId)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: foreignPtyId }] },
ptyIdsByTabId: { 'tab-1': [foreignPtyId] },
repos: [{ id: 'repo1', connectionId: 'conn-1' }],
sshConnectionStates: new Map([['conn-1', { status: 'disconnected' }]]),
deferredSshReconnectTargets: ['conn-1'],
deferredSshSessionIdsByTabId: { 'tab-1': foreignPtyId }
}
const pane = createPane(1)
const { writes } = captureCallbackTerminalWrites(pane)
const binding = connectPanePty(
pane as never,
createManager(1) as never,
createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: foreignPtyId }
}) as never
)
await flushAsyncTicks(12)
expect(window.api.pty.getMainBufferSnapshot).not.toHaveBeenCalled()
expect(writes.join('')).not.toContain('FOREIGN-CONNECTION-SNAPSHOT')
binding.dispose()
sshConnect.resolve(null)
await flushAsyncTicks(4)
})
it('does not paint a delayed parked snapshot over an expired-session replacement', async () => {
const { connectPanePty } = await import('./pty-connection')
const sshPtyId = toAppSshPtyId('conn-1', 'relay-pty-expired')
const freshPtyId = toAppSshPtyId('conn-1', 'relay-pty-fresh')
const snapshot = createDeferred<{
data: string
cols: number
rows: number
seq: number
source: 'headless'
}>()
const transport = createMockTransport()
transport.connect.mockImplementation(async (opts) => {
if (opts.sessionId) {
opts.callbacks?.onError?.(`SSH_SESSION_EXPIRED: ${opts.sessionId}`)
return undefined
}
transport.getPtyId.mockReturnValue(freshPtyId)
return freshPtyId
})
transportFactoryQueue.push(transport)
vi.mocked(window.api.pty.getMainBufferSnapshot).mockReturnValue(snapshot.promise)
await parkTabForReveal('tab-1', sshPtyId)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: sshPtyId }] },
ptyIdsByTabId: { 'tab-1': [sshPtyId] },
repos: [{ id: 'repo1', connectionId: 'conn-1' }],
sshConnectionStates: new Map([['conn-1', { status: 'connected' }]]),
deferredSshReconnectTargets: ['conn-1'],
deferredSshSessionIdsByTabId: { 'tab-1': sshPtyId }
}
const pane = createPane(1)
const { writes } = captureCallbackTerminalWrites(pane)
const binding = connectPanePty(
pane as never,
createManager(1) as never,
createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: sshPtyId }
}) as never
)
await flushAsyncTicks(30)
expect(transport.connect).toHaveBeenCalledTimes(2)
snapshot.resolve({
data: 'EXPIRED-SESSION-SNAPSHOT\r\n',
cols: 101,
rows: 31,
seq: 126,
source: 'headless'
})
await flushAsyncTicks(20)
expect(writes.join('')).not.toContain('EXPIRED-SESSION-SNAPSHOT')
binding.dispose()
})
it('restores configured paired scrollback after an ordinary park reveal', async () => {
const { connectPanePty } = await import('./pty-connection')
const remotePtyId = 'remote:env-1@@terminal-1'

View File

@ -1054,6 +1054,7 @@ export function connectPanePty(
// that follows this mount, so connect time is the only moment a pane can tell
// a reveal remount from an in-place reattach.
let mountFollowsTerminalPark = isTerminalTabParked(deps.tabId)
let authoritativeReattachGeneration = 0
exposeE2eTerminalPtyOutputDebug()
let disposed = false
const structuralReplayCoordinator = createTerminalStructuralReplayCoordinator(pane.terminal)
@ -5147,6 +5148,7 @@ export function connectPanePty(
// about to unmount — so skip the doomed respawn instead of racing it.
return Promise.resolve(null)
}
authoritativeReattachGeneration += 1
clearPaneMode2031State()
clearHiddenOutputRestoreState()
// Why: a canceled old replay clear can preserve xterm's native
@ -7753,12 +7755,107 @@ export function connectPanePty(
return true
}
let parkedSshSnapshotPrefetch: {
ptyId: string
fetch: () => Promise<PtyBufferSnapshot | null>
} | null = null
const createSshMainModelSnapshotProbe = (
ptyId: string
): (() => Promise<PtyBufferSnapshot | null>) =>
memoizeSshReattachModelSnapshotProbe(async (): Promise<PtyBufferSnapshot | null> => {
const sshParkingEnabled = useAppStore.getState().settings?.terminalSshViewParking !== false
if (!shouldFetchSshReattachModelSnapshot({ ptyId, sshParkingEnabled })) {
return null
}
const snapshot = await resolveSshReattachModelSnapshotWithTimeout(
window.api.pty.getMainBufferSnapshot(ptyId, {
scrollbackRows: resolveHiddenRestoreScrollbackRows(pane.terminal.options.scrollback)
})
)
return snapshot &&
decideSshReattachPaintSource({ ptyId, sshParkingEnabled, snapshot }) ===
'main-model-snapshot'
? snapshot
: null
})
const getSshMainModelSnapshotProbe = (
ptyId: string
): (() => Promise<PtyBufferSnapshot | null>) => {
if (parkedSshSnapshotPrefetch?.ptyId !== ptyId) {
parkedSshSnapshotPrefetch = { ptyId, fetch: createSshMainModelSnapshotProbe(ptyId) }
}
return parkedSshSnapshotPrefetch.fetch
}
const prepaintParkedSshSnapshot = (ptyId: string | null): void => {
const parsedPtyId = ptyId ? parseAppSshPtyId(ptyId) : null
if (
!ptyId ||
!mountFollowsTerminalPark ||
parsedPtyId?.connectionId !== connectionId ||
!capturedDirectSshRetryLeaseMatches()
) {
return
}
const capturedGeneration = authoritativeReattachGeneration
const isCurrent = (): boolean =>
!disposed &&
mountFollowsTerminalPark &&
authoritativeReattachGeneration === capturedGeneration &&
capturedDirectSshRetryLeaseMatches()
const fetchSnapshot = getSshMainModelSnapshotProbe(ptyId)
void fetchSnapshot()
.then(async (snapshot) => {
if (!snapshot || !isCurrent()) {
return
}
await structuralReplayCoordinator.run(
async () => {
if (!isCurrent()) {
return
}
const modelData = `${snapshot.scrollbackAnsi ?? ''}${snapshot.data}`
rememberReattachPayloadAgentSignal(modelData, { fullScreenReplay: true })
if (
hasPositiveTerminalDimensions(snapshot.cols, snapshot.rows) &&
(pane.terminal.cols !== snapshot.cols || pane.terminal.rows !== snapshot.rows)
) {
suppressStructuralReplayPtyResize = true
try {
pane.terminal.resize(snapshot.cols, snapshot.rows)
} finally {
suppressStructuralReplayPtyResize = false
}
}
kittyKeyboardModes.scanReplay(modelData)
for (const replayChunk of buildMainModelSnapshotReplayWrites(snapshot)) {
writeReplayData(replayChunk)
}
writeReplayData(reattachReplayResetSequence(modelData))
if (snapshot.pendingEscapeTailAnsi) {
writeReplayData(snapshot.pendingEscapeTailAnsi)
}
recordTerminalOutput(pane.terminal)
await waitForTerminalReplayWritesParsed(pane.terminal)
if (isCurrent()) {
manager.rebuildPaneWebgl(pane.id)
}
},
{ shouldRestore: isCurrent }
)
})
.catch(() => {})
}
const handleReattachResult = async (
result: PtyConnectResult | string | void,
staleSessionId?: string | null,
coldRestoreStartup?: ColdRestoreAgentResumeStartup | null,
attemptGeneration = transportStreamGeneration
): Promise<boolean> => {
authoritativeReattachGeneration += 1
if (disposed) {
return false
}
@ -7886,28 +7983,7 @@ export function connectPanePty(
// deadlock on the coordinator's tail chain.
// Memoized: the prefetch and the payload task share one probe result, so a
// null prefetch can never buy a second timeout before the relay paint.
const fetchSshMainModelReattachSnapshot = memoizeSshReattachModelSnapshotProbe(
async (): Promise<PtyBufferSnapshot | null> => {
const sshParkingEnabled =
useAppStore.getState().settings?.terminalSshViewParking !== false
if (!shouldFetchSshReattachModelSnapshot({ ptyId, sshParkingEnabled })) {
return null
}
const snapshot = await resolveSshReattachModelSnapshotWithTimeout(
window.api.pty.getMainBufferSnapshot(ptyId, {
scrollbackRows: resolveHiddenRestoreScrollbackRows(pane.terminal.options.scrollback)
})
)
if (
!snapshot ||
decideSshReattachPaintSource({ ptyId, sshParkingEnabled, snapshot }) !==
'main-model-snapshot'
) {
return null
}
return snapshot
}
)
const fetchSshMainModelReattachSnapshot = getSshMainModelSnapshotProbe(ptyId)
// Why consume-once: only the first reattach of a reveal remount may pay
// the probe; a later in-place reconnect on this same mount must not buy a
// second timeout before the relay paint.
@ -8189,6 +8265,7 @@ export function connectPanePty(
const attachRetainedLegacyPty = (ptyId: string): boolean => {
try {
authoritativeReattachGeneration += 1
clearPaneMode2031State()
clearHiddenOutputRestoreState()
const outputCallbacks = captureTransportOutputCallbacks(reportError)
@ -8246,6 +8323,8 @@ export function connectPanePty(
)
const legacyWorkerOwnsPane = isLegacyWorkerAutomaticResumeBlocked()
if (gate.enterDeferredFlow && (!legacyWorkerOwnsPane || !gate.sshConnected)) {
// Paint main's parked model while SSH recovery continues off the render path.
prepaintParkedSshSnapshot(pendingSessionId)
void (async () => {
// Why: for a passphrase target with no cached credential, don't auto-fire ssh.connect — a prompt popping just from focusing a tab / Cmd+J would surprise the user.
// Wait for a user-initiated connect first; no-passphrase targets return false here and auto-connect as before.
@ -8608,6 +8687,7 @@ export function connectPanePty(
if (deferredReattachSessionId) {
allowInitialIdleCacheSeed = true
recordPtyConnectDiagnostic(`pane=${pane.id} -> REATTACH ${deferredReattachSessionId}`)
prepaintParkedSshSnapshot(deferredReattachSessionId)
// Why: pre-signal (declare) before the reattach connect so the cooperation gate suppresses the daemon seed for this paneKey; Electron preserves IPC order.
// See docs/mobile-prefer-renderer-scrollback.md (Renderer-side prerequisite requirement #4).