Skip to content

[Bug]: renderTable misaligns borders when a wide CJK/emoji grapheme lands in a narrow (width-1) column #88556

Description

@jbetala7

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions