fix(ui): correct styled table width calculations (#20042)

This commit is contained in:
Dev Randalpura
2026-02-26 17:31:21 -08:00
committed by GitHub
parent 085441352b
commit ecfa4e0437
25 changed files with 1312 additions and 602 deletions

View File

@@ -5,18 +5,19 @@
*/
import React, { useMemo } from 'react';
import { Text, Box } from 'ink';
import { styledCharsToString } from '@alcalzone/ansi-tokenize';
import {
Text,
Box,
type StyledChar,
toStyledCharacters,
styledCharsToString,
styledCharsWidth,
wordBreakStyledChars,
wrapStyledChars,
widestLineFromStyledChars,
} from 'ink';
import { theme } from '../semantic-colors.js';
import { RenderInline } from './InlineMarkdownRenderer.js';
import { parseMarkdownToANSI } from './InlineMarkdownRenderer.js';
import { stripUnsafeCharacters } from './textUtils.js';
interface TableRendererProps {
@@ -29,6 +30,19 @@ const MIN_COLUMN_WIDTH = 5;
const COLUMN_PADDING = 2;
const TABLE_MARGIN = 2;
/**
* Parses markdown to StyledChar array by first converting to ANSI.
* This ensures character counts are accurate (markdown markers are removed
* and styles are applied to the character's internal style object).
*/
const parseMarkdownToStyledChars = (
text: string,
defaultColor?: string,
): StyledChar[] => {
const ansi = parseMarkdownToANSI(text, defaultColor);
return toStyledCharacters(ansi);
};
const calculateWidths = (styledChars: StyledChar[]) => {
const contentWidth = styledCharsWidth(styledChars);
@@ -53,25 +67,26 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
rows,
terminalWidth,
}) => {
// Clean headers: remove bold markers since we already render headers as bold
// and having them can break wrapping when the markers are split across lines.
const cleanedHeaders = useMemo(
() => headers.map((header) => header.replace(/\*\*(.*?)\*\*/g, '$1')),
[headers],
);
const styledHeaders = useMemo(
() =>
cleanedHeaders.map((header) =>
toStyledCharacters(stripUnsafeCharacters(header)),
headers.map((header) =>
parseMarkdownToStyledChars(
stripUnsafeCharacters(header),
theme.text.link,
),
),
[cleanedHeaders],
[headers],
);
const styledRows = useMemo(
() =>
rows.map((row) =>
row.map((cell) => toStyledCharacters(stripUnsafeCharacters(cell))),
row.map((cell) =>
parseMarkdownToStyledChars(
stripUnsafeCharacters(cell),
theme.text.primary,
),
),
),
[rows],
);
@@ -132,7 +147,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
const scale =
(availableWidth - finalTotalShortColumnWidth) /
(totalMinWidth - finalTotalShortColumnWidth);
(totalMinWidth - finalTotalShortColumnWidth) || 0;
finalContentWidths = constraints.map((c) => {
if (c.maxWidth <= MIN_COLUMN_WIDTH && finalTotalShortColumnWidth > 0) {
return c.minWidth;
@@ -201,6 +216,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
return { wrappedHeaders, wrappedRows, adjustedWidths };
}, [styledHeaders, styledRows, terminalWidth]);
// Helper function to render a cell with proper width
const renderCell = (
content: ProcessedLine,
@@ -216,10 +232,10 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
<Text>
{isHeader ? (
<Text bold color={theme.text.link}>
<RenderInline text={content.text} />
{content.text}
</Text>
) : (
<RenderInline text={content.text} />
<Text>{content.text}</Text>
)}
{' '.repeat(paddingNeeded)}
</Text>
@@ -253,18 +269,18 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
});
return (
<Text color={theme.text.primary}>
<Text color={theme.border.default}></Text>{' '}
<Box flexDirection="row">
<Text color={theme.border.default}></Text>
{renderedCells.map((cell, index) => (
<React.Fragment key={index}>
{cell}
<Box paddingX={1}>{cell}</Box>
{index < renderedCells.length - 1 && (
<Text color={theme.border.default}>{' │ '}</Text>
<Text color={theme.border.default}></Text>
)}
</React.Fragment>
))}{' '}
))}
<Text color={theme.border.default}></Text>
</Text>
</Box>
);
};
@@ -274,7 +290,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
rowIndex?: number,
isHeader = false,
): React.ReactNode => {
const key = isHeader ? 'header' : `${rowIndex}`;
const key = rowIndex === -1 ? 'header' : `${rowIndex}`;
const maxHeight = Math.max(...wrappedCells.map((lines) => lines.length), 1);
const visualRows: React.ReactNode[] = [];