From ca4294a032f2edebef6f1b27793d88665ae899ad Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:46:00 -0700 Subject: [PATCH] Clear pending input when attaching a different remote terminal handle When switching between different remote terminal handles, debounced input meant for the previous terminal could get incorrectly flushed and sent to the newly attached terminal. Fix this by clearing the batcher's pending state (text and byte counts) and invoking `inputBatcher.clear()` if the attached terminal handle changes. --- .../remote-runtime-pty-batching.ts | 2 + .../remote-runtime-pty-transport.test.ts | 44 +++++++++++++++++++ .../remote-runtime-pty-transport.ts | 8 +++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.ts index 8390affb5ac..8e1ee1b0ab5 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.ts @@ -50,6 +50,8 @@ export function createRemoteRuntimePtyTextBatcher( const clear = (): void => { clearTimer() + pending = '' + pendingBytes = 0 validationVersion += 1 validationTail = null } 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 82a2369fa13..8078125dc5e 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 @@ -342,6 +342,50 @@ describe('createRemoteRuntimePtyTransport', () => { expect(transport.isConnected()).toBe(false) }) + it('drops pending input when attaching a different remote terminal handle', async () => { + vi.useFakeTimers() + runtimeSubscribe.mockImplementation( + async (_args: unknown, callbacks: typeof subscriptionCallbacks) => { + subscriptionCallbacks = callbacks + return { unsubscribe: vi.fn(), sendBinary: subscriptionSendBinary } + } + ) + try { + const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport') + const transport = createRemoteRuntimePtyTransport('env-1', { + worktreeId: 'wt-1', + tabId: 'tab-1', + leafId: 'pane:1' + }) + + transport.attach({ + existingPtyId: 'remote:env-1@@terminal-old', + cols: 80, + rows: 24, + callbacks: {} + }) + expect(transport.sendInput('queued-for-old')).toBe(true) + + transport.attach({ + existingPtyId: 'remote:env-1@@terminal-new', + cols: 80, + rows: 24, + callbacks: {} + }) + runtimeCall.mockClear() + + await vi.advanceTimersByTimeAsync(10) + + expect(runtimeCall).not.toHaveBeenCalledWith( + expect.objectContaining({ + method: 'terminal.send' + }) + ) + } finally { + vi.useRealTimers() + } + }) + it('ignores stale attach subscription rejection after reattaching a newer remote terminal', async () => { let rejectOldSubscription: ((error: Error) => void) | null = null const newStream = { 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 534e4b64eb9..7bf5ffc02f5 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 @@ -507,7 +507,13 @@ export function createRemoteRuntimePtyTransport( storedCallbacks = options.callbacks currentRuntimeEnvironmentId = getRemoteRuntimePtyEnvironmentId(options.existingPtyId) ?? runtimeEnvironmentId - handle = getRemoteRuntimeTerminalHandle(options.existingPtyId) + const previousHandle = handle + const nextHandle = getRemoteRuntimeTerminalHandle(options.existingPtyId) + if (previousHandle && previousHandle !== nextHandle) { + // Why: debounced input is scoped by the current terminal handle at flush time. + inputBatcher.clear() + } + handle = nextHandle if (!handle) { connected = false remotePtyId = null