fix(chat): skip empty assistant tool-call placeholder rows in REST transcript (#953)
* fix(chat): skip empty assistant tool-call placeholder rows in REST transcript Non-reasoning models (e.g. gemini-3.5-flash) store a tool-call turn as an empty assistant row (content = "", no reasoning) with a tool_calls list. The REST transcript returns these verbatim, and mapServerMessages was only filtering empty assistant rows that carried reasoning text (the issue #771 thinking-model case). With no reasoning, the filter check failed and the placeholder rows fell through — rendering as empty bubbles with Copy buttons in the chat list. Fix: skip ANY empty assistant row regardless of whether reasoning is present. If it carries reasoning, stash it for folding (issue #771 path unchanged). If it doesn't, it's a pure tool-call placeholder — silently drop it. No user-visible content is lost: these rows never held displayable text. Verified on device (hermes37 emulator): session 'Friendly greeting #9' had 10 ghost empty bubbles; after fix the transcript shows only real messages. Issue: #956 * fix(chat): blank-content guard in FullBleedAgentMessage prose renderer Defense-in-depth for the ghost-bubble fix (#953): never render an empty prose block (blank text + lone Copy button) if a placeholder row ever slips past upstream mapping. Streaming messages keep rendering so the live cursor survives until the first delta lands. Verified on device: cold start of 'Friendly greeting #9' renders only real content — no empty bubbles. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
5153e1572f
commit
a11761f120
2 files changed
+17
-4
No files matched your search
@@ -2929,11 +2929,18 @@ class ChatViewModel(
|
|||||||
existingReasoningMap[rawContent]?.reasoningText.orEmpty()
|
existingReasoningMap[rawContent]?.reasoningText.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reasoning-only assistant row (the gateway's split storage of a
|
// Empty assistant row — two cases stored by the gateway:
|
||||||
// reasoning turn): stash the trace, skip the empty bubble, and
|
// 1. Reasoning-only: thinking-model split storage (content = "",
|
||||||
// attach it to the next assistant message that has content.
|
// reasoning = trace). Stash the trace and fold it into the
|
||||||
if (role == MessageRole.ASSISTANT && rawContent.isBlank() && rowReasoning.isNotBlank()) {
|
// next assistant message that has content (issue #771).
|
||||||
|
// 2. Tool-call placeholder: non-reasoning models emit content = ""
|
||||||
|
// with tool_calls metadata and no reasoning. These carry no
|
||||||
|
// user-visible text and must not render as empty bubbles
|
||||||
|
// (issue #956).
|
||||||
|
if (role == MessageRole.ASSISTANT && rawContent.isBlank()) {
|
||||||
|
if (rowReasoning.isNotBlank()) {
|
||||||
pendingReasoning = rowReasoning
|
pendingReasoning = rowReasoning
|
||||||
|
}
|
||||||
return@forEachIndexed
|
return@forEachIndexed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ internal fun FullBleedAgentMessage(
|
|||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defense-in-depth: never render an empty prose block (blank bubble +
|
||||||
|
// lone Copy button). Blank settled rows are tool-call placeholders that
|
||||||
|
// slipped through upstream mapping; streaming keeps rendering so the
|
||||||
|
// live cursor survives until the first delta lands.
|
||||||
|
if (message.content.isNotBlank() || message.isStreaming) {
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
text = message.content,
|
text = message.content,
|
||||||
@@ -113,6 +118,7 @@ internal fun FullBleedAgentMessage(
|
|||||||
onImageClick = onImageClick,
|
onImageClick = onImageClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render inline attachments (mirrors ChatBubble so agent-delivered
|
// Render inline attachments (mirrors ChatBubble so agent-delivered
|
||||||
// media — images, files — shows in full-bleed mode too).
|
// media — images, files — shows in full-bleed mode too).
|
||||||
|
|||||||
Reference in new issue
Block a user