From cf1558d82bf8df5e6caaab490650da5307d11b57 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:41:45 -0700 Subject: [PATCH] fix(pty-connection): correct sequencing and state bugs in spawn and reat - Fix terminal tail slice to take prefix instead of suffix, preserving escape sequence markers needed by next scan - Clear pending pane serializer when direct SSH retry PTY is unclaimed - Initialize interrupt status baseline to undefined so first input advances sequence counter - Bump reattach generation only after confirming current attempt owns the stream, preventing superseded results from canceling in-flight prepaint --- .../pty-connection/fresh-spawn-follow-reset.ts | 4 +++- .../terminal-pane/pty-connection/fresh-spawn-start.ts | 10 +++++++++- .../pty-connection/interrupt-input-intent.ts | 5 ++++- .../pty-connection/reattach-result-handler.ts | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts index b1fb8d1d642..cda86479fbc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-follow-reset.ts @@ -85,7 +85,9 @@ export function bindFreshSpawnFollowReset(session: ConnectPanePtySession): void return '' } } - return tail.slice(-TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS) + // Why: keep the head — a suffix slice of an oversized parameter run drops + // the `\x1b[` the next scan needs to recognize the carried sequence. + return tail.slice(0, TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS) } session.foregroundRendererRiskOutputPrefersRenderRefresh = function (data: string): boolean { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts index c722170dc6b..0110f8a50d7 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts @@ -114,6 +114,12 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { : session.transport.getPtyId() if (resolvedPtyId && !session.claimCapturedDirectSshRetryPty(resolvedPtyId)) { session.finishReattachLiveDataDeferral(false, outputCallbacks.generation) + // Why: an outstanding declare keeps main's cooperation gate suppressing + // this paneKey's daemon-snapshot seed until something releases it. + const gen = await preSignalPromise + if (typeof gen === 'number') { + void window.api.pty.clearPendingPaneSerializer(session.cacheKey, gen).catch(() => {}) + } return null } const connectResult = @@ -193,7 +199,9 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void { session.reconcilePtySizeAfterSpawn(resolvedPtyId, session.cols, session.rows) } const gen = await preSignalPromise - if (resolvedPtyId && (typeof gen === 'number' || isRemoteRuntimePtyId(resolvedPtyId))) { + // Why: a bound PTY owns the renderer serializer even when the declare was + // rejected; the gen token only settles or clears the pending declaration. + if (resolvedPtyId) { if (!isRemoteRuntimePtyId(resolvedPtyId) || !hasPtySerializer(resolvedPtyId)) { session.registerPaneSerializerFor(resolvedPtyId) } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts b/src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts index b11b289f1cc..1206eeffc5a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts @@ -162,7 +162,10 @@ export function installInterruptInputIntent(session: ConnectPanePtySession): voi session.questionAnsweredInference.observeSentTerminalInput(data) } session.pendingTerminalInputWrite = null - session.sequencedInterruptStatusBaseline = 0 + // Why: an unset baseline must differ from the first `?? null` snapshot so the + // first acknowledged input still advances the sequence. + session.sequencedInterruptStatusBaseline = undefined + session.interruptStatusBaselineSequence = 0 session.setPendingTerminalInputWrite = (promise: Promise): void => { session.pendingTerminalInputWrite = promise void promise.finally(() => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts index 29998677fea..bb45572b7a1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler.ts @@ -57,13 +57,15 @@ export function bindHandleReattachResult(sessionBag: ConnectPanePtySession): voi coldRestoreStartup?: ColdRestoreAgentResumeStartup | null, attemptGeneration = session.transportStreamGeneration ): Promise => { - session.authoritativeReattachGeneration += 1 if (session.disposed) { return false } if (attemptGeneration !== session.transportStreamGeneration) { return false } + // Why: bump only once this attempt owns the stream, or a superseded result + // would cancel the current attempt's in-flight snapshot prepaint. + session.authoritativeReattachGeneration += 1 const connectResult = result && typeof result === 'object' && 'id' in result ? (result as PtyConnectResult) : null