fix(terminal): clamp wide graphemes in narrow table cells#88557
Conversation
A width-2 CJK/emoji grapheme placed in a column whose content width is 1 cannot be wrapped by wrapLine, and padCell only padded (never truncated), so the content row rendered one column wider than the borders and pushed the right border out of alignment. Add an ANSI-aware truncateToVisibleWidth helper and clamp over-wide cells with it so every rendered line keeps the documented visibleWidth(line) === width invariant.
|
Codex review: needs maintainer review before merge. Reviewed May 31, 2026, 11:51 AM ET / 15:51 UTC. Summary PR surface: Source +51, Tests +58. Total +109 across 4 files. Reproducibility: yes. Current main's padCell returns over-wide text unchanged when visibleWidth(text) >= width, and wrapLine can pass a width-2 grapheme through a width-1 cell; I did not execute the Node repro in this read-only review. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Next step before merge
Security Review detailsBest possible solution: Land the focused terminal-core clamp with its regression tests after normal maintainer review and required checks, then let it resolve #88556. Do we have a high-confidence way to reproduce the issue? Yes. Current main's padCell returns over-wide text unchanged when visibleWidth(text) >= width, and wrapLine can pass a width-2 grapheme through a width-1 cell; I did not execute the Node repro in this read-only review. Is this the best way to solve the issue? Yes. Clamping in padCell is the narrowest maintainable fix because it is the final cell-width enforcement point before borders are emitted, and the helper preserves ANSI resets while enforcing the visible-width invariant. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against a71b121c69f0. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +51, Tests +58. Total +109 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Good catch on the width-2 grapheme overflow case, and I like that you covered both CJK and emoji paths. One edge case worth considering: do we need an explicit test for OSC8 hyperlinks or other zero-width control sequences that span past the truncation point? |
|
Land-ready proof for #88557. Summary:
Local proof:
CI proof:
Known proof gap:
|
Summary
Fixes #88556.
renderTablebroke its border-alignment invariant when a wide (width-2) CJK/emoji grapheme landed in a column whose content width is 1:wrapLinecannot split a single grapheme, andpadCellonly padded (never truncated), so the content row rendered one column wider than the borders and pushed the right border out of alignment. The repo already asserts this invariant for wide graphemes at a comfortable width (table.test.ts— "keeps table borders aligned when cells contain wide emoji graphemes"); this PR closes the narrow-column boundary it missed.renderTablebacks many CLI status/list tables (devices,plugins,skills,nodes,pairing,dns), whose cells can hold CJK/emoji names, and columns shrink on narrow terminals — so the misalignment is user-visible.Change
truncateToVisibleWidth(input, maxWidth)topackages/terminal-core/src/ansi.ts: drops whole grapheme clusters that would overflow the budget (never emits a half-width glyph) while copying ANSI sequences verbatim, including trailing resets, so a clamped styled cell does not bleed styling into padding/borders. Result always satisfiesvisibleWidth(result) <= maxWidth.padCellclamps over-wide content through that helper before padding, and the alignment branches emit the clamped content, so every rendered line keepsvisibleWidth(line) === width.truncateToVisibleWidth(budget + ANSI preservation) and tworenderTablealignment regression tests (narrow width-1 cell, and a narrow flex column receiving wide content).Verification
node scripts/run-vitest.mjs run packages/terminal-core/src/ansi.test.ts packages/terminal-core/src/table.test.ts→ 28 passed.node scripts/run-tsgo.mjs -p tsconfig.core.json(clean, non-incremental) → green.oxfmtwritten on the four touched files;git diff --checkclean.check-prod-typeslane is currently red onmainitself due to a pre-existingtsgo:extensionserror inextensions/acpx/src/runtime.ts(SessionRecordcast) that this PR does not touch.Real behavior proof
Behavior addressed: terminal table border misalignment when a width-2 grapheme lands in a width-1 (or otherwise too-narrow) column.
Real environment tested: local Node 24 dev checkout, OpenClaw
main@ 444562b, exercisingrenderTable/padCelldirectly vianode --import tsx.Exact steps or command run after this patch:
Evidence after fix:
Observed result after fix: all five lines are 24 columns; the right border is aligned. Before the fix the data row measured 25 against 24-wide borders.
What was not tested: live terminal rendering on a physical narrow-width TTY (validated programmatically via
visibleWidth); no channel/gateway path is involved.