diff --git a/src/Tool.ts b/src/Tool.ts index caeac479e..889f4ba9c 100644 --- a/src/Tool.ts +++ b/src/Tool.ts @@ -324,7 +324,8 @@ export function filterToolProgressMessages( ): ProgressMessage[] { return progressMessagesForMessage.filter( (msg): msg is ProgressMessage => - (msg.data as { type?: string })?.type !== 'hook_progress', + msg.data != null && + (msg.data as { type?: string }).type !== 'hook_progress', ) } diff --git a/src/__tests__/Tool.test.ts b/src/__tests__/Tool.test.ts index 9292459c3..3e86eb3ff 100644 --- a/src/__tests__/Tool.test.ts +++ b/src/__tests__/Tool.test.ts @@ -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') + }) }) diff --git a/src/components/messages/AssistantToolUseMessage.tsx b/src/components/messages/AssistantToolUseMessage.tsx index ccf55e53e..6e11a36b9 100644 --- a/src/components/messages/AssistantToolUseMessage.tsx +++ b/src/components/messages/AssistantToolUseMessage.tsx @@ -236,7 +236,8 @@ function renderToolUseProgressMessage( terminalSize: { columns: number; rows: number }, ): React.ReactNode { const toolProgressMessages = progressMessagesForMessage.filter( - (msg): msg is ProgressMessage => (msg.data as Record).type !== 'hook_progress', + (msg): msg is ProgressMessage => + msg.data != null && (msg.data as Record).type !== 'hook_progress', ); try { const toolMessages = diff --git a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx index bada2d9b5..cb7a52537 100644 --- a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +++ b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx @@ -109,7 +109,16 @@ export function UserToolSuccessMessage({ return ( - {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. + */} + {wrappedMessage} {feature('BASH_CLASSIFIER') ? classifierRule && ( diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 51884e804..1b2e0fc6d 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -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) {