Skip to content

Commit 9800767

Browse files
Fix tool toggle rendering by wrapping content in text element
Tool content wasn't wrapped in <text> element, causing OpenTUI reconciler errors when renderMarkdown() returned Fragments with raw text nodes. Agent content worked because it was already wrapped. Now all content is consistently wrapped in <text> elements. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 1b075cd commit 9800767

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

cli/knowledge.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,65 @@ const Parent = () => {
353353
```
354354

355355
This pattern allows multiple styled components to be composed together within a single `<text>` element while avoiding the "Text must be created inside of a text node" error.
356+
357+
### Markdown Renderer Fragment Issue
358+
359+
**CRITICAL**: When `renderMarkdown()` returns a Fragment, it contains a **mix of JSX elements AND raw text strings** (newlines, text content, etc.). These raw strings become text nodes that violate OpenTUI's reconciler rules if not wrapped properly.
360+
361+
**The problem:**
362+
```tsx
363+
// renderMarkdown() returns something like:
364+
<>
365+
<strong>Bold text</strong>
366+
'\n' // ⚠️ Raw string!
367+
<span>More content</span>
368+
'\n' // ⚠️ Raw string!
369+
</>
370+
371+
// ❌ WRONG: Passing directly to <box>
372+
<box>
373+
{renderMarkdown(content)} // Raw strings create text nodes outside <text>
374+
</box>
375+
```
376+
377+
**The solution:**
378+
```tsx
379+
// ✅ CORRECT: Always wrap markdown output in <text>
380+
<box>
381+
<text wrap>
382+
{renderMarkdown(content)} // Raw strings now inside <text> element
383+
</text>
384+
</box>
385+
```
386+
387+
**Real-world example from BranchItem component:**
388+
389+
The bug occurred when tool toggles were rendered. Agent toggles worked fine, but tool toggles crashed.
390+
391+
**Why agents worked:**
392+
```tsx
393+
// Agent content always wrapped in <text>
394+
<text wrap style={{ fg: theme.agentText }}>
395+
{nestedBlock.content}
396+
</text>
397+
```
398+
399+
**Why tools failed before fix:**
400+
```tsx
401+
// Tool content passed directly to <box> - raw strings violated reconciler rules!
402+
<box>
403+
{displayContent} // Could be renderMarkdown() output with raw strings
404+
</box>
405+
```
406+
407+
**The fix:**
408+
```tsx
409+
// Always wrap ALL content in <text>, whether string or ReactNode
410+
<box>
411+
<text wrap fg={theme.agentText}>
412+
{content} // Safe for both strings and markdown Fragments
413+
</text>
414+
</box>
415+
```
416+
417+
**Key lesson:** Any component that receives content from `renderMarkdown()` or `renderStreamingMarkdown()` MUST wrap it in a `<text>` element, even if the content might be ReactNode. The Fragment can contain raw strings that need the text wrapper to be valid.

cli/src/components/branch-item.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ export const BranchItem = ({
8686
{finishedPreview}
8787
</text>
8888
)}
89-
{!isCollapsed && content && content}
89+
{!isCollapsed && content && (
90+
<text wrap fg={theme.agentText}>
91+
{content}
92+
</text>
93+
)}
9094
</box>
9195
</box>
9296
</box>

0 commit comments

Comments
 (0)