fix(tui): width-aware markdown table rendering with vertical fallback (#26195)

* refactor(tui): thread cols through Md/StreamingMd/renderTable, update cache key

* feat(tui): three-tier width calc + full-line string rendering in renderTable

Replaces the old renderTable (L203-244) with:
- Empty table guard
- Ragged row normalization
- Three-tier column width calculation (ideal → proportional shrink → hard scale)
- Rounding remainder distribution
- Full-line string rendering (one <Text> per row, not per cell)
- wrap=truncate-end on all table lines
- All cells rendered as plain text via stripInlineMarkup

No wrapping or vertical fallback yet — those come in Phase 3 and 4.

* feat(tui): wrapCell with grapheme-safe hard-break + multi-line row rendering

Adds:
- Intl.Segmenter-based grapheme splitting (fallback to [...word])
- wrapCell() for width-correct word wrapping on stripped text
- Multi-line row rendering with LineEntry metadata (header/separator/body)
- Post-render safety condition (maxLineWidth computed, vertical fallback in Task 4)
- Non-wrapping path preserved for tables that fit at ideal widths

* feat(tui): vertical key-value fallback with scaled threshold + safety check

Wires:
- Scaled row-height threshold (numCols<=3: 8, <=6: 5, else: 4)
- Post-render safety check (maxLineWidth > available space)
- Header-only edge case
- Vertical format: bold headers, stripped cell text, clamped separator width
- Iterates headers (not rows) for consistent key-value fields on ragged rows

* test(tui): pass cols to Md in test helpers, add width-overflow assertions

- renderAtWidth now passes cols={columns} to <Md> so width-aware code paths
  are exercised in tests
- tableFuzz: every rendered line must fit within allocated width (stringWidth)
- tableRepro: separator regex updated to match truncation ellipsis
- stringWidth imported from @hermes/ink for CJK-correct assertions

* fix(tui): address adversarial review — comment tier 3 budget overshoot, eliminate redundant wrapCell

- Add comment on Tier 3 MIN_COL_WIDTH clamp exceeding budget (self-heals via safetyOverflow)
- Track tallestBodyRow during allEntries build pass instead of re-wrapping every cell
  in a second traversal (eliminates O(cells) of redundant stripInlineMarkup+stringWidth)

* fix(tui): pass cols to recursive fenced-markdown Md, fix test frame extraction

- Thread cols into <Md> for fenced markdown blocks (L734) so nested
  tables use the width-aware renderer instead of max-content path
- Fix renderAtWidth helpers to extract final Ink repaint frame instead
  of concatenating all intermediate frames (REPAINT_RE split)
- Add fenced-markdown-table fixture to tableFuzz (exercises the nested path)

* chore: remove repro test suites and tmux driver script

These were scaffolding for development/reproduction — not needed in the PR.
This commit is contained in:
Siddharth Balyan 2026-05-16 06:55:56 +05:30 committed by GitHub
parent 006937f7d0
commit 55c9f32060
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 295 additions and 47 deletions

View file

@ -139,13 +139,15 @@ export const MessageLine = memo(function MessageLine({
}
if (msg.role === 'assistant') {
const bodyWidth = transcriptBodyWidth(cols, msg.role, t.brand.prompt)
return isStreaming ? (
// Incremental markdown: split at the last stable block boundary so
// only the in-flight tail re-tokenizes per delta. See
// streamingMarkdown.tsx for the cost model.
<StreamingMd compact={compact} t={t} text={boundedLiveRenderText(msg.text)} />
<StreamingMd cols={bodyWidth} compact={compact} t={t} text={boundedLiveRenderText(msg.text)} />
) : (
<Md compact={compact} t={t} text={limitHistoryRender ? boundedHistoryRenderText(msg.text) : msg.text} />
<Md cols={bodyWidth} compact={compact} t={t} text={limitHistoryRender ? boundedHistoryRenderText(msg.text) : msg.text} />
)
}