From 08019f4293e5e8a86f695e455dfeb3b5c5b2656b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:11:22 -0700 Subject: [PATCH] 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 --- .../daemon-pty-adapter-wsl-sleep-cwd.test.ts | 22 ++++++++++++++++ .../daemon/daemon-pty-process-inspection.ts | 25 +++++++++++++------ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/daemon/daemon-pty-adapter-wsl-sleep-cwd.test.ts b/src/main/daemon/daemon-pty-adapter-wsl-sleep-cwd.test.ts index 613cf816ef3..78c90ed8be9 100644 --- a/src/main/daemon/daemon-pty-adapter-wsl-sleep-cwd.test.ts +++ b/src/main/daemon/daemon-pty-adapter-wsl-sleep-cwd.test.ts @@ -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() }) diff --git a/src/main/daemon/daemon-pty-process-inspection.ts b/src/main/daemon/daemon-pty-process-inspection.ts index a1f9caeca23..65f221a4584 100644 --- a/src/main/daemon/daemon-pty-process-inspection.ts +++ b/src/main/daemon/daemon-pty-process-inspection.ts @@ -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 { // 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( @@ -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) } }