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