Skip to content

Commit d8bf6da

Browse files
Improve markdown rendering stability and scroll anchoring
Enhance rendering consistency for streaming and toggled content: - Add unique render keys based on content length and streaming state - Prevent React reconciliation errors during markdown updates - Trim trailing newlines only while streaming to avoid layout shifts - Register agent/tool refs for scroll-to-element behavior - Wrap ReactNode content in keyed containers to prevent TextNode errors - Add explicit keys to preview text elements for stability - Use proper streaming detection (block.status === 'running') 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 7fa1d8c commit d8bf6da

File tree

2 files changed

+96
-27
lines changed

2 files changed

+96
-27
lines changed

cli/src/components/branch-item.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,40 @@ export const BranchItem = ({
7676

7777
if (isTextRenderable(value)) {
7878
return (
79-
<text wrap fg={theme.agentText}>
79+
<text wrap fg={theme.agentText} key="expanded-text">
8080
{value}
8181
</text>
8282
)
8383
}
8484

85-
return value
85+
if (React.isValidElement(value)) {
86+
if (value.key === null || value.key === undefined) {
87+
return (
88+
<box key="expanded-node" style={{ flexDirection: 'column', gap: 0 }}>
89+
{value}
90+
</box>
91+
)
92+
}
93+
return value
94+
}
95+
96+
if (Array.isArray(value)) {
97+
return (
98+
<box key="expanded-array" style={{ flexDirection: 'column', gap: 0 }}>
99+
{value.map((child, idx) => (
100+
<box key={`expanded-array-${idx}`} style={{ flexDirection: 'column', gap: 0 }}>
101+
{child}
102+
</box>
103+
))}
104+
</box>
105+
)
106+
}
107+
108+
return (
109+
<box key="expanded-unknown" style={{ flexDirection: 'column', gap: 0 }}>
110+
{value}
111+
</box>
112+
)
86113
}
87114

88115
return (
@@ -125,6 +152,7 @@ export const BranchItem = ({
125152
<box style={{ flexShrink: 1, marginBottom: 0 }}>
126153
{isStreaming && isCollapsed && streamingPreview && (
127154
<text
155+
key="streaming-preview"
128156
wrap
129157
fg={theme.agentText}
130158
attributes={TextAttributes.ITALIC}
@@ -134,6 +162,7 @@ export const BranchItem = ({
134162
)}
135163
{!isStreaming && isCollapsed && finishedPreview && (
136164
<text
165+
key="finished-preview"
137166
wrap
138167
fg={theme.agentResponseCount}
139168
attributes={TextAttributes.ITALIC}

cli/src/components/message-block.tsx

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {
1313
import type { ContentBlock } from '../chat'
1414
import type { ChatTheme } from '../utils/theme-system'
1515

16+
const trimTrailingNewlines = (value: string): string =>
17+
value.replace(/[\r\n]+$/g, '')
18+
1619
interface MessageBlockProps {
1720
messageId: string
1821
blocks?: ContentBlock[]
@@ -33,6 +36,7 @@ interface MessageBlockProps {
3336
collapsedAgents: Set<string>
3437
streamingAgents: Set<string>
3538
onToggleCollapsed: (id: string) => void
39+
registerAgentRef: (id: string, element: any) => void
3640
}
3741

3842
export const MessageBlock = ({
@@ -55,6 +59,7 @@ export const MessageBlock = ({
5559
collapsedAgents,
5660
streamingAgents,
5761
onToggleCollapsed,
62+
registerAgentRef,
5863
}: MessageBlockProps): ReactNode => {
5964
return (
6065
<>
@@ -76,10 +81,16 @@ export const MessageBlock = ({
7681
<box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
7782
{blocks.map((block, idx) => {
7883
if (block.type === 'text') {
79-
const trimmedContent = block.content.trim()
80-
const renderedContent = hasMarkdown(trimmedContent)
81-
? renderStreamingMarkdown(trimmedContent, markdownOptions)
82-
: trimmedContent
84+
const isStreamingText = isLoading || !isComplete
85+
const rawContent = isStreamingText
86+
? trimTrailingNewlines(block.content)
87+
: block.content.trim()
88+
const renderKey = `${messageId}-text-${idx}-${rawContent.length}-${isStreamingText ? 'stream' : 'final'}`
89+
const renderedContent = hasMarkdown(rawContent)
90+
? isStreamingText
91+
? renderStreamingMarkdown(rawContent, markdownOptions)
92+
: renderMarkdown(rawContent, markdownOptions)
93+
: rawContent
8394
const prevBlock = idx > 0 ? blocks[idx - 1] : null
8495
const marginTop =
8596
prevBlock &&
@@ -88,7 +99,7 @@ export const MessageBlock = ({
8899
: 0
89100
return (
90101
<text
91-
key={`${messageId}-text-${idx}`}
102+
key={renderKey}
92103
wrap
93104
style={{ fg: textColor, marginTop }}
94105
>
@@ -155,7 +166,10 @@ export const MessageBlock = ({
155166
const branchChar = isLastBranch ? '└─ ' : '├─ '
156167

157168
return (
158-
<box key={`${messageId}-tool-${block.toolCallId}`}>
169+
<box
170+
key={`${messageId}-tool-${block.toolCallId}`}
171+
ref={(el: any) => registerAgentRef(block.toolCallId, el)}
172+
>
159173
<BranchItem
160174
name={displayInfo.name}
161175
content={displayContent}
@@ -171,7 +185,8 @@ export const MessageBlock = ({
171185
)
172186
} else if (block.type === 'agent') {
173187
const isCollapsed = collapsedAgents.has(block.agentId)
174-
const isStreaming = streamingAgents.has(block.agentId)
188+
const isStreaming =
189+
block.status === 'running' || streamingAgents.has(block.agentId)
175190

176191
const allTextContent =
177192
block.blocks
@@ -208,15 +223,27 @@ export const MessageBlock = ({
208223
<box style={{ flexDirection: 'column', gap: 0 }}>
209224
{block.blocks?.map((nestedBlock, nestedIdx) => {
210225
if (nestedBlock.type === 'text') {
211-
const renderedContent = hasMarkdown(nestedBlock.content)
212-
? renderStreamingMarkdown(
213-
nestedBlock.content,
214-
agentMarkdownOptions,
215-
)
216-
: nestedBlock.content
226+
const nestedStatus =
227+
typeof (nestedBlock as any).status === 'string'
228+
? (nestedBlock as any).status
229+
: undefined
230+
const isNestedStreamingText =
231+
isStreaming || nestedStatus === 'running'
232+
const rawNestedContent = isNestedStreamingText
233+
? trimTrailingNewlines(nestedBlock.content)
234+
: nestedBlock.content.trim()
235+
const renderKey = `${messageId}-agent-${block.agentId}-text-${nestedIdx}-${rawNestedContent.length}-${isNestedStreamingText ? 'stream' : 'final'}`
236+
const renderedContent = hasMarkdown(rawNestedContent)
237+
? isNestedStreamingText
238+
? renderStreamingMarkdown(
239+
rawNestedContent,
240+
agentMarkdownOptions,
241+
)
242+
: renderMarkdown(rawNestedContent, agentMarkdownOptions)
243+
: rawNestedContent
217244
return (
218245
<text
219-
key={`${messageId}-agent-${block.agentId}-text-${nestedIdx}`}
246+
key={renderKey}
220247
wrap
221248
style={{ fg: theme.agentText, marginLeft: 2 }}
222249
>
@@ -305,6 +332,9 @@ export const MessageBlock = ({
305332
return (
306333
<box
307334
key={`${messageId}-agent-${block.agentId}-tool-${nestedBlock.toolCallId}`}
335+
ref={(el: any) =>
336+
registerAgentRef(nestedBlock.toolCallId, el)
337+
}
308338
>
309339
<BranchItem
310340
name={displayInfo.name}
@@ -333,6 +363,7 @@ export const MessageBlock = ({
333363
return (
334364
<box
335365
key={`${messageId}-agent-${block.agentId}`}
366+
ref={(el: any) => registerAgentRef(block.agentId, el)}
336367
style={{ flexDirection: 'column', gap: 0 }}
337368
>
338369
<BranchItem
@@ -354,17 +385,26 @@ export const MessageBlock = ({
354385
})}
355386
</box>
356387
) : (
357-
<text
358-
key={`message-content-${messageId}`}
359-
wrap
360-
style={{ fg: textColor }}
361-
>
362-
{isLoading
363-
? ''
364-
: hasMarkdown(content)
365-
? renderStreamingMarkdown(content, markdownOptions)
366-
: content}
367-
</text>
388+
(() => {
389+
const isStreamingMessage = isLoading || !isComplete
390+
const normalizedContent = isStreamingMessage
391+
? trimTrailingNewlines(content)
392+
: content.trim()
393+
const displayContent = hasMarkdown(normalizedContent)
394+
? isStreamingMessage
395+
? renderStreamingMarkdown(normalizedContent, markdownOptions)
396+
: renderMarkdown(normalizedContent, markdownOptions)
397+
: normalizedContent
398+
return (
399+
<text
400+
key={`message-content-${messageId}-${normalizedContent.length}-${isStreamingMessage ? 'stream' : 'final'}`}
401+
wrap
402+
style={{ fg: textColor }}
403+
>
404+
{displayContent}
405+
</text>
406+
)
407+
})()
368408
)}
369409
{isAi && isComplete && (completionTime || credits) && (
370410
<text

0 commit comments

Comments
 (0)