From 0607177cf24993a80c5f759e963c56b625839313 Mon Sep 17 00:00:00 2001
From: CodebuffAI <189203002+CodebuffAI@users.noreply.github.com>
Date: Mon, 9 Feb 2026 04:27:26 +0000
Subject: [PATCH] Update from Aether
---
.../__tests__/markdown-renderer.test.tsx | 42 +++---
cli/src/utils/markdown-renderer.tsx | 140 +++++++++++-------
2 files changed, 111 insertions(+), 71 deletions(-)
diff --git a/cli/src/utils/__tests__/markdown-renderer.test.tsx b/cli/src/utils/__tests__/markdown-renderer.test.tsx
index 26f9697a2..9cc2d35ff 100644
--- a/cli/src/utils/__tests__/markdown-renderer.test.tsx
+++ b/cli/src/utils/__tests__/markdown-renderer.test.tsx
@@ -323,13 +323,13 @@ codebuff "implement feature" --verbose
expect(nodes[2]).toBe(' to commit.')
})
- test('truncates table columns when content exceeds available width', () => {
- // Table with very long content that should be truncated
- const markdown = `| ID | This is a very long column header that should be truncated |
-| -- | ---------------------------------------------------------- |
+ test('wraps table columns when content exceeds available width', () => {
+ // Table with very long content that should be wrapped
+ const markdown = `| ID | This is a very long column header that should wrap |
+| -- | -------------------------------------------------- |
| 1 | This cell has extremely long content that definitely exceeds the width |`
- // Use a narrow codeBlockWidth to force truncation
+ // Use a narrow codeBlockWidth to force wrapping
const output = renderMarkdown(markdown, { codeBlockWidth: 50 })
const nodes = flattenNodes(output)
@@ -343,24 +343,28 @@ codebuff "implement feature" --verbose
})
.join('')
- // Should contain ellipsis indicating truncation of the long column
- expect(textContent).toContain('…')
- // The short column content should be present (ID and 1 are short enough)
+ // Should NOT contain ellipsis - content wraps instead of truncating
+ expect(textContent).not.toContain('…')
+ // The short column content should be present
expect(textContent).toContain('ID')
expect(textContent).toContain('1')
// Box-drawing characters should still be present
expect(textContent).toContain('│')
expect(textContent).toContain('─')
- // The long header should be truncated (not fully present)
- expect(textContent).not.toContain('This is a very long column header that should be truncated')
+ // The full content should be present across wrapped lines
+ expect(textContent).toContain('long')
+ expect(textContent).toContain('header')
+ expect(textContent).toContain('wrap')
+ expect(textContent).toContain('extremely')
+ expect(textContent).toContain('exceeds')
})
- test('does not truncate table columns when content fits available width', () => {
+ test('does not wrap table columns when content fits available width', () => {
const markdown = `| Name | Age |
| ---- | --- |
| John | 30 |`
- // Use a wide codeBlockWidth so no truncation is needed
+ // Use a wide codeBlockWidth so no wrapping is needed
const output = renderMarkdown(markdown, { codeBlockWidth: 80 })
const nodes = flattenNodes(output)
@@ -374,8 +378,6 @@ codebuff "implement feature" --verbose
})
.join('')
- // Should NOT contain ellipsis when content fits
- expect(textContent).not.toContain('…')
// All content should be present in full
expect(textContent).toContain('Name')
expect(textContent).toContain('Age')
@@ -383,13 +385,13 @@ codebuff "implement feature" --verbose
expect(textContent).toContain('30')
})
- test('proportionally shrinks table columns when table is too wide', () => {
+ test('wraps and shows full content when table is too wide', () => {
// Three columns of roughly equal width
const markdown = `| Column One | Column Two | Column Three |
| ---------- | ---------- | ------------ |
| Value1 | Value2 | Value3 |`
- // Very narrow width to force significant shrinking
+ // Very narrow width to force significant wrapping
const output = renderMarkdown(markdown, { codeBlockWidth: 30 })
const nodes = flattenNodes(output)
@@ -407,7 +409,11 @@ codebuff "implement feature" --verbose
expect(textContent).toContain('│')
expect(textContent).toContain('┌')
expect(textContent).toContain('└')
- // With such narrow width, some content should be truncated
- expect(textContent).toContain('…')
+ // Full content should still be visible (wrapped, not truncated)
+ expect(textContent).not.toContain('…')
+ // All values should be present
+ expect(textContent).toContain('Value1')
+ expect(textContent).toContain('Value2')
+ expect(textContent).toContain('Value3')
})
})
diff --git a/cli/src/utils/markdown-renderer.tsx b/cli/src/utils/markdown-renderer.tsx
index 0363ed8f2..662602cc2 100644
--- a/cli/src/utils/markdown-renderer.tsx
+++ b/cli/src/utils/markdown-renderer.tsx
@@ -644,28 +644,55 @@ const renderLink = (link: Link, state: RenderState): ReactNode[] => {
}
/**
- * Truncates text to fit within a specified width, adding ellipsis if needed.
+ * Wraps text to fit within a specified width, returning an array of lines.
* Uses stringWidth to properly measure Unicode and wide characters.
+ * Performs word-wrapping where possible, falling back to character-level
+ * breaking for words that exceed the column width.
*/
-const truncateText = (text: string, maxWidth: number): string => {
- if (maxWidth < 1) return ''
+const wrapText = (text: string, maxWidth: number): string[] => {
+ if (maxWidth < 1) return ['']
+ if (!text) return ['']
const textWidth = stringWidth(text)
- if (textWidth <= maxWidth) {
- return text
- }
-
- // Need to truncate - leave room for ellipsis
- if (maxWidth === 1) return '…'
-
- let truncated = ''
- let width = 0
- for (const char of text) {
- const charWidth = stringWidth(char)
- if (width + charWidth + 1 > maxWidth) break // +1 for ellipsis
- truncated += char
- width += charWidth
+ if (textWidth <= maxWidth) return [text]
+
+ const lines: string[] = []
+ let currentLine = ''
+ let currentWidth = 0
+ const tokens = text.split(/(\s+)/)
+
+ for (const token of tokens) {
+ if (!token) continue
+ const tokenWidth = stringWidth(token)
+ const isWhitespace = /^\s+$/.test(token)
+
+ // Skip leading whitespace on new lines
+ if (isWhitespace && currentWidth === 0) continue
+
+ if (tokenWidth > maxWidth && !isWhitespace) {
+ // Break long words character by character
+ for (const char of token) {
+ const charWidth = stringWidth(char)
+ if (currentWidth + charWidth > maxWidth) {
+ if (currentLine) lines.push(currentLine)
+ currentLine = char
+ currentWidth = charWidth
+ } else {
+ currentLine += char
+ currentWidth += charWidth
+ }
+ }
+ } else if (currentWidth + tokenWidth > maxWidth) {
+ if (currentLine) lines.push(currentLine.trimEnd())
+ currentLine = isWhitespace ? '' : token
+ currentWidth = isWhitespace ? 0 : tokenWidth
+ } else {
+ currentLine += token
+ currentWidth += tokenWidth
+ }
}
- return truncated + '…'
+
+ if (currentLine) lines.push(currentLine.trimEnd())
+ return lines.length > 0 ? lines : ['']
}
/**
@@ -756,53 +783,60 @@ const renderTable = (table: Table, state: RenderState): ReactNode[] => {
nodes.push('\n')
}
+ // Pre-wrap all cell contents so we know the height of each row
+ const wrappedRows: string[][][] = rows.map((row) =>
+ Array.from({ length: numCols }, (_, i) => {
+ const cellText = row[i] || ''
+ return wrapText(cellText, columnWidths[i])
+ }),
+ )
+
// Render top border
renderSeparator('┌', '┬', '┐')
- // Render each row
- table.children.forEach((row, rowIdx) => {
+ // Render each row with word-wrapped cells
+ wrappedRows.forEach((wrappedCells, rowIdx) => {
const isHeader = rowIdx === 0
- const cells = (row as TableRow).children as TableCell[]
+ const rowHeight = Math.max(...wrappedCells.map((lines) => lines.length), 1)
+
+ // Render each visual line in the row
+ for (let lineIdx = 0; lineIdx < rowHeight; lineIdx++) {
+ for (let cellIdx = 0; cellIdx < numCols; cellIdx++) {
+ const colWidth = columnWidths[cellIdx]
+ const lineText = wrappedCells[cellIdx][lineIdx] || ''
+ const displayText = padText(lineText, colWidth)
+
+ // Left border for first cell
+ if (cellIdx === 0) {
+ nodes.push(
+
+ │
+ ,
+ )
+ }
+
+ // Cell content with padding
+ nodes.push(
+
+ {' '}
+ {displayText}
+ {' '}
+ ,
+ )
- // Render row content
- for (let cellIdx = 0; cellIdx < numCols; cellIdx++) {
- const cell = cells[cellIdx]
- const cellText = cell ? nodeToPlainText(cell).trim() : ''
- const colWidth = columnWidths[cellIdx]
-
- // Truncate and pad the cell content
- const displayText = padText(truncateText(cellText, colWidth), colWidth)
-
- // Left border for first cell
- if (cellIdx === 0) {
+ // Separator or right border
nodes.push(
│
,
)
}
-
- // Cell content with padding
- nodes.push(
-
- {' '}
- {displayText}
- {' '}
- ,
- )
-
- // Separator or right border
- nodes.push(
-
- │
- ,
- )
+ nodes.push('\n')
}
- nodes.push('\n')
// Add separator line after header
if (isHeader) {