From e86573ac2fda675f315703adbe4f1a4b7c024177 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Tue, 7 Apr 2026 23:54:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20-r=20=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E4=B8=8B=E9=94=AE=E7=9B=98=E8=BE=93=E5=85=A5=E6=97=A0?= =?UTF-8?q?=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两个根因: 1. earlyInput 的 readableHandler 残留在 stdin 上 setAppCallbacks() 在反编译项目中从未被调用,导致 stopCapturingEarlyInput() 是 no-op,readableHandler 在 Ink 的 handleReadable 之前消费所有 stdin 数据。 修复:在 handleSetRawMode(true) 时移除非自身的 readable listeners。 2. React 19 layout effect cleanup 顺序问题 React 19 先运行新树的 layout effects,再清理旧树。 当旧树(showSetupDialog)比新树(launchResumeChooser) 有更多 useInput hooks 时,旧树 cleanup 把 rawModeEnabledCount 降到 0,错误关闭 raw mode。 修复:当 count=0 但仍有活跃 EventEmitter listeners 时恢复 count。 Co-Authored-By: Claude Opus 4.6 --- packages/@ant/ink/src/components/App.tsx | 27 ++++++++++++++++++++++-- src/dialogLaunchers.tsx | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/@ant/ink/src/components/App.tsx b/packages/@ant/ink/src/components/App.tsx index 6796e1370..8b7f5bdaa 100644 --- a/packages/@ant/ink/src/components/App.tsx +++ b/packages/@ant/ink/src/components/App.tsx @@ -315,9 +315,21 @@ export default class App extends PureComponent { if (this.rawModeEnabledCount === 0) { // Stop early input capture right before we add our own readable handler. // Both use the same stdin 'readable' + read() pattern, so they can't - // coexist -- our handler would drain stdin before Ink's can see it. - // The buffered text is preserved for REPL.tsx via consumeEarlyInput(). + // coexist -- the early capture handler would drain stdin before ours + // can see it. The buffered text is preserved for REPL.tsx via consumeEarlyInput(). defaultCallbacks.stopCapturingEarlyInput() + + // Safety net: remove any pre-existing readable listeners that aren't + // ours. In builds where setAppCallbacks() was never called, the early + // input capture's readableHandler remains attached and would consume + // all stdin data before our handleReadable sees it. + const existingListeners = stdin.listeners('readable') + for (const listener of existingListeners) { + if (listener !== this.handleReadable) { + stdin.removeListener('readable', listener as any) + } + } + stdin.ref() stdin.setRawMode(true) stdin.addListener('readable', this.handleReadable) @@ -363,6 +375,17 @@ export default class App extends PureComponent { // Disable raw mode only when no components left that are using it if (--this.rawModeEnabledCount === 0) { + // Guard: React 19 runs new useLayoutEffect setup before old cleanup when + // replacing the tree (e.g., showSetupDialog → launchResumeChooser). + // If the old tree had more useInput hooks than the new tree, the old + // cleanup over-decrements the count to 0 even though the new tree has + // active listeners. Detect this and fix the count instead of disabling. + const activeListeners = this.internal_eventEmitter.listenerCount('input') + if (activeListeners > 0) { + this.rawModeEnabledCount = activeListeners + return + } + this.props.stdout.write(DISABLE_MODIFY_OTHER_KEYS) this.props.stdout.write(DISABLE_KITTY_KEYBOARD) // Disable terminal focus reporting (DECSET 1004) diff --git a/src/dialogLaunchers.tsx b/src/dialogLaunchers.tsx index 5a6ed7372..ab903ca1b 100644 --- a/src/dialogLaunchers.tsx +++ b/src/dialogLaunchers.tsx @@ -168,7 +168,7 @@ export async function launchTeleportRepoMismatchDialog( /** * Site ~4903: ResumeConversation mount (interactive session picker). - * Uses renderAndRun, NOT showSetupDialog. Wraps in . + * Wraps in and uses renderAndRun. * Preserves original Promise.all parallelism between getWorktreePaths and imports. */ export async function launchResumeChooser(