From dcdde2a285d0071c5b817f8704ebcc46243136b2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:19:17 -0700 Subject: [PATCH] fix(terminal): preserve current mirror input queue --- .../remote-runtime-pty-transport.test.ts | 48 +++++++++++++++++++ .../remote-runtime-pty-transport.ts | 6 ++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts index d61c71dfe44..d0b63186d48 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts @@ -889,6 +889,54 @@ describe('createRemoteRuntimePtyTransport', () => { transport.destroy?.() }) + it('keeps the current mirror input queue when an older connect settles', async () => { + const healthyRuntimeCall = runtimeCall.getMockImplementation() + let releaseFirstActivation = (): void => {} + const firstActivationGate = new Promise((resolve) => { + releaseFirstActivation = resolve + }) + let activationCalls = 0 + runtimeCall.mockImplementation((request: { method: string; params?: unknown }) => { + if (request.method === 'session.tabs.activate' && activationCalls++ === 0) { + return firstActivationGate.then(() => healthyRuntimeCall?.(request)) + } + if (request.method === 'terminal.send') { + return Promise.resolve({ ok: true, result: { send: { accepted: true } } }) + } + return healthyRuntimeCall?.(request) + }) + const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport') + const transport = createRemoteRuntimePtyTransport('env-1', { + worktreeId: 'wt-1', + tabId: 'web-terminal-host-tab-1', + leafId: 'pane:1' + }) + + const staleConnect = transport.connect({ url: '', cols: 100, rows: 30, callbacks: {} }) + await vi.waitFor(() => expect(activationCalls).toBe(1)) + await transport.connect({ url: '', cols: 100, rows: 30, callbacks: {} }) + expect(transport.sendInput('typed-for-current-connect\r')).toBe(true) + const acceptedInput = transport.sendInputAccepted?.('accepted-for-current-connect\r') + + releaseFirstActivation() + await staleConnect + expect(transport.sendInput('typed-after-stale-connect\r')).toBe(true) + emitSnapshot(latestSubscribePayload().streamId, 'authoritative state') + + await vi.waitFor(() => + expect(runtimeCall).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'terminal.send', + params: expect.objectContaining({ + text: 'typed-for-current-connect\raccepted-for-current-connect\rtyped-after-stale-connect\r' + }) + }) + ) + ) + await expect(acceptedInput).resolves.toBe(true) + transport.destroy?.() + }) + it('drops pre-connect web mirror input when attach takes ownership', async () => { const healthyRuntimeCall = runtimeCall.getMockImplementation() let releasePaneAuthority = (): void => {} diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts index 2bf1e74a44c..b0a3ed893ac 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts @@ -2061,7 +2061,7 @@ export function createRemoteRuntimePtyTransport( undefined, connectLifecycleEpoch ) - if (!result) { + if (!result && lifecycleEpoch === connectLifecycleEpoch) { clearInputQueuedWhileConnecting() } return result @@ -2274,7 +2274,9 @@ export function createRemoteRuntimePtyTransport( surfaceErrorMessage(message) } } - clearInputQueuedWhileConnecting() + if (lifecycleEpoch === connectLifecycleEpoch) { + clearInputQueuedWhileConnecting() + } return undefined } },