diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 898bfaee118..439c86b9cca 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -3638,8 +3638,9 @@ export function connectPanePty( // cooldown window while typing. Local panes keep the lenient gate. requireAuthoritativeLiveness: Boolean(transport.getConnectionId?.()) || isRemoteRuntimePtyId(undeliverablePtyId), - // Why only the rejected path: a stalled-pipeline remount reattaches to the - // same shell, so its half-typed line is still on screen and intact. + // Why only the rejected path: it is the only one whose remount can land on + // a fresh shell. A stalled-pipeline remount always reattaches to the same + // shell, so its half-typed line is still on screen and intact. endpointReplaced: providerRejected }) } diff --git a/src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts b/src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts index 0d9743dfc7b..63679bec8f1 100644 --- a/src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts @@ -56,7 +56,7 @@ describe('terminal input quarantine', () => { expect(isTerminalInputQuarantined(TAB)).toBe(false) }) - it('releases on an idle gap so a fresh command is never eaten', () => { + it('releases on an idle gap once a quarantined byte has been seen', () => { armTerminalInputQuarantine(TAB, 0) // Burst tail, no Enter — the user was still mid-line when the endpoint died. expect(shouldDropQuarantinedTerminalInput(TAB, 'c', REATTACH_MS)).toBe(true) @@ -66,6 +66,22 @@ describe('terminal input quarantine', () => { expect(isTerminalInputQuarantined(TAB)).toBe(false) }) + // The accepted cost of the cap, pinned so it is not "fixed" by shortening it: + // over-quarantine is visible (nothing echoes) and recoverable by retyping, + // while under-quarantine executes half a command. A tail takes seconds to + // type, so a cap short enough to spare this command would cut that tail + // mid-line and deliver its remainder to the fresh shell. + it('eats a fresh command typed inside the cap, terminator included', () => { + armTerminalInputQuarantine(TAB, 0) + // No byte yet, so the idle gate cannot release the first keystroke however + // long the user waited; from there normal typing never opens a 700ms gap. + let at = 1_500 + for (const char of 'ls -la\r') { + expect(shouldDropQuarantinedTerminalInput(TAB, char, at)).toBe(true) + at += 150 + } + }) + it('does not let the idle gate fire on the re-attach delay itself', () => { armTerminalInputQuarantine(TAB, 0) // First quarantined byte arrives well past the idle window, because the diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts b/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts index 4ed0b8344e7..ca3d23daede 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts @@ -38,10 +38,11 @@ type RecoveryRequest = { * registry doesn't own, and treating null as "proceed" would let a * disconnected remote pane churn reconnects on every cooldown window. */ requireAuthoritativeLiveness?: boolean - /** The provider rejected the write because its endpoint died, so re-attach - * lands on a *fresh* shell. Only then is the in-flight line mangled, and - * only then may input be quarantined — a recovery that keeps the same live - * shell would have a legitimate command eaten. */ + /** The provider rejected the write because its endpoint stopped accepting + * writes, so re-attach MAY land on a *fresh* shell (a respawn; a transient + * socket drop reconnects to the same sessions). Only this path can mangle the + * in-flight line, and only it may quarantine input — a recovery that always + * keeps the same live shell would have a legitimate command eaten. */ endpointReplaced?: boolean }