Bug type
Regression / correctness (terminal table rendering)
Summary
renderTable (packages/terminal-core/src/table.ts) breaks its border-alignment invariant when a wide (width-2) CJK or emoji grapheme lands in a column whose content width is 1. wrapLine cannot split a single grapheme, and padCell only pads — it never truncates — so the content row renders one column wider than the border/separator lines and the right border is pushed out of alignment.
The repo already asserts this invariant for wide graphemes at a comfortable width (packages/terminal-core/src/table.test.ts — "keeps table borders aligned when cells contain wide emoji graphemes", which checks visibleWidth(line) === width for every line). The narrow-column boundary is simply uncovered.
This is reachable in real CLI output: renderTable backs many status/list tables (devices, plugins, skills, nodes, pairing, dns, etc.), whose cells can hold user/plugin-provided names containing CJK or emoji, and columns shrink on narrow terminals.
Steps to reproduce
On upstream main (444562b), run:
node --import tsx -e '
import { renderTable } from "./packages/terminal-core/src/table.ts";
import { visibleWidth } from "./packages/terminal-core/src/ansi.ts";
const out = renderTable({
width: 10, border: "ascii",
columns: [{ key: "A", header: "long header here" }, { key: "B", header: "", flex: true }],
rows: [{ A: "data", B: "中" }],
});
const lines = out.trimEnd().split("\n");
console.log("line widths:", JSON.stringify(lines.map(visibleWidth)));
console.log(out);
'
Current behavior (upstream main)
line widths: [24,24,24,25,24]
+------------------+---+
| long header here | |
+------------------+---+
| data | 中 |
+------------------+---+
The data row is 25 columns while every border line is 24 — the right border no longer lines up. A more minimal trigger: renderTable({ border: "ascii", padding: 0, columns: [{ key: "B", header: "B", minWidth: 1, maxWidth: 1 }], rows: [{ B: "中" }] }) yields a width-4 content row against width-3 borders.
Expected behavior
Every rendered line should have equal visible width (the invariant the existing wide-emoji test asserts). A wide grapheme that cannot fit a narrow cell should be clamped so the cell stays exactly width columns, rather than overflowing and misaligning the borders.
Root cause
padCell (packages/terminal-core/src/table.ts) returns the cell text unchanged whenever visibleWidth(text) >= width, so an un-wrappable over-wide grapheme overflows the cell.
Candidate fix shape
Add an ANSI-aware truncateToVisibleWidth(input, maxWidth) helper to packages/terminal-core/src/ansi.ts that drops whole grapheme clusters that would overflow (preserving ANSI sequences, including trailing resets, so styling does not bleed), and have padCell clamp over-wide content through it before padding. That keeps visibleWidth(line) === width for all lines.
Duplicate search
Searched open issues (table border alignment, table wide emoji column, renderTable CJK) and open/closed PRs (padCell, truncateToVisibleWidth, terminal-core table align, table border wide grapheme, renderTable alignment) — no existing issue or PR covers this. (PR #88209 fixes an unrelated token formatter; PR #55596 touches a different Markdown IR table renderer in src/markdown/ir.ts.)
Environment
OpenClaw main @ 444562b
Bug type
Regression / correctness (terminal table rendering)
Summary
renderTable(packages/terminal-core/src/table.ts) breaks its border-alignment invariant when a wide (width-2) CJK or emoji grapheme lands in a column whose content width is 1.wrapLinecannot split a single grapheme, andpadCellonly pads — it never truncates — so the content row renders one column wider than the border/separator lines and the right border is pushed out of alignment.The repo already asserts this invariant for wide graphemes at a comfortable width (
packages/terminal-core/src/table.test.ts— "keeps table borders aligned when cells contain wide emoji graphemes", which checksvisibleWidth(line) === widthfor every line). The narrow-column boundary is simply uncovered.This is reachable in real CLI output:
renderTablebacks many status/list tables (devices,plugins,skills,nodes,pairing,dns, etc.), whose cells can hold user/plugin-provided names containing CJK or emoji, and columns shrink on narrow terminals.Steps to reproduce
On upstream
main(444562b), run:Current behavior (upstream main)
The data row is 25 columns while every border line is 24 — the right border no longer lines up. A more minimal trigger:
renderTable({ border: "ascii", padding: 0, columns: [{ key: "B", header: "B", minWidth: 1, maxWidth: 1 }], rows: [{ B: "中" }] })yields a width-4 content row against width-3 borders.Expected behavior
Every rendered line should have equal visible width (the invariant the existing wide-emoji test asserts). A wide grapheme that cannot fit a narrow cell should be clamped so the cell stays exactly
widthcolumns, rather than overflowing and misaligning the borders.Root cause
padCell(packages/terminal-core/src/table.ts) returns the cell text unchanged whenevervisibleWidth(text) >= width, so an un-wrappable over-wide grapheme overflows the cell.Candidate fix shape
Add an ANSI-aware
truncateToVisibleWidth(input, maxWidth)helper topackages/terminal-core/src/ansi.tsthat drops whole grapheme clusters that would overflow (preserving ANSI sequences, including trailing resets, so styling does not bleed), and havepadCellclamp over-wide content through it before padding. That keepsvisibleWidth(line) === widthfor all lines.Duplicate search
Searched open issues (table border alignment, table wide emoji column, renderTable CJK) and open/closed PRs (
padCell,truncateToVisibleWidth, terminal-core table align, table border wide grapheme, renderTable alignment) — no existing issue or PR covers this. (PR #88209 fixes an unrelated token formatter; PR #55596 touches a different Markdown IR table renderer insrc/markdown/ir.ts.)Environment
OpenClaw
main@ 444562b