mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-26 01:55:50 +00:00
fix: prevent null output and string render crashes in MessagesBoundary
UserToolSuccessMessage now requires parsedOutput.success before trusting data, and guards toolResult against non-object values before calling renderToolResultMessage. String renderedMessage is wrapped in <Text> so multi-line tool reports (e.g. GoalTool usage report) don't crash Ink. Defense in depth added to VaultHttpFetchTool/UI (matches the existing pattern in LocalMemoryRecallTool and GoalTool) and to the mapToolResultToToolResultBlockParam of both vault/memory tools. Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "2.8.1",
|
"version": "2.8.2",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ export const GoalTool = buildTool({
|
|||||||
return `Updating goal: ${input.status}${input.reason ? ` — ${input.reason}` : ''}`
|
return `Updating goal: ${input.status}${input.reason ? ` — ${input.reason}` : ''}`
|
||||||
},
|
},
|
||||||
renderToolResultMessage(output: Output) {
|
renderToolResultMessage(output: Output) {
|
||||||
if (output.error) return `Goal error: ${output.error}`
|
if (!output) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (output?.error) return `Goal error: ${output.error}`
|
||||||
if (output.report) return output.report
|
if (output.report) return output.report
|
||||||
if (output.goal) {
|
if (output.goal) {
|
||||||
return `Goal "${output.goal.objective}" — ${output.goal.status}`
|
return `Goal "${output.goal.objective}" — ${output.goal.status}`
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ export const LocalMemoryRecallTool = buildTool({
|
|||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: toolUseID,
|
tool_use_id: toolUseID,
|
||||||
content: jsonStringify(output),
|
content: jsonStringify(output),
|
||||||
is_error: output.error !== undefined,
|
is_error: output?.error !== undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ToolDef<InputSchema, Output>)
|
} satisfies ToolDef<InputSchema, Output>)
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export function renderToolResultMessage(
|
|||||||
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
||||||
{ verbose }: { verbose: boolean },
|
{ verbose }: { verbose: boolean },
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
|
// Defense in depth: framework validates via outputSchema, but resumed
|
||||||
|
// transcripts can still produce null here via deserialization edge cases.
|
||||||
|
if (!output) return null;
|
||||||
if (output.error) {
|
if (output.error) {
|
||||||
return (
|
return (
|
||||||
<MessageResponse height={1}>
|
<MessageResponse height={1}>
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ export const VaultHttpFetchTool = buildTool({
|
|||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: toolUseID,
|
tool_use_id: toolUseID,
|
||||||
content: jsonStringify(output),
|
content: jsonStringify(output),
|
||||||
is_error: output.error !== undefined,
|
is_error: output?.error !== undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ToolDef<InputSchema, Output>)
|
} satisfies ToolDef<InputSchema, Output>)
|
||||||
|
|||||||
@@ -67,7 +67,14 @@ export function UserToolSuccessMessage({
|
|||||||
if (parsedOutput && !parsedOutput.success) {
|
if (parsedOutput && !parsedOutput.success) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const toolResult = parsedOutput?.data ?? message.toolUseResult;
|
// Only trust schema-validated output. Fall back to raw toolUseResult only
|
||||||
|
// when it's a non-null object — schemas without outputSchema, or successful
|
||||||
|
// parses that yield null/undefined data, must not reach renderToolResultMessage
|
||||||
|
// (tool UIs access output.error / output.action on first line and crash).
|
||||||
|
const toolResult = parsedOutput?.success ? parsedOutput.data : message.toolUseResult;
|
||||||
|
if (!toolResult || typeof toolResult !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Collapse diff display for old messages (verbose/ctrl+o overrides)
|
// Collapse diff display for old messages (verbose/ctrl+o overrides)
|
||||||
const effectiveStyle = shouldCollapseDiffs && !verbose ? 'condensed' : style;
|
const effectiveStyle = shouldCollapseDiffs && !verbose ? 'condensed' : style;
|
||||||
@@ -88,6 +95,11 @@ export function UserToolSuccessMessage({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ink requires text strings to be inside <Text>. Tools that return plain
|
||||||
|
// multi-line strings (e.g. GoalTool's usage report) crash without the wrap.
|
||||||
|
// React elements from UI.tsx files pass through unchanged.
|
||||||
|
const wrappedMessage = typeof renderedMessage === 'string' ? <Text>{renderedMessage}</Text> : renderedMessage;
|
||||||
|
|
||||||
// Tools that return '' from userFacingName opt out of tool chrome and
|
// Tools that return '' from userFacingName opt out of tool chrome and
|
||||||
// render like plain assistant text. Skip the tool-result width constraint
|
// render like plain assistant text. Skip the tool-result width constraint
|
||||||
// so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col
|
// so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col
|
||||||
@@ -97,7 +109,7 @@ export function UserToolSuccessMessage({
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
|
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
|
||||||
{renderedMessage}
|
{wrappedMessage}
|
||||||
{feature('BASH_CLASSIFIER')
|
{feature('BASH_CLASSIFIER')
|
||||||
? classifierRule && (
|
? classifierRule && (
|
||||||
<MessageResponse height={1}>
|
<MessageResponse height={1}>
|
||||||
|
|||||||
Reference in New Issue
Block a user