From a9667209efdd116cdfb5397cefd857008e89e1c2 Mon Sep 17 00:00:00 2001 From: Juan Francisco Lebrero <101231690+frizynn@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:48:18 -0300 Subject: [PATCH] fix(interactive): clean up leaked SIGINT and extension selector listeners (#2172) - Wrap handleCtrlZ() suspend logic in try-catch so the SIGINT listener is removed if process.kill() or ui.stop() throws - Dispose previous extension selector in showExtensionSelector() before creating a new one, preventing promise leaks on rapid calls --- .../src/modes/interactive/interactive-mode.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts index df9d4d681..2f0beb331 100644 --- a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts @@ -1519,6 +1519,13 @@ export class InteractiveMode { options: string[], opts?: ExtensionUIDialogOptions, ): Promise { + // If a previous selector is still active, dispose it before creating a + // new one. This avoids leaking the previous promise and DOM state when + // showExtensionSelector is called rapidly. + if (this.extensionSelector) { + this.hideExtensionSelector(); + } + return new Promise((resolve) => { if (opts?.signal?.aborted) { resolve(undefined); @@ -2331,18 +2338,24 @@ export class InteractiveMode { const ignoreSigint = () => {}; process.on("SIGINT", ignoreSigint); - // Set up handler to restore TUI when resumed - process.once("SIGCONT", () => { + try { + // Set up handler to restore TUI when resumed + process.once("SIGCONT", () => { + process.removeListener("SIGINT", ignoreSigint); + this.ui.start(); + this.ui.requestRender(true); + }); + + // Stop the TUI (restore terminal to normal mode) + this.ui.stop(); + + // Send SIGTSTP to process group (pid=0 means all processes in group) + process.kill(0, "SIGTSTP"); + } catch { + // If suspend fails (e.g. SIGTSTP not supported), ensure the + // SIGINT listener doesn't leak. process.removeListener("SIGINT", ignoreSigint); - this.ui.start(); - this.ui.requestRender(true); - }); - - // Stop the TUI (restore terminal to normal mode) - this.ui.stop(); - - // Send SIGTSTP to process group (pid=0 means all processes in group) - process.kill(0, "SIGTSTP"); + } } private async handleFollowUp(): Promise {