From 24b5862c6975310b2ba6d4983c3c04f26bc84d8c Mon Sep 17 00:00:00 2001 From: Dev Randalpura Date: Tue, 10 Feb 2026 16:42:19 -0500 Subject: [PATCH 1/3] Precomputed styled headers and rows instead of computing them multiple times --- packages/cli/src/ui/utils/TableRenderer.tsx | 40 ++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx index c94e5c18a77..909e4a88f09 100644 --- a/packages/cli/src/ui/utils/TableRenderer.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.tsx @@ -28,8 +28,7 @@ const MIN_COLUMN_WIDTH = 5; const COLUMN_PADDING = 2; const TABLE_MARGIN = 2; -const calculateWidths = (text: string) => { - const styledChars = toStyledCharacters(text); +const calculateWidths = (styledChars: StyledChar[]) => { const contentWidth = styledCharsWidth(styledChars); const words: StyledChar[][] = wordBreakStyledChars(styledChars); @@ -60,16 +59,26 @@ export const TableRenderer: React.FC = ({ [headers], ); + const styledHeaders = useMemo( + () => cleanedHeaders.map((header) => toStyledCharacters(header)), + [cleanedHeaders], + ); + + const styledRows = useMemo( + () => rows.map((row) => row.map((cell) => toStyledCharacters(cell))), + [rows], + ); + const { wrappedHeaders, wrappedRows, adjustedWidths } = useMemo(() => { // --- Define Constraints per Column --- - const constraints = cleanedHeaders.map((header, colIndex) => { + const constraints = styledHeaders.map((headerStyledChars, colIndex) => { let { contentWidth: maxContentWidth, maxWordWidth } = - calculateWidths(header); + calculateWidths(headerStyledChars); - rows.forEach((row) => { - const cell = row[colIndex] || ''; + styledRows.forEach((row) => { + const cellStyledChars = row[colIndex] || []; const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } = - calculateWidths(cell); + calculateWidths(cellStyledChars); maxContentWidth = Math.max(maxContentWidth, cellWidth); maxWordWidth = Math.max(maxWordWidth, cellWordWidth); @@ -84,7 +93,7 @@ export const TableRenderer: React.FC = ({ // --- Calculate Available Space --- // Fixed overhead: borders (n+1) + padding (2n) const fixedOverhead = - cleanedHeaders.length + 1 + cleanedHeaders.length * COLUMN_PADDING; + styledHeaders.length + 1 + styledHeaders.length * COLUMN_PADDING; const availableWidth = Math.max( 0, terminalWidth - fixedOverhead - TABLE_MARGIN, @@ -136,17 +145,16 @@ export const TableRenderer: React.FC = ({ } // --- Pre-wrap and Optimize Widths --- - const actualColumnWidths = new Array(cleanedHeaders.length).fill(0); + const actualColumnWidths = new Array(styledHeaders.length).fill(0); - const wrapAndProcessRow = (row: string[]) => { + const wrapAndProcessRow = (row: StyledChar[][]) => { const rowResult: ProcessedLine[][] = []; - row.forEach((cell, colIndex) => { + row.forEach((cellStyledChars, colIndex) => { const allocatedWidth = finalContentWidths[colIndex]; const contentWidth = Math.max(1, allocatedWidth); - const contentStyledChars = toStyledCharacters(cell); const wrappedStyledLines = wrapStyledChars( - contentStyledChars, + cellStyledChars, contentWidth, ); @@ -165,14 +173,14 @@ export const TableRenderer: React.FC = ({ return rowResult; }; - const wrappedHeaders = wrapAndProcessRow(cleanedHeaders); - const wrappedRows = rows.map((row) => wrapAndProcessRow(row)); + const wrappedHeaders = wrapAndProcessRow(styledHeaders); + const wrappedRows = styledRows.map((row) => wrapAndProcessRow(row)); // Use the TIGHTEST widths that fit the wrapped content + padding const adjustedWidths = actualColumnWidths.map((w) => w + COLUMN_PADDING); return { wrappedHeaders, wrappedRows, adjustedWidths }; - }, [cleanedHeaders, rows, terminalWidth]); + }, [styledHeaders, styledRows, terminalWidth]); // Helper function to render a cell with proper width const renderCell = ( From 23ac64fb0a3ca4aa1bbb43d88b4b2c3ece24a430 Mon Sep 17 00:00:00 2001 From: Dev Randalpura Date: Tue, 10 Feb 2026 17:09:01 -0500 Subject: [PATCH 2/3] Fixed edge case where no headers would cause a runtime crash --- .../cli/src/ui/utils/TableRenderer.test.tsx | 31 +++++++++++ packages/cli/src/ui/utils/TableRenderer.tsx | 52 +++++++++++-------- .../__snapshots__/TableRenderer.test.tsx.snap | 20 +++++++ 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/ui/utils/TableRenderer.test.tsx b/packages/cli/src/ui/utils/TableRenderer.test.tsx index 1059f841fef..422a40ce3ae 100644 --- a/packages/cli/src/ui/utils/TableRenderer.test.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.test.tsx @@ -314,4 +314,35 @@ describe('TableRenderer', () => { }); expect(output).toMatchSnapshot(); }); + + it.each([ + { + name: 'renders correctly when headers are empty but rows have data', + headers: [] as string[], + rows: [['Data 1', 'Data 2']], + expected: ['Data 1', 'Data 2'], + }, + { + name: 'renders correctly when there are more headers than columns in rows', + headers: ['Header 1', 'Header 2', 'Header 3'], + rows: [['Data 1', 'Data 2']], + expected: ['Header 1', 'Header 2', 'Header 3', 'Data 1', 'Data 2'], + }, + ])('$name', ({ headers, rows, expected }) => { + const terminalWidth = 50; + + const { lastFrame } = renderWithProviders( + , + ); + + const output = lastFrame(); + expected.forEach((text) => { + expect(output).toContain(text); + }); + expect(output).toMatchSnapshot(); + }); }); diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx index 909e4a88f09..399793d37e7 100644 --- a/packages/cli/src/ui/utils/TableRenderer.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.tsx @@ -70,30 +70,37 @@ export const TableRenderer: React.FC = ({ ); const { wrappedHeaders, wrappedRows, adjustedWidths } = useMemo(() => { - // --- Define Constraints per Column --- - const constraints = styledHeaders.map((headerStyledChars, colIndex) => { - let { contentWidth: maxContentWidth, maxWordWidth } = - calculateWidths(headerStyledChars); - - styledRows.forEach((row) => { - const cellStyledChars = row[colIndex] || []; - const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } = - calculateWidths(cellStyledChars); + const numColumns = Math.max( + styledHeaders.length, + ...styledRows.map((r) => r.length), + ); - maxContentWidth = Math.max(maxContentWidth, cellWidth); - maxWordWidth = Math.max(maxWordWidth, cellWordWidth); - }); + // --- Define Constraints per Column --- + const constraints = Array.from({ length: numColumns }).map( + (_, colIndex) => { + const headerStyledChars = styledHeaders[colIndex] || []; + let { contentWidth: maxContentWidth, maxWordWidth } = + calculateWidths(headerStyledChars); + + styledRows.forEach((row) => { + const cellStyledChars = row[colIndex] || []; + const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } = + calculateWidths(cellStyledChars); + + maxContentWidth = Math.max(maxContentWidth, cellWidth); + maxWordWidth = Math.max(maxWordWidth, cellWordWidth); + }); - const minWidth = maxWordWidth; - const maxWidth = Math.max(minWidth, maxContentWidth); + const minWidth = maxWordWidth; + const maxWidth = Math.max(minWidth, maxContentWidth); - return { minWidth, maxWidth }; - }); + return { minWidth, maxWidth }; + }, + ); // --- Calculate Available Space --- // Fixed overhead: borders (n+1) + padding (2n) - const fixedOverhead = - styledHeaders.length + 1 + styledHeaders.length * COLUMN_PADDING; + const fixedOverhead = numColumns + 1 + numColumns * COLUMN_PADDING; const availableWidth = Math.max( 0, terminalWidth - fixedOverhead - TABLE_MARGIN, @@ -145,11 +152,13 @@ export const TableRenderer: React.FC = ({ } // --- Pre-wrap and Optimize Widths --- - const actualColumnWidths = new Array(styledHeaders.length).fill(0); + const actualColumnWidths = new Array(numColumns).fill(0); const wrapAndProcessRow = (row: StyledChar[][]) => { const rowResult: ProcessedLine[][] = []; - row.forEach((cellStyledChars, colIndex) => { + // Ensure we iterate up to numColumns, filling with empty cells if needed + for (let colIndex = 0; colIndex < numColumns; colIndex++) { + const cellStyledChars = row[colIndex] || []; const allocatedWidth = finalContentWidths[colIndex]; const contentWidth = Math.max(1, allocatedWidth); @@ -169,7 +178,7 @@ export const TableRenderer: React.FC = ({ width: styledCharsWidth(line), })); rowResult.push(lines); - }); + } return rowResult; }; @@ -181,7 +190,6 @@ export const TableRenderer: React.FC = ({ return { wrappedHeaders, wrappedRows, adjustedWidths }; }, [styledHeaders, styledRows, terminalWidth]); - // Helper function to render a cell with proper width const renderCell = ( content: ProcessedLine, diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap index c565b0c2068..48bc00993a4 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap @@ -44,6 +44,26 @@ exports[`TableRenderer > 'renders a table with only emojis and …' 1`] = ` " `; +exports[`TableRenderer > 'renders correctly when headers are em…' 1`] = ` +" +┌────────┬────────┐ +│ │ │ +├────────┼────────┤ +│ Data 1 │ Data 2 │ +└────────┴────────┘ +" +`; + +exports[`TableRenderer > 'renders correctly when there are more…' 1`] = ` +" +┌──────────┬──────────┬──────────┐ +│ Header 1 │ Header 2 │ Header 3 │ +├──────────┼──────────┼──────────┤ +│ Data 1 │ Data 2 │ │ +└──────────┴──────────┴──────────┘ +" +`; + exports[`TableRenderer > handles wrapped bold headers without showing markers 1`] = ` " ┌─────────────┬───────┬─────────┐ From 12a2e0ef87d4d3ffc52fc500840394c7d0d01511 Mon Sep 17 00:00:00 2001 From: Dev Randalpura Date: Tue, 10 Feb 2026 17:14:34 -0500 Subject: [PATCH 3/3] Changed .map to .reduce for calculating column count --- packages/cli/src/ui/utils/TableRenderer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx index 399793d37e7..fd19b510009 100644 --- a/packages/cli/src/ui/utils/TableRenderer.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.tsx @@ -70,9 +70,9 @@ export const TableRenderer: React.FC = ({ ); const { wrappedHeaders, wrappedRows, adjustedWidths } = useMemo(() => { - const numColumns = Math.max( + const numColumns = styledRows.reduce( + (max, row) => Math.max(max, row.length), styledHeaders.length, - ...styledRows.map((r) => r.length), ); // --- Define Constraints per Column ---