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 }