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) {