M7 #13 (pass-2): suppress empty assistant bubble during reasoning-only frames

Pass-2 turned up a ghost-message UX bug we missed in pass-1: every
"Thinking…" reasoning disclosure had an empty gray bubble next to
it. Happens because assistant messages exist momentarily in a
reasoning-only state (chunks of thinking text arrive before any
primary content), and the bubble path always rendered its padded
background regardless of content.

Gate the bubble render on non-empty content for assistant messages.
User bubbles still always render (the user explicitly submitted
content and saw it land — suppressing it on trim-empty would be
surprising). `trimmingCharacters(in: .whitespacesAndNewlines)` so
purely-whitespace assistant frames also don't render a bubble.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-24 14:29:39 +02:00
parent 444d43dea8
commit 723ef6743d
+12 -1
View File
@@ -672,7 +672,18 @@ private struct MessageBubble: View {
if message.hasReasoning, let r = message.reasoning, !r.isEmpty { if message.hasReasoning, let r = message.reasoning, !r.isEmpty {
ReasoningDisclosure(reasoning: r) ReasoningDisclosure(reasoning: r)
} }
bubbleContent // Only render the bubble when there's actual text
// to show. Assistant messages can exist in a
// "reasoning-only" or "tool-calls-only" state
// while the agent is thinking / invoking tools
// rendering an empty gray bubble next to every
// "Thinking" disclosure looked like a ghost
// message. User bubbles we always render (the
// user explicitly submitted content, even if
// it's just whitespace, they saw it land).
if message.isUser || !message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
bubbleContent
}
if !message.toolCalls.isEmpty { if !message.toolCalls.isEmpty {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
ForEach(message.toolCalls) { call in ForEach(message.toolCalls) { call in