Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ export function filterToolProgressMessages(
): ProgressMessage<ToolProgressData>[] {
return progressMessagesForMessage.filter(
(msg): msg is ProgressMessage<ToolProgressData> =>
(msg.data as { type?: string })?.type !== 'hook_progress',
msg.data != null &&
(msg.data as { type?: string }).type !== 'hook_progress',
)
}

Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/Tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,19 @@ describe('filterToolProgressMessages', () => {
const result = filterToolProgressMessages(messages)
expect(result).toHaveLength(1)
})

test('filters out messages with null/undefined data', () => {
// Regression: a progress message whose data is null used to pass through
// (null?.type === undefined !== 'hook_progress') and reach tool progress
// renderers / lookup builders, which then crashed on data.type / data.taskId
// field access or on 'message' in data (claude-code-best/claude-code#1330).
const messages = [
{ data: null },
{ data: undefined },
{ data: { type: 'tool_progress', toolName: 'Bash' } },
] as any[]
const result = filterToolProgressMessages(messages)
expect(result).toHaveLength(1)
expect((result[0]!.data as any).type).toBe('tool_progress')
})
})
3 changes: 2 additions & 1 deletion src/components/messages/AssistantToolUseMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ function renderToolUseProgressMessage(
terminalSize: { columns: number; rows: number },
): React.ReactNode {
const toolProgressMessages = progressMessagesForMessage.filter(
(msg): msg is ProgressMessage<ToolProgressData> => (msg.data as Record<string, unknown>).type !== 'hook_progress',
(msg): msg is ProgressMessage<ToolProgressData> =>
msg.data != null && (msg.data as Record<string, unknown>).type !== 'hook_progress',
);
try {
const toolMessages =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,16 @@ export function UserToolSuccessMessage({
return (
<Box flexDirection="column">
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
{wrappedMessage}
{/*
Tool-provided result UIs are rendered from runtime data
(message.toolUseResult). Resumed transcripts deserialize it via raw
JSON.parse (parseJSONL), so a partial/corrupt/old-format result can
crash renderToolResultMessage on first field access
(anthropics/claude-code#39817, claude-code-best/claude-code#1330).
Keep the result slot behind its own boundary so a bad result only
degrades that row instead of tearing down the whole Messages tree.
*/}
<SentryErrorBoundary name="ToolResultMessage">{wrappedMessage}</SentryErrorBoundary>
Comment on lines +112 to +121

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'UserToolSuccessMessage\.tsx$' . || true

echo "== file outline =="
ast-grep outline src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx --view expanded || true

echo "== relevant lines =="
sed -n '1,180p' src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx

echo "== search renderToolResultMessage definitions/usages =="
rg -n "renderToolResultMessage|function renderToolResultMessage|const renderToolResultMessage|export .*renderToolResultMessage" src || true

echo "== search SentryErrorBoundary =="
rg -n "SentryErrorBoundary|class SentryErrorBoundary|function SentryErrorBoundary" src || true

Repository: claude-code-best/claude-code

Length of output: 11212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SentryErrorBoundary implementation =="
sed -n '1,90p' src/components/SentryErrorBoundary.tsx

echo "== Tool renderToolResultMessage signature and comments =="
sed -n '550,610p' src/Tool.ts

echo "== test files mentioning UserToolSuccessMessage or ToolResultMessage =="
rg -n "UserToolSuccessMessage|ToolResultMessage|SentryErrorBoundary.*ToolResult|toolUseResult" src/utils src/components/messages/src src --glob '*test*' --glob '*Test*' || true

Repository: claude-code-best/claude-code

Length of output: 5381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path
import re

path = Path("src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx")
src = path.read_text()

# Deterministic check based on source order and lexical assignment to JSX children.
try:
    tree = ast.parse(src)
except Exception as e:
    print(f"parse_error: {type(e).__name__}: {e}")
    raise

assignments = {}
calls = []
boundary = None
func = None
for node in tree.body:
    if isinstance(node, ast.FunctionDef) and node.name == "UserToolSuccessMessage":
        func = node
        for child in ast.walk(node):
            if isinstance(child, ast.Return) and child.value is not None and isinstance(child.value, ast.Subscript):
                # JSX subscripts: expressions in the JSX tree appear as Subscript nodes in this TypeScript-like AST if imported/typed.
                # Use text pattern for this simple check rather than parsing JSX into AST.
                pass
        calls = []
        boundary = None
        for child in ast.walk(node):
            if isinstance(child, ast.Call):
                calls.append(ast.get_source_segment(src, child))
            if isinstance(child, ast.Assign) and len(child.targets) == 1 and isinstance(child.targets[0], ast.Name) and child.targets[0].id == "wrappedMessage":
                assignments[child.targets[0].id] = ast.get_source_segment(src, child.value)

        # Find boundary text and its child expression.
        text = src[node.lineno-1:tree.body.index(func)+1]
        for m in re.finditer(r"<SentryErrorBoundary[^>]*>(.*?)</SentryErrorBoundary>", text, flags=re.S):
            boundary = ast.get_source_segment(src, node).find(m.group(0))
            print("boundary_text", m.group(0))
        render_call = next((x for x in calls if "renderToolResultMessage" in str(x)), None)
        wrapped_text = assignments.get("wrappedMessage")
        print("render_call_present", render_call is not None)
        print("wrapped_message", wrapped_text)
        print("boundary_text_contains_render_call", boundary is not None and render_call in boundary if render_call else False)
        print("boundary_text_contains_wrappedMessage", boundary is not None and wrapped_text is not None and "wrappedMessage" in m.group(0) if render_call else False)
        break
PY

Repository: claude-code-best/claude-code

Length of output: 517


Move the synchronous tool renderer under SentryErrorBoundary.

tool.renderToolResultMessage runs before React creates SentryErrorBoundary, so a synchronous exception still leaves UserToolSuccessMessage. Move the renderer call into a child component and render that child inside the named boundary. Add a regression test where renderToolResultMessage throws synchronously.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx`
around lines 112 - 121, Move the synchronous tool renderer call into a dedicated
child component, then render that child inside the named SentryErrorBoundary in
UserToolSuccessMessage so renderer exceptions are caught. Add a regression test
covering a synchronous throw from renderToolResultMessage and verify the
boundary fallback replaces only the tool-result row.

{feature('BASH_CLASSIFIER')
? classifierRule && (
<MessageResponse height={1}>
Expand Down
7 changes: 5 additions & 2 deletions src/utils/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,8 +1276,11 @@ export function buildMessageLookups(
}

// Count in-progress hooks
const progressData = msg.data as { type: string; hookEvent: HookEvent }
if (progressData.type === 'hook_progress') {
const progressData = msg.data as
| { type: string; hookEvent: HookEvent }
| null
| undefined
if (progressData && progressData.type === 'hook_progress') {
const hookEvent = progressData.hookEvent
let byHookEvent = inProgressHookCounts.get(toolUseID)
if (!byHookEvent) {
Expand Down