From 16958cdbe75ddf7514c002307e6264fde8871983 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:22:05 +0800 Subject: [PATCH] fix(pty): observe readiness before exact probe threshold --- packages/pty/pty-local/src/session.ts | 16 ++++++++----- packages/pty/pty-local/tests/session.spec.ts | 25 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/pty/pty-local/src/session.ts b/packages/pty/pty-local/src/session.ts index a11201bc6c..46f3be2ca8 100644 --- a/packages/pty/pty-local/src/session.ts +++ b/packages/pty/pty-local/src/session.ts @@ -125,7 +125,8 @@ class LocalSendOperation implements PtySendOperation { acceptsStdinWait(pgid: number, waiting: boolean): boolean { // The same group may still expose the wait that existed before terminal.write. - // It becomes post-write evidence only after polling observes it leave that wait. + // Observe every poll so a departure before the exact-settlement threshold + // still makes a later return to that wait post-write evidence. if (pgid !== this.initialForegroundPgid) return waiting if (!waiting) this.initialForegroundLeftWait = true return waiting && this.initialForegroundLeftWait @@ -338,12 +339,15 @@ export class LocalPtySession implements PtyBackendSession { } const elapsed = Date.now() - operation.startedAt const startupHasOutput = !this.initializing || this.scrollback.snapshot().text.length > 0 - if (startupHasOutput && elapsed >= this.config.exactProbeAfterMs) { + let acceptsStdinWait = false + if (startupHasOutput) { const pgid = this.inspector.foregroundPgid(this.pid) - if (pgid !== undefined && operation.acceptsStdinWait(pgid, this.inspector.isStdinWaiting(pgid))) { - this.settleActive('stdin_read') - return - } + acceptsStdinWait = pgid !== undefined + && operation.acceptsStdinWait(pgid, this.inspector.isStdinWaiting(pgid)) + } + if (elapsed >= this.config.exactProbeAfterMs && acceptsStdinWait) { + this.settleActive('stdin_read') + return } // A prompt candidate can race bash's foreground handoff, but an interactive // child also inherits PROMPT_COMMAND. Silence therefore remains the bound diff --git a/packages/pty/pty-local/tests/session.spec.ts b/packages/pty/pty-local/tests/session.spec.ts index 92ac828154..53ce61eebc 100644 --- a/packages/pty/pty-local/tests/session.spec.ts +++ b/packages/pty/pty-local/tests/session.spec.ts @@ -144,6 +144,31 @@ describe('LocalPtySession readiness and output', () => { expect((await operation.done).waitReason).toBe('stdin_read') }) + it('tracks a pre-write wait exit before exact probing begins', async () => { + vi.useFakeTimers() + const terminal = new FakeTerminal() + const inspector = new FakeInspector() + const session = new LocalPtySession(terminal.asPty(), inspector, config({ + exactProbeAfterMs: 50, + idleSilenceMs: 100, + timeoutMs: 200, + })) + await initialize(session, terminal) + + inspector.waiting = true + const operation = session.startSend({ text: 'fast command', submit: true }) + let settled = false + void operation.done.then(() => { settled = true }) + inspector.waiting = false + await vi.advanceTimersByTimeAsync(10) + inspector.waiting = true + await vi.advanceTimersByTimeAsync(30) + expect(settled).toBe(false) + await vi.advanceTimersByTimeAsync(10) + expect(settled).toBe(true) + expect((await operation.done).waitReason).toBe('stdin_read') + }) + it('distinguishes inferred idle, timeout, exit signal, and operation reads', async () => { vi.useFakeTimers() const terminal = new FakeTerminal()