diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index d215fd25bd4..76c11c040a5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -75,9 +75,9 @@ import { waitForTerminalReplayWritesParsed } from './replay-guard' import { - armTerminalWriteStallWatch, isTerminalWritePipelineCertifiedDead, - registerUndeliverableWriteHandler + registerUndeliverableWriteHandler, + requestTerminalWritePipelineProbe } from '@/lib/pane-manager/terminal-write-pipeline-health' import { captureTerminalPaneRecoveryGeneration, @@ -3358,6 +3358,8 @@ export function connectPanePty( const MAX_DEFERRED_REATTACH_LIVE_CHUNKS = 1_024 const markTerminalInputSent = (): void => { lastTerminalInputAt = performance.now() + // Why: input must probe a wedged xterm even when the PTY produces no renderer output. + requestTerminalWritePipelineProbe(pane.terminal) } const recordTerminalInputForHibernation = (): void => { useAppStore.getState().recordTerminalInput(cacheKey) @@ -3605,10 +3607,6 @@ export function connectPanePty( // pre-existing session when a late reattach resolves, so a remount racing // a slow-but-alive connect costs a wasted view rebuild, not a shell. const TRANSPORT_CONNECT_SETTLE_GRACE_MS = 60_000 - // Why: user input must probe a wedged xterm even when no PTY output reaches the renderer. - const armWritePipelineWatchForUserInput = (): void => { - armTerminalWriteStallWatch(pane.terminal, { requireProbe: true }) - } const requestRecoveryForUndeliverableInput = (): void => { if (transport.isConnected?.() && transport.getPtyId() !== null) { return @@ -3734,7 +3732,6 @@ export function connectPanePty( } clearPendingTerminalInputIntent() markTerminalInputSent() - armWritePipelineWatchForUserInput() const writePromise = transport .sendInputAccepted(data) .then((accepted) => { @@ -3760,7 +3757,6 @@ export function connectPanePty( claimViewportForUserActivity() if (transport.sendInput(data)) { markAcceptedTerminalInputSent() - armWritePipelineWatchForUserInput() observeAcceptedShellCommandInput(data) observeAcceptedTerminalInput(data, intent) } else { @@ -3772,7 +3768,6 @@ export function connectPanePty( claimViewportForUserActivity() if (transport.sendInput(data)) { markAcceptedTerminalInputSent() - armWritePipelineWatchForUserInput() observeAcceptedShellCommandInput(data) observeAcceptedTerminalInput(data) observeSentTerminalInputIntent(data) diff --git a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts index 2df009b36a4..b8299c6652a 100644 --- a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts +++ b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts @@ -9,6 +9,7 @@ import { isTerminalWritePipelineCertifiedDead, notifyUndeliverableWrite, registerUndeliverableWriteHandler, + requestTerminalWritePipelineProbe, settleTerminalWriteStallWatch, WRITE_PIPELINE_STALL_CHECK_MS } from './terminal-write-pipeline-health' @@ -113,14 +114,15 @@ describe('terminal write pipeline health', () => { _resetWritePipelineHealthForTests(terminal) }) - it('keeps a required probe armed through unrelated parse progress', () => { + it('keeps a requested probe armed through unrelated parse progress', () => { vi.useFakeTimers() const terminal = makeTerminal() terminal.dropCallbacks = true const handler = vi.fn() registerUndeliverableWriteHandler(terminal, handler) - armTerminalWriteStallWatch(terminal, { requireProbe: true }) + armTerminalWriteStallWatch(terminal) + requestTerminalWritePipelineProbe(terminal) settleTerminalWriteStallWatch(terminal) vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS * 2) diff --git a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts index bc3729b898c..df1e231fda5 100644 --- a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts +++ b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts @@ -42,10 +42,12 @@ export function hasTerminalParseProgressSince(terminal: object, generation: numb return captureTerminalParseProgressGeneration(terminal) !== generation } +type StallWatchMode = 'output-completion' | 'fifo-probe' + type StallWatch = { timer: ReturnType onCertifiedDead?: () => void - requireProbe: boolean + mode: StallWatchMode } const stallWatchByTerminal = new WeakMap() @@ -111,27 +113,28 @@ export function isTerminalWritePipelineCertifiedDead(terminal: object): boolean * alive (slow parse), re-arm and keep waiting; probe silent for another * interval → dead, notify. */ -export function armTerminalWriteStallWatch( +function armTerminalWritePipelineWatch( terminal: WriteTarget, options: { + mode: StallWatchMode onCertifiedDead?: () => void - /** Ignore unrelated write completions so user-input health checks reach their FIFO probe. */ - requireProbe?: boolean stallCheckMs?: number - } = {} + } ): void { if (certifiedDeadTerminals.has(terminal)) { return } const existingWatch = stallWatchByTerminal.get(terminal) if (existingWatch) { - existingWatch.requireProbe ||= options.requireProbe === true + if (options.mode === 'fifo-probe') { + existingWatch.mode = 'fifo-probe' + } return } const stallCheckMs = options.stallCheckMs ?? WRITE_PIPELINE_STALL_CHECK_MS const watch: StallWatch = { + mode: options.mode, onCertifiedDead: options.onCertifiedDead, - requireProbe: options.requireProbe === true, timer: setTimeout(probeForStall, stallCheckMs) } const certifyDead = (): void => certifyTerminalWritePipelineDead(terminal, watch) @@ -167,6 +170,21 @@ export function armTerminalWriteStallWatch( stallWatchByTerminal.set(terminal, watch) } +export function armTerminalWriteStallWatch( + terminal: WriteTarget, + options: { onCertifiedDead?: () => void; stallCheckMs?: number } = {} +): void { + armTerminalWritePipelineWatch(terminal, { ...options, mode: 'output-completion' }) +} + +/** Require the active watch to reach its FIFO probe before it may settle. */ +export function requestTerminalWritePipelineProbe( + terminal: WriteTarget, + options: { stallCheckMs?: number } = {} +): void { + armTerminalWritePipelineWatch(terminal, { ...options, mode: 'fifo-probe' }) +} + /** Cancel a pending watch without claiming that any bytes parsed. */ export function cancelTerminalWriteStallWatch(terminal: object): void { const watch = stallWatchByTerminal.get(terminal) @@ -177,10 +195,10 @@ export function cancelTerminalWriteStallWatch(terminal: object): void { stallWatchByTerminal.delete(terminal) } -/** Write completed normally — record progress and drop ordinary write watches. */ +/** Write completed normally — the pipeline is healthy; drop any pending watch. */ export function settleTerminalWriteStallWatch(terminal: object): void { recordTerminalParseProgress(terminal) - if (stallWatchByTerminal.get(terminal)?.requireProbe) { + if (stallWatchByTerminal.get(terminal)?.mode === 'fifo-probe') { return } cancelTerminalWriteStallWatch(terminal)