diff --git a/packages/cli/src/ui/utils/TableRenderer.test.tsx b/packages/cli/src/ui/utils/TableRenderer.test.tsx
index 1059f841fe..422a40ce3a 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 c94e5c18a7..fd19b51000 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,31 +59,48 @@ 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(() => {
+ const numColumns = styledRows.reduce(
+ (max, row) => Math.max(max, row.length),
+ styledHeaders.length,
+ );
+
// --- Define Constraints per Column ---
- const constraints = cleanedHeaders.map((header, colIndex) => {
- let { contentWidth: maxContentWidth, maxWordWidth } =
- calculateWidths(header);
+ const constraints = Array.from({ length: numColumns }).map(
+ (_, colIndex) => {
+ const headerStyledChars = styledHeaders[colIndex] || [];
+ let { contentWidth: maxContentWidth, maxWordWidth } =
+ calculateWidths(headerStyledChars);
- rows.forEach((row) => {
- const cell = row[colIndex] || '';
- const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
- calculateWidths(cell);
+ styledRows.forEach((row) => {
+ const cellStyledChars = row[colIndex] || [];
+ const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
+ calculateWidths(cellStyledChars);
- maxContentWidth = Math.max(maxContentWidth, cellWidth);
- maxWordWidth = Math.max(maxWordWidth, cellWordWidth);
- });
+ 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 =
- cleanedHeaders.length + 1 + cleanedHeaders.length * COLUMN_PADDING;
+ const fixedOverhead = numColumns + 1 + numColumns * COLUMN_PADDING;
const availableWidth = Math.max(
0,
terminalWidth - fixedOverhead - TABLE_MARGIN,
@@ -136,17 +152,18 @@ export const TableRenderer: React.FC = ({
}
// --- Pre-wrap and Optimize Widths ---
- const actualColumnWidths = new Array(cleanedHeaders.length).fill(0);
+ const actualColumnWidths = new Array(numColumns).fill(0);
- const wrapAndProcessRow = (row: string[]) => {
+ const wrapAndProcessRow = (row: StyledChar[][]) => {
const rowResult: ProcessedLine[][] = [];
- row.forEach((cell, 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);
- const contentStyledChars = toStyledCharacters(cell);
const wrappedStyledLines = wrapStyledChars(
- contentStyledChars,
+ cellStyledChars,
contentWidth,
);
@@ -161,19 +178,18 @@ export const TableRenderer: React.FC = ({
width: styledCharsWidth(line),
}));
rowResult.push(lines);
- });
+ }
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 = (
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 c565b0c206..48bc00993a 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`] = `
"
┌─────────────┬───────┬─────────┐