diff --git a/desktop/src/components/chat/AssistantMessage.tsx b/desktop/src/components/chat/AssistantMessage.tsx
index 0ed75e69..32647c45 100644
--- a/desktop/src/components/chat/AssistantMessage.tsx
+++ b/desktop/src/components/chat/AssistantMessage.tsx
@@ -8,6 +8,8 @@ type Props = {
}
export function AssistantMessage({ content, isStreaming }: Props) {
+ if (!content.trim()) return null
+
const documentLayout = shouldUseDocumentLayout(content)
return (
diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx
index fff5e34f..70d16cbb 100644
--- a/desktop/src/components/chat/MessageList.test.tsx
+++ b/desktop/src/components/chat/MessageList.test.tsx
@@ -95,6 +95,41 @@ describe('MessageList nested tool calls', () => {
expect(container.textContent).toContain('Agent')
})
+ it('does not render blank assistant bubbles for whitespace-only text', () => {
+ const messages: UIMessage[] = [
+ {
+ id: 'assistant-empty',
+ type: 'assistant_text',
+ content: '\n\n ',
+ timestamp: 1,
+ },
+ {
+ id: 'tool-bash',
+ type: 'tool_use',
+ toolName: 'Bash',
+ toolUseId: 'bash-1',
+ input: { command: 'pwd' },
+ timestamp: 2,
+ },
+ ]
+
+ const { renderItems } = buildRenderModel(messages)
+ expect(renderItems).toHaveLength(1)
+ expect(renderItems[0]).toMatchObject({ kind: 'tool_group' })
+
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ messages,
+ streamingText: '\n ',
+ }),
+ },
+ })
+
+ const { container } = render()
+ expect(container.querySelectorAll('[data-message-shell="assistant"]')).toHaveLength(0)
+ })
+
it('keeps root tool runs split when nested child tool calls appear between them', () => {
const messages: UIMessage[] = [
{
diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx
index 36844b25..a221ad24 100644
--- a/desktop/src/components/chat/MessageList.tsx
+++ b/desktop/src/components/chat/MessageList.tsx
@@ -90,6 +90,10 @@ export function buildRenderModel(messages: UIMessage[]): RenderModel {
}
for (const msg of messages) {
+ if (msg.type === 'assistant_text' && !msg.content.trim()) {
+ continue
+ }
+
if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) {
continue
}
@@ -495,7 +499,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
)
})}
- {streamingText && (
+ {streamingText.trim() && (
)}
diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts
index 9d71c779..91a30f63 100644
--- a/desktop/src/stores/chatStore.test.ts
+++ b/desktop/src/stores/chatStore.test.ts
@@ -184,6 +184,35 @@ describe('chatStore history mapping', () => {
])
})
+ it('skips whitespace-only assistant transcript messages', () => {
+ const messages: MessageEntry[] = [
+ {
+ id: 'assistant-empty',
+ type: 'assistant',
+ timestamp: '2026-04-06T00:00:00.000Z',
+ model: 'opus',
+ content: '\n\n ',
+ },
+ {
+ id: 'assistant-real',
+ type: 'assistant',
+ timestamp: '2026-04-06T00:00:01.000Z',
+ model: 'opus',
+ content: '可见回复',
+ },
+ ]
+
+ const mapped = mapHistoryMessagesToUiMessages(messages)
+
+ expect(mapped).toMatchObject([
+ {
+ id: 'assistant-real',
+ type: 'assistant_text',
+ content: '可见回复',
+ },
+ ])
+ })
+
it('surfaces teammate prompt content when mapping member transcript history', () => {
const messages: MessageEntry[] = [
{
diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts
index 5ac8187e..5284489f 100644
--- a/desktop/src/stores/chatStore.ts
+++ b/desktop/src/stores/chatStore.ts
@@ -1080,6 +1080,7 @@ export function mapHistoryMessagesToUiMessages(
continue
}
if (msg.type === 'assistant' && typeof msg.content === 'string') {
+ if (!msg.content.trim()) continue
uiMessages.push({ id: msg.id || nextId(), type: 'assistant_text', content: msg.content, timestamp, model: msg.model })
continue
}