fix(conversations): Stop rendering raw tool call JSON as message content#117065
Conversation
When an assistant response contains only tool calls without text, parseAssistantContent would fall through to gen_ai.response.object and render the raw tool call JSON as message content. This caused: 1. Raw JSON blobs appearing in conversation messages 2. Duplicate rendering of tool calls (badges + JSON content) Fix by detecting tool-call-only outputs in extractAssistantOutput and returning null instead of falling through to response attributes. Also create assistant messages for tool-call-only turns so the badges still render, and skip the content area when message content is empty. Closes TET-2448 Co-Authored-By: Claude <noreply@anthropic.com>
📊 Type Coverage Diff✅ no issues found |
| {message.content !== '' && ( | ||
| <StyledClippedBox | ||
| clipHeight={200} | ||
| buttonProps={{variant: 'secondary', size: 'xs'}} | ||
| collapsible | ||
| > | ||
| <Container padding="md"> | ||
| <MessageText size="sm" align="left"> | ||
| <AIContentRenderer | ||
| text={message.content} | ||
| inline | ||
| autoCollapseLimit={10} | ||
| /> | ||
| </MessageText> | ||
| </Container> | ||
| </StyledClippedBox> | ||
| )} |
There was a problem hiding this comment.
🟠 high · possible confidence
Null Content Guard — The guard message.content !== '' only skips rendering when content is exactly an empty string. If content can be null or undefined at runtime (the old code set content: turn.assistantContent which could be null), the condition evaluates to true and AIContentRenderer receives a null/undefined text prop, which may throw or render incorrectly. The new code sets content: turn.assistantContent ?? '' so this should be safe for newly created messages, but any cached or legacy message objects with content: null would bypass the guard.
✅ Implements the stated intentIntent: Fix raw tool-call JSON appearing as message content by detecting tool-call-only assistant outputs and returning null instead of falling through to response attributes, and skip rendering the content area when content is empty. This PR fixes an issue where tool-call-only assistant outputs were rendering raw JSON as message content. The fix detects when an assistant message contains only tool calls (no text) and returns null from parseAssistantContent, then guards the content rendering in MessagesPanel to skip when content is empty. Finding [1] identifies a critical gap: the guard checks Findings (1): 1 high · 📍 1 commented inline on changed lines
📋 Basis of reviewExamined control flow in 🔍 Needs human verification
|
| } | ||
|
|
||
| if ( | ||
| const hasAssistantContent = |
There was a problem hiding this comment.
🟡 medium · likely confidence
Deduplication Bypass — The hasAssistantContent check uses seenAssistantContent.has(turn.assistantContent) to deduplicate, but when hasToolCalls is true and assistantContent is null, the block is entered via hasToolCalls without adding anything to seenAssistantContent. If a subsequent turn has the same null/empty assistantContent with tool calls, it will also pass through. More importantly, if hasAssistantContent is false (content already seen) but hasToolCalls is true, the block is entered and an assistant message is emitted with the duplicate content set to turn.assistantContent ?? ''. This means a previously-deduplicated content string could be re-emitted as an empty string, silently changing behavior from the old code where the entire block was skipped.
✅ Implements the stated intentIntent: Fix raw tool call JSON appearing as message content by detecting tool-call-only assistant outputs and returning null instead of falling through to response attributes, and skip rendering the content area when message content is empty. The diff implements all described fixes: Findings (1): 1 medium · 📍 1 commented inline on changed lines
📋 Basis of reviewExamined control flow and logic changes in 🔍 Needs human verification
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c2b482b. Configure here.
| timestamp: endTs, | ||
| nodeId: turn.generation.id, | ||
| toolCalls: turn.toolCalls.length > 0 ? turn.toolCalls : undefined, | ||
| toolCalls: hasToolCalls ? turn.toolCalls : undefined, |
There was a problem hiding this comment.
Assistant dedup bypassed with tools
Medium Severity
turnsToMessages now emits an assistant message when hasToolCalls is true even if that turn’s assistantContent was already deduplicated. A later turn with the same assistant text plus tool calls still pushes a full duplicate message (same content again), regressing the existing “deduplicates assistant messages by exact content” behavior.
Reviewed by Cursor Bugbot for commit c2b482b. Configure here.


When an assistant response contains only tool calls without text,
parseAssistantContentwould fall through togen_ai.response.objectand render the raw tool call JSON as message content. This caused:Fix by detecting tool-call-only outputs in
extractAssistantOutputandreturningnullinstead of falling through to response attributes. Also creates assistant messages for tool-call-only turns so the tool call badges still render, and skips the content area when message content is empty.Before:

After:

Closes TET-2448