From 7abe2a0aed4e36b4153f3520149f8e19f086d319 Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Wed, 12 Nov 2025 12:00:52 +0800 Subject: [PATCH] fix: proper SIGINT handler and output in nonInteractive mode --- docs/cli/_meta.ts | 1 - packages/cli/src/gemini.tsx | 15 ++++++++----- packages/cli/src/nonInteractive/session.ts | 10 --------- packages/cli/src/nonInteractiveCli.ts | 26 +++++++++++++--------- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/docs/cli/_meta.ts b/docs/cli/_meta.ts index 239757f1..1557b595 100644 --- a/docs/cli/_meta.ts +++ b/docs/cli/_meta.ts @@ -5,7 +5,6 @@ export default { commands: 'Commands', configuration: 'Configuration', 'configuration-v1': 'Configuration (v1)', - 'structured-output': 'Structured Output', themes: 'Themes', tutorials: 'Tutorials', 'keyboard-shortcuts': 'Keyboard Shortcuts', diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 2ff352ee..3b87f07e 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -220,12 +220,6 @@ export async function main() { } const isDebugMode = cliConfig.isDebugMode(argv); - const consolePatcher = new ConsolePatcher({ - stderr: true, - debugMode: isDebugMode, - }); - consolePatcher.patch(); - registerCleanup(consolePatcher.cleanup); dns.setDefaultResultOrder( validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder), @@ -350,6 +344,15 @@ export async function main() { process.exit(0); } + // Setup unified ConsolePatcher based on interactive mode + const isInteractive = config.isInteractive(); + const consolePatcher = new ConsolePatcher({ + stderr: isInteractive, + debugMode: isDebugMode, + }); + consolePatcher.patch(); + registerCleanup(consolePatcher.cleanup); + const wasRaw = process.stdin.isRaw; let kittyProtocolDetectionComplete: Promise | undefined; if (config.isInteractive() && !wasRaw && process.stdin.isTTY) { diff --git a/packages/cli/src/nonInteractive/session.ts b/packages/cli/src/nonInteractive/session.ts index 8c4fd173..75f780ee 100644 --- a/packages/cli/src/nonInteractive/session.ts +++ b/packages/cli/src/nonInteractive/session.ts @@ -16,7 +16,6 @@ */ import type { Config } from '@qwen-code/qwen-code-core'; -import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js'; import { StreamJsonInputReader } from './io/StreamJsonInputReader.js'; import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js'; import { ControlContext } from './control/ControlContext.js'; @@ -96,7 +95,6 @@ class SessionManager { private dispatcher: ControlDispatcher | null = null; private controlService: ControlService | null = null; private controlSystemEnabled: boolean | null = null; - private consolePatcher: ConsolePatcher; private debugMode: boolean; private shutdownHandler: (() => void) | null = null; private initialPrompt: CLIUserMessage | null = null; @@ -113,11 +111,6 @@ class SessionManager { this.abortController = new AbortController(); this.initialPrompt = initialPrompt ?? null; - this.consolePatcher = new ConsolePatcher({ - stderr: true, - debugMode: this.debugMode, - }); - this.inputReader = new StreamJsonInputReader(); this.outputAdapter = new StreamJsonOutputAdapter( config, @@ -232,8 +225,6 @@ class SessionManager { */ async run(): Promise { try { - this.consolePatcher.patch(); - if (this.debugMode) { console.error('[SessionManager] Starting session', this.sessionId); } @@ -264,7 +255,6 @@ class SessionManager { await this.shutdown(); throw error; } finally { - this.consolePatcher.cleanup(); // Ensure signal handlers are always cleaned up even if shutdown wasn't called this.cleanupSignalHandlers(); } diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 2ff8ea03..8e5a9c90 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -25,7 +25,6 @@ import { StreamJsonOutputAdapter } from './nonInteractive/io/StreamJsonOutputAda import type { ControlService } from './nonInteractive/control/ControlService.js'; import { handleSlashCommand } from './nonInteractiveCliCommands.js'; -import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { handleAtCommand } from './ui/hooks/atCommandProcessor.js'; import { handleError, @@ -67,11 +66,6 @@ export async function runNonInteractive( options: RunNonInteractiveOptions = {}, ): Promise { return promptIdContext.run(prompt_id, async () => { - const consolePatcher = new ConsolePatcher({ - stderr: true, - debugMode: config.getDebugMode(), - }); - // Create output adapter based on format let adapter: JsonOutputAdapterInterface | undefined; const outputFormat = config.getOutputFormat(); @@ -102,12 +96,22 @@ export async function runNonInteractive( } }; + const geminiClient = config.getGeminiClient(); + const abortController = options.abortController ?? new AbortController(); + + // Setup signal handlers for graceful shutdown + const shutdownHandler = () => { + if (config.getDebugMode()) { + console.error('[runNonInteractive] Shutdown signal received'); + } + abortController.abort(); + }; + try { - consolePatcher.patch(); process.stdout.on('error', stdoutErrorHandler); - const geminiClient = config.getGeminiClient(); - const abortController = options.abortController ?? new AbortController(); + process.on('SIGINT', shutdownHandler); + process.on('SIGTERM', shutdownHandler); let initialPartList: PartListUnion | null = extractPartsFromUserMessage( options.userMessage, @@ -362,7 +366,9 @@ export async function runNonInteractive( handleError(error, config); } finally { process.stdout.removeListener('error', stdoutErrorHandler); - consolePatcher.cleanup(); + // Cleanup signal handlers + process.removeListener('SIGINT', shutdownHandler); + process.removeListener('SIGTERM', shutdownHandler); if (isTelemetrySdkInitialized()) { await shutdownTelemetry(config); }