From f9e77eb49c02a0b22cb2d613de126a8506183fd0 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Fri, 10 Jul 2026 14:32:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ExecuteTool=20?= =?UTF-8?q?=E5=A7=94=E6=89=98=20null=20result=20=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84=20worktree=20UI=20=E5=B4=A9=E6=BA=83=E5=8F=8A=20debug?= =?UTF-8?q?=20=E6=97=A5=E5=BF=97=20ENOENT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExecuteTool 在内部工具调用失败时产出 result: null,其 renderToolResultMessage 通过 as never 将 null 直接传给 EnterWorktreeTool 的 UI,导致读取 output.worktreeBranch 时抛出 TypeError。修复方案:在 ExecuteTool 委托前增加 null 检查,同时为 EnterWorktreeTool/ExitWorktreeTool 的 renderToolResultMessage 增加 !output 防护。 debug.ts 中 getDebugFilePath 返回相对路径,appendFileSync 在某些场景下 因 CWD 变化抛出 ENOENT 击穿到 RootREPLBoundary。修复方案:路径 resolve 为 绝对路径,writeFn 增加重试机制,二次失败静默吞错不阻塞 UI。 Co-Authored-By: glm-5.2 --- .../src/tools/EnterWorktreeTool/UI.tsx | 9 ++++++- .../src/tools/ExecuteTool/ExecuteTool.ts | 6 +++++ .../src/tools/ExitWorktreeTool/UI.tsx | 1 + src/utils/debug.ts | 25 +++++++++++++++---- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/builtin-tools/src/tools/EnterWorktreeTool/UI.tsx b/packages/builtin-tools/src/tools/EnterWorktreeTool/UI.tsx index e0d503b181..07b406d7a0 100644 --- a/packages/builtin-tools/src/tools/EnterWorktreeTool/UI.tsx +++ b/packages/builtin-tools/src/tools/EnterWorktreeTool/UI.tsx @@ -14,10 +14,17 @@ export function renderToolResultMessage( _progressMessagesForMessage: ProgressMessage[], _options: { theme: ThemeName }, ): React.ReactNode { + if (!output) return null; return ( - Switched to worktree on branch {output.worktreeBranch} + Switched to worktree + {output.worktreeBranch ? ( + <> + {' '} + on branch {output.worktreeBranch} + + ) : null} {output.worktreePath} diff --git a/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts b/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts index bfe9fb32b4..da13524c14 100644 --- a/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts +++ b/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts @@ -250,6 +250,12 @@ export const ExecuteTool = buildTool({ renderToolResultMessage(content, progressMessages, options) { const innerTool = options.tools.find(t => t.name === content.tool_name) if (!innerTool?.renderToolResultMessage) return null + // Guard against null/undefined result — several error branches in this + // tool (tool-not-found, validation-failed, permission-denied, etc.) set + // result: null, and delegating null to the inner tool's UI would crash + // on any property access (e.g. output.worktreeBranch). + if (content.result == null || typeof content.result !== 'object') + return null const innerInput = (options.input as { params?: unknown } | undefined) ?.params return innerTool.renderToolResultMessage( diff --git a/packages/builtin-tools/src/tools/ExitWorktreeTool/UI.tsx b/packages/builtin-tools/src/tools/ExitWorktreeTool/UI.tsx index 76e0de4dda..e5dd5c2477 100644 --- a/packages/builtin-tools/src/tools/ExitWorktreeTool/UI.tsx +++ b/packages/builtin-tools/src/tools/ExitWorktreeTool/UI.tsx @@ -14,6 +14,7 @@ export function renderToolResultMessage( _progressMessagesForMessage: ProgressMessage[], _options: { theme: ThemeName }, ): React.ReactNode { + if (!output) return null; const actionLabel = output.action === 'keep' ? 'Kept worktree' : 'Removed worktree'; return ( diff --git a/src/utils/debug.ts b/src/utils/debug.ts index f70e9a68df..7e51efdee9 100644 --- a/src/utils/debug.ts +++ b/src/utils/debug.ts @@ -1,6 +1,6 @@ import { appendFile, mkdir, symlink, unlink } from 'fs/promises' import memoize from 'lodash-es/memoize.js' -import { dirname, join } from 'path' +import { dirname, join, resolve } from 'path' import { getSessionId } from 'src/bootstrap/state.js' import { type BufferedWriter, createBufferedWriter } from './bufferedWriter.js' @@ -90,10 +90,10 @@ export const getDebugFilePath = memoize((): string | null => { for (let i = 0; i < process.argv.length; i++) { const arg = process.argv[i]! if (arg.startsWith('--debug-file=')) { - return arg.substring('--debug-file='.length) + return resolve(arg.substring('--debug-file='.length)) } if (arg === '--debug-file' && i + 1 < process.argv.length) { - return process.argv[i + 1]! + return resolve(process.argv[i + 1]!) } } return null @@ -170,7 +170,20 @@ function getDebugWriter(): BufferedWriter { // Directory already exists } } - getFsImplementation().appendFileSync(path, content) + try { + getFsImplementation().appendFileSync(path, content) + } catch (e) { + // If the directory was removed between mkdirSync and + // appendFileSync (or the path was relative and CWD changed), + // retry once with a fresh mkdirSync. Swallow on second failure + // to avoid crashing the React render tree via the error boundary. + try { + getFsImplementation().mkdirSync(dir) + getFsImplementation().appendFileSync(path, content) + } catch { + // Best-effort: debug log write failed, don't crash the UI + } + } void updateLatestDebugLogSymlink() return } @@ -228,7 +241,9 @@ export function logForDebugging( export function getDebugLogPath(): string { return ( getDebugFilePath() ?? - process.env.CLAUDE_CODE_DEBUG_LOGS_DIR ?? + (process.env.CLAUDE_CODE_DEBUG_LOGS_DIR + ? resolve(process.env.CLAUDE_CODE_DEBUG_LOGS_DIR) + : null) ?? join(getClaudeConfigHomeDir(), 'debug', `${getSessionId()}.txt`) ) }