From cccd1ab4f2465285a39039dfe7fd34138ceadd33 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sun, 2 Aug 2026 23:02:32 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E5=8A=A0=E5=9B=BA=20taskId=20?= =?UTF-8?q?=E7=A9=BA=E5=BC=95=E7=94=A8=E6=B8=B2=E6=9F=93=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=20(#1330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filterToolProgressMessages 放行 data:null 的 progress 消息(结构漏洞), UserToolSuccessMessage 的工具结果渲染不在嵌套边界内(任何字段访问崩溃 都会撕裂整个 MessagesBoundary)。补齐两层防线: - Tool.ts: data != null 过滤 + UserToolSuccessMessage 结果槽位加 SentryErrorBoundary(name=ToolResultMessage) - BashTool/PowerShellTool/MonitorTool/AttachmentMessage/ UserTeammateMessage/TaskAssignmentMessage 的 taskId 读取改可选链 - 新增 filterToolProgressMessages data:null 回归测试 Co-Authored-By: deepseek-v4-flash --- packages/builtin-tools/src/tools/BashTool/UI.tsx | 2 +- .../src/tools/MonitorTool/MonitorTool.tsx | 2 +- .../builtin-tools/src/tools/PowerShellTool/UI.tsx | 2 +- src/Tool.ts | 3 ++- src/__tests__/Tool.test.ts | 15 +++++++++++++++ src/components/messages/AttachmentMessage.tsx | 4 ++-- src/components/messages/TaskAssignmentMessage.tsx | 4 ++-- src/components/messages/UserTeammateMessage.tsx | 2 +- .../UserToolSuccessMessage.tsx | 11 ++++++++++- 9 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/builtin-tools/src/tools/BashTool/UI.tsx b/packages/builtin-tools/src/tools/BashTool/UI.tsx index ef53d8553..ecc1be30c 100644 --- a/packages/builtin-tools/src/tools/BashTool/UI.tsx +++ b/packages/builtin-tools/src/tools/BashTool/UI.tsx @@ -144,7 +144,7 @@ export function renderToolUseProgressMessage( totalLines={data.totalLines} totalBytes={data.totalBytes} timeoutMs={data.timeoutMs} - taskId={data.taskId} + taskId={data?.taskId} verbose={verbose} /> ); diff --git a/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx b/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx index cf70d0a16..3d6f6bbd5 100644 --- a/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx +++ b/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx @@ -173,7 +173,7 @@ Examples: renderToolResultMessage(output: MonitorOutput) { return ( - Monitor started (task {output.taskId}). Output: {output.outputFile} + Monitor started (task {output?.taskId}). Output: {output?.outputFile} ); }, diff --git a/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx b/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx index 75c35ee6b..14db6162b 100644 --- a/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx +++ b/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx @@ -85,7 +85,7 @@ export function renderToolUseProgressMessage( totalLines={data.totalLines} totalBytes={data.totalBytes} timeoutMs={data.timeoutMs} - taskId={data.taskId} + taskId={data?.taskId} verbose={verbose} /> ); 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..c817b0b9f 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, which then crashed on data.taskId / data.output field access + // (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/AttachmentMessage.tsx b/src/components/messages/AttachmentMessage.tsx index 574209158..8d43ec667 100644 --- a/src/components/messages/AttachmentMessage.tsx +++ b/src/components/messages/AttachmentMessage.tsx @@ -81,7 +81,7 @@ export function AttachmentMessage({ attachment, addMargin, verbose, isTranscript {BLACK_CIRCLE} Task assigned: - #{parsedMsg.taskId} + #{parsedMsg?.taskId} - {parsedMsg.subject} (from {parsedMsg.assignedBy || msg.from}) @@ -463,7 +463,7 @@ function GenericTaskStatus({ attachment }: { attachment: TaskStatusAttachment }) function TeammateTaskStatus({ attachment }: { attachment: TaskStatusAttachment }): React.ReactNode { const bg = useSelectedMessageBg(); // Narrow selector: only re-render when this specific task changes. - const task = useAppState(s => s.tasks[attachment.taskId]); + const task = useAppState(s => s.tasks?.[attachment.taskId]); if (task?.type !== 'in_process_teammate') { // Fall through to generic rendering (task not yet in store, or wrong type) return ; diff --git a/src/components/messages/TaskAssignmentMessage.tsx b/src/components/messages/TaskAssignmentMessage.tsx index f81544de6..3920d103d 100644 --- a/src/components/messages/TaskAssignmentMessage.tsx +++ b/src/components/messages/TaskAssignmentMessage.tsx @@ -15,7 +15,7 @@ export function TaskAssignmentDisplay({ assignment }: Props): React.ReactNode { - Task #{assignment.taskId} assigned by {assignment.assignedBy} + Task #{assignment?.taskId} assigned by {assignment.assignedBy} @@ -48,7 +48,7 @@ export function tryRenderTaskAssignmentMessage(content: string): React.ReactNode export function getTaskAssignmentSummary(content: string): string | null { const assignment = isTaskAssignment(content); if (assignment) { - return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`; + return `[Task Assigned] #${assignment?.taskId} - ${assignment.subject}`; } return null; } diff --git a/src/components/messages/UserTeammateMessage.tsx b/src/components/messages/UserTeammateMessage.tsx index c7b3cc2ce..2d9d260ba 100644 --- a/src/components/messages/UserTeammateMessage.tsx +++ b/src/components/messages/UserTeammateMessage.tsx @@ -130,7 +130,7 @@ export function UserTeammateMessage({ addMargin, param: { text }, isTranscriptMo {' '} - Completed task #{taskCompleted.taskId} + Completed task #{taskCompleted?.taskId} {taskCompleted.taskSubject && ({taskCompleted.taskSubject})} 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 && ( From 5aa20afb3511172af93ffbda840004b36fb163f2 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sun, 2 Aug 2026 23:10:03 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=94=B6=E7=AA=84=20#1330=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=8C=83=E5=9B=B4=E8=87=B3=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E6=80=A7=E5=B0=81=E5=A0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 后移除 8 处纯防御性可选链改动(原有守卫已完整),仅保留: - filterToolProgressMessages 拒绝 data 为 null 的 progress 消息 - ToolResultMessage 边界包裹,隔离工具结果 UI 渲染崩溃 - AssistantToolUseMessage / messages.ts 同源 data 空值守卫 Co-Authored-By: deepseek-v4-flash --- packages/builtin-tools/src/tools/BashTool/UI.tsx | 2 +- .../builtin-tools/src/tools/MonitorTool/MonitorTool.tsx | 2 +- packages/builtin-tools/src/tools/PowerShellTool/UI.tsx | 2 +- src/__tests__/Tool.test.ts | 4 ++-- src/components/messages/AssistantToolUseMessage.tsx | 3 ++- src/components/messages/AttachmentMessage.tsx | 4 ++-- src/components/messages/TaskAssignmentMessage.tsx | 4 ++-- src/components/messages/UserTeammateMessage.tsx | 2 +- src/utils/messages.ts | 7 +++++-- 9 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/builtin-tools/src/tools/BashTool/UI.tsx b/packages/builtin-tools/src/tools/BashTool/UI.tsx index ecc1be30c..ef53d8553 100644 --- a/packages/builtin-tools/src/tools/BashTool/UI.tsx +++ b/packages/builtin-tools/src/tools/BashTool/UI.tsx @@ -144,7 +144,7 @@ export function renderToolUseProgressMessage( totalLines={data.totalLines} totalBytes={data.totalBytes} timeoutMs={data.timeoutMs} - taskId={data?.taskId} + taskId={data.taskId} verbose={verbose} /> ); diff --git a/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx b/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx index 3d6f6bbd5..cf70d0a16 100644 --- a/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx +++ b/packages/builtin-tools/src/tools/MonitorTool/MonitorTool.tsx @@ -173,7 +173,7 @@ Examples: renderToolResultMessage(output: MonitorOutput) { return ( - Monitor started (task {output?.taskId}). Output: {output?.outputFile} + Monitor started (task {output.taskId}). Output: {output.outputFile} ); }, diff --git a/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx b/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx index 14db6162b..75c35ee6b 100644 --- a/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx +++ b/packages/builtin-tools/src/tools/PowerShellTool/UI.tsx @@ -85,7 +85,7 @@ export function renderToolUseProgressMessage( totalLines={data.totalLines} totalBytes={data.totalBytes} timeoutMs={data.timeoutMs} - taskId={data?.taskId} + taskId={data.taskId} verbose={verbose} /> ); diff --git a/src/__tests__/Tool.test.ts b/src/__tests__/Tool.test.ts index c817b0b9f..3e86eb3ff 100644 --- a/src/__tests__/Tool.test.ts +++ b/src/__tests__/Tool.test.ts @@ -208,8 +208,8 @@ describe('filterToolProgressMessages', () => { 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, which then crashed on data.taskId / data.output field access - // (claude-code-best/claude-code#1330). + // 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 }, 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/AttachmentMessage.tsx b/src/components/messages/AttachmentMessage.tsx index 8d43ec667..574209158 100644 --- a/src/components/messages/AttachmentMessage.tsx +++ b/src/components/messages/AttachmentMessage.tsx @@ -81,7 +81,7 @@ export function AttachmentMessage({ attachment, addMargin, verbose, isTranscript {BLACK_CIRCLE} Task assigned: - #{parsedMsg?.taskId} + #{parsedMsg.taskId} - {parsedMsg.subject} (from {parsedMsg.assignedBy || msg.from}) @@ -463,7 +463,7 @@ function GenericTaskStatus({ attachment }: { attachment: TaskStatusAttachment }) function TeammateTaskStatus({ attachment }: { attachment: TaskStatusAttachment }): React.ReactNode { const bg = useSelectedMessageBg(); // Narrow selector: only re-render when this specific task changes. - const task = useAppState(s => s.tasks?.[attachment.taskId]); + const task = useAppState(s => s.tasks[attachment.taskId]); if (task?.type !== 'in_process_teammate') { // Fall through to generic rendering (task not yet in store, or wrong type) return ; diff --git a/src/components/messages/TaskAssignmentMessage.tsx b/src/components/messages/TaskAssignmentMessage.tsx index 3920d103d..f81544de6 100644 --- a/src/components/messages/TaskAssignmentMessage.tsx +++ b/src/components/messages/TaskAssignmentMessage.tsx @@ -15,7 +15,7 @@ export function TaskAssignmentDisplay({ assignment }: Props): React.ReactNode { - Task #{assignment?.taskId} assigned by {assignment.assignedBy} + Task #{assignment.taskId} assigned by {assignment.assignedBy} @@ -48,7 +48,7 @@ export function tryRenderTaskAssignmentMessage(content: string): React.ReactNode export function getTaskAssignmentSummary(content: string): string | null { const assignment = isTaskAssignment(content); if (assignment) { - return `[Task Assigned] #${assignment?.taskId} - ${assignment.subject}`; + return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`; } return null; } diff --git a/src/components/messages/UserTeammateMessage.tsx b/src/components/messages/UserTeammateMessage.tsx index 2d9d260ba..c7b3cc2ce 100644 --- a/src/components/messages/UserTeammateMessage.tsx +++ b/src/components/messages/UserTeammateMessage.tsx @@ -130,7 +130,7 @@ export function UserTeammateMessage({ addMargin, param: { text }, isTranscriptMo {' '} - Completed task #{taskCompleted?.taskId} + Completed task #{taskCompleted.taskId} {taskCompleted.taskSubject && ({taskCompleted.taskSubject})} 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) {