Skip to content

Commit 96700db

Browse files
fix(terminal): skip Ctrl+C retry when terminal is reused by a different process (#266)
1 parent 62f95ec commit 96700db

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

src/integrations/terminal/TerminalProcess.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,12 @@ export class TerminalProcess extends BaseTerminalProcess {
313313

314314
const terminal = this.terminalRef.deref()
315315

316-
if (!terminal || !terminal.busy) {
316+
// Stop if the terminal is gone, idle, or has already moved on to a different
317+
// command. If the original command exits and the terminal is reused before this
318+
// tick fires, `terminal.busy` can be true for the NEW command while
319+
// `terminal.process` points at a different TerminalProcess — re-sending Ctrl+C
320+
// then would interrupt an unrelated command, so we bail out.
321+
if (!terminal || !terminal.busy || terminal.process !== this) {
317322
return
318323
}
319324

src/integrations/terminal/__tests__/TerminalProcess.spec.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ describe("TerminalProcess", () => {
196196

197197
beforeEach(() => {
198198
vi.useFakeTimers()
199+
// abort() runs against the terminal's *current* process; mirror that wiring so
200+
// the reuse guard (terminal.process === this) lets the retry loop proceed.
201+
mockTerminalInfo.process = terminalProcess
199202
})
200203

201204
afterEach(() => {
@@ -242,8 +245,29 @@ describe("TerminalProcess", () => {
242245
await vi.advanceTimersByTimeAsync(RETRY_DELAY_MS)
243246
expect(mockTerminal.sendText).toHaveBeenCalledTimes(2)
244247

245-
// Process exits before the next tick.
246-
mockTerminalInfo.busy = false
248+
// Process exits before the next tick — drive the real completion lifecycle
249+
// (shellExecutionComplete clears busy and releases terminal.process) rather than
250+
// mutating busy directly, so the test exercises the production wiring.
251+
mockTerminalInfo.shellExecutionComplete({ exitCode: 0 })
252+
await vi.advanceTimersByTimeAsync(RETRY_DELAY_MS * MAX_ATTEMPTS)
253+
254+
expect(mockTerminal.sendText).toHaveBeenCalledTimes(2)
255+
})
256+
257+
it("stops re-sending Ctrl+C if the terminal is reused for a different process (#266)", async () => {
258+
mockTerminalInfo.busy = true
259+
260+
terminalProcess.abort()
261+
expect(mockTerminal.sendText).toHaveBeenCalledTimes(1)
262+
263+
// First retry tick: still busy, re-send.
264+
await vi.advanceTimersByTimeAsync(RETRY_DELAY_MS)
265+
expect(mockTerminal.sendText).toHaveBeenCalledTimes(2)
266+
267+
// The original command exits and the terminal is reused for a NEW command before
268+
// the next tick: terminal stays busy, but terminal.process now points at a
269+
// different process. The retry must not interrupt that unrelated command.
270+
mockTerminalInfo.process = new TestTerminalProcess(mockTerminalInfo)
247271
await vi.advanceTimersByTimeAsync(RETRY_DELAY_MS * MAX_ATTEMPTS)
248272

249273
expect(mockTerminal.sendText).toHaveBeenCalledTimes(2)

0 commit comments

Comments
 (0)