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`) ) }