From 723ef6743d67a66e6624fad19f12115bef0867d4 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Fri, 24 Apr 2026 14:29:39 +0200 Subject: [PATCH] M7 #13 (pass-2): suppress empty assistant bubble during reasoning-only frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scarf/Scarf iOS/Chat/ChatView.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scarf/Scarf iOS/Chat/ChatView.swift b/scarf/Scarf iOS/Chat/ChatView.swift index 47fa4a3..725a481 100644 --- a/scarf/Scarf iOS/Chat/ChatView.swift +++ b/scarf/Scarf iOS/Chat/ChatView.swift @@ -672,7 +672,18 @@ private struct MessageBubble: View { if message.hasReasoning, let r = message.reasoning, !r.isEmpty { 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 { VStack(alignment: .leading, spacing: 6) { ForEach(message.toolCalls) { call in