diff --git a/docs/chat-chain-changes/2026-11-05-empty-assistant-bubbles.md b/docs/chat-chain-changes/2026-11-05-empty-assistant-bubbles.md new file mode 100644 index 0000000..d9db5d5 --- /dev/null +++ b/docs/chat-chain-changes/2026-11-05-empty-assistant-bubbles.md @@ -0,0 +1,12 @@ +--- +date: 2026-11-05 +pr: pending +feature: Empty assistant bubbles during tool activity +impact: Whitespace-only assistant messages are hidden, and assistant text within a turn is displayed together with a completion marker. Stream data and tool activity are unchanged. +--- + +Tool cycles can leave assistant messages containing only whitespace. The chat +list excludes these empty bubbles while retaining the underlying streamed data, +so later text can still be appended to the same message. +Assistant messages within a turn are combined for display; tool rows remain +available below the combined text, and a separator marks the end of the turn. diff --git a/packages/client/src/components/hermes/chat/MessageList.vue b/packages/client/src/components/hermes/chat/MessageList.vue index e55a8a5..1ab1c27 100644 --- a/packages/client/src/components/hermes/chat/MessageList.vue +++ b/packages/client/src/components/hermes/chat/MessageList.vue @@ -19,6 +19,7 @@ import { ref, computed, nextTick, onBeforeUnmount, onMounted, watch } from "vue" import { useI18n } from "vue-i18n"; import VirtualMessageList from "./VirtualMessageList.vue"; import MessageItem from "./MessageItem.vue"; +import { groupRunMessages } from "./groupRunMessages"; import { LIVE_CHAT_MAX_LOADED_MESSAGES, useChatStore, type Message } from "@/stores/hermes/chat"; import thinkingImage from "@/assets/thinking.gif"; import { useToolTraceVisibility } from "@/composables/useToolTraceVisibility"; @@ -100,10 +101,15 @@ const emptyState = computed(() => ({ const displayMessages = computed(() => { const currentToolIds = new Set(currentToolCalls.value.map((tool) => tool.id)); - return chatStore.messages.filter((m) => { + const messages = chatStore.messages.filter((m) => { if (m.role === "tool") { return toolTraceVisible.value && !!m.toolName && !(chatStore.isRunActive && currentToolIds.has(m.id)); } + // Tool cycles can produce whitespace-only assistant deltas. Keep them in + // the store for subsequent deltas, but don't render an empty bubble. + if (m.role === "assistant" && !m.content?.trim() && !m.reasoning?.trim() && !m.attachments?.length) { + return false; + } if ( m.role === "assistant" && m.isStreaming && @@ -115,6 +121,7 @@ const displayMessages = computed(() => { } return true; }); + return groupRunMessages(messages, chatStore.isRunActive || !!chatStore.abortState); }); function forkDividerId(sessionId: string): string { @@ -484,7 +491,8 @@ defineExpose({