fix(terminal): a WSL pane a relaunched window reattached keeps its folder when slept (#33496)

After an app relaunch, the window reattaches a live pane without its spawn environment, so the
daemon client never learned the pane's ORCA_PANE_KEY. Sleep then skipped the folder read for it,
and the pane woke at its spawn folder. A move's kill-at-quiesced rollback also put a pane `cd`'d
before the relaunch back at the project root (awin verify-11, 5e). Sleep now takes the pane key
from the daemon's session list when it knows the session's distro but not its key. That is the
same one `listSessions` the adoption of an unattached session already uses.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-25 03:11:22 -07:00
committed by GitHub
co-authored by Claude
parent 069d85f113
commit 08019f4293
2 changed files with 39 additions and 8 deletions
@@ -184,6 +184,28 @@ describe('a sleeping bridge WSL pane keeps its shell folder', () => {
expect(checkpointCwd(id)).toBe(`${WSL_CWD}\\deep\\er`)
})
// awin verify-11, 5e: a pane the relaunched window reattached (no spawn env, so no pane key)
// slept at its spawn folder.
it('reads it for a pane a relaunched app reattached without its spawn environment', async () => {
const before = new DaemonPtyAdapter({ socketPath, tokenPath, historyPath: historyDir() })
const { id } = await before.spawn({
cols: 80,
rows: 24,
cwd: WSL_CWD,
env: { ORCA_PANE_KEY: PANE_KEY },
sessionId: 'repo::/repo@@reattached'
})
lastSubprocess._simulateData('$ cd deep/er\r\n')
await before.disconnectOnly()
adapter = new DaemonPtyAdapter({ socketPath, tokenPath, historyPath: historyDir() })
await adapter.attach(id)
await adapter.shutdown(id, { immediate: true, keepHistory: true })
expect(readRows).toHaveBeenCalledWith('Ubuntu', expect.any(Number), { running: true })
expect(checkpointCwd(id)).toBe(`${WSL_CWD}\\deep\\er`)
})
it('keeps the spawn folder when the distro cannot be read', async () => {
readRows.mockResolvedValue(null)
adapter = new DaemonPtyAdapter({ socketPath, tokenPath, historyPath: historyDir() })
@@ -138,8 +138,10 @@ export abstract class DaemonPtyProcessInspection extends DaemonPtyBufferSnapshot
}
/**
* A session this client never spawned or reattached, such as one a relaunched app's window has
* not reopened: adopted before its Sleep, or that Sleep writes no checkpoint and keeps no folder.
* Before a Sleep, what only the daemon still knows about a session: one this client never
* spawned or reattached (a relaunched window has not reopened it) is adopted, or that Sleep
* writes no checkpoint; one it reattached has no spawn environment, so no pane key to read its
* WSL shell's folder by.
*/
protected async adoptListedSessionForSleep(
sessionId: string,
@@ -147,11 +149,16 @@ export abstract class DaemonPtyProcessInspection extends DaemonPtyBufferSnapshot
): Promise<void> {
// Why active too: a session this client suspended after a seed failure must stay suspended,
// or its fresh-only screen would replace the history it was restored from.
if (
!this.historyManager ||
this.historyManager.hasWriter(sessionId) ||
this.activeSessionIds.has(sessionId)
) {
const needsAdoption =
!!this.historyManager &&
!this.historyManager.hasWriter(sessionId) &&
!this.activeSessionIds.has(sessionId)
const needsPaneKey =
process.platform === 'win32' &&
!needsAdoption &&
!this.wslPaneKeysBySessionId.has(sessionId) &&
this.wslDistrosBySessionId.get(sessionId) !== undefined
if (!needsAdoption && !needsPaneKey) {
return
}
const result = await this.client.request<ListSessionsResult>(
@@ -160,8 +167,10 @@ export abstract class DaemonPtyProcessInspection extends DaemonPtyBufferSnapshot
remainingDaemonRequestTimeoutMs(deadlineMs)
)
const session = result.sessions.find((entry) => entry.sessionId === sessionId && entry.isAlive)
if (session) {
if (session && needsAdoption) {
await this.adoptLiveSession(session, { activeOnlyIfReconciled: true })
} else if (session?.wslDistro && session.paneKey) {
this.wslPaneKeysBySessionId.set(sessionId, session.paneKey)
}
}