Skip to content

fix(terminal): wrap long wide-char words by visible width, not code-point count#96689

Closed
ly-wang19 wants to merge 1 commit into
openclaw:mainfrom
ly-wang19:fix/note-wrap-cjk-width
Closed

fix(terminal): wrap long wide-char words by visible width, not code-point count#96689
ly-wang19 wants to merge 1 commit into
openclaw:mainfrom
ly-wang19:fix/note-wrap-cjk-width

Conversation

@ly-wang19

Copy link
Copy Markdown
Contributor

What Problem This Solves

wrapNoteMessage (packages/terminal-core/src/note.ts) wraps note/diagnostic text to a visible-column budget — every fit decision in wrapLine is made with visibleWidth(). But its splitLongWord fallback (used when a single word is longer than the line budget, e.g. an unbroken CJK run) sliced the word into groups of maxLen code points via Array.from(word). Since maxLen is a visible-column budget and wide characters (CJK / fullwidth / emoji) are 2 columns each, a wide-char run produced lines up to twice the budget — e.g. a 24-character CJK word at maxWidth: 20 emitted a single 40-column line.

Why This Change Was Made

splitLongWord was the lone place that measured in code points while the rest of wrapLine measures visible columns. The fix accumulates grapheme visible width (the same splitGraphemes/visibleWidth decomposition the module already uses) and starts a new segment when the next grapheme would exceed maxLen, so every wrapped segment fits the budget. A single grapheme wider than the budget still emits on its own (preserving loop progress). ASCII wrapping is byte-identical.

User Impact

Note bodies containing long wide-character runs (CJK text, emoji) without separators now wrap within the requested column width instead of overflowing to ~2× and breaking terminal alignment / re-wrapping. No API change.

Evidence

oxfmt, oxlint, tsgo:core pass; the full packages/terminal-core/src/table.test.ts suite passes with a new CJK regression test, and reverting the fix makes that test fail (fail-before).

Real behavior proof

Behavior addressed: wrapNoteMessage emitted wrapped lines up to twice the visible-column budget for long wide-character words, because splitLongWord split by code-point count rather than visible width.

Real environment tested: Ran the exported wrapNoteMessage and visibleWidth from packages/terminal-core/src/{note,ansi}.ts via tsx on Node v22.22.1 (no mocks), before and after the patch, plus the colocated Vitest suite via node scripts/run-vitest.mjs.

Exact steps or command run after this patch: wrapNoteMessage("東京特許許可局長今日休暇許可局長今日休暇東京特許", { maxWidth: 20, columns: 80 }).split("\n"), asserting visibleWidth(line) <= 20 for each line; then node scripts/run-vitest.mjs packages/terminal-core/src/table.test.ts, node scripts/run-oxlint.mjs, pnpm tsgo:core.

Evidence after fix:

BEFORE (origin/main):
  wrapNoteMessage(<24 CJK chars>, {maxWidth:20}) -> line[0] visibleWidth=40, line[1]=8   (40 > 20 budget)
AFTER:
  wrapNoteMessage(<24 CJK chars>, {maxWidth:20}) -> line[0]=20, line[1]=20, line[2]=8     (all <= 20; join("") == input)
  ASCII control ("a".repeat(70), maxWidth 20)    -> unchanged (lines <= 20, as before)
table.test.ts -> exit 0 (all pass, incl. new CJK regression test); fail-before (revert source) -> the new test fails
oxlint -> exit 0 ; tsgo:core -> exit 0

Observed result after fix: every wrapped line of the CJK input is ≤ 20 visible columns (the long word breaks into 3 lines instead of one 40-column line), content is preserved (join("") equals the input), and ASCII wrapping is unchanged.

What was not tested: the actual @clack/prompts terminal render of a note (no interactive terminal in this environment); the fix is a pure wrapping-math change proven at the function and unit-test level using the module's own visibleWidth measurement.

…oint count

wrapNoteMessage measures every fit decision in visible columns, but its
splitLongWord fallback (for a single word longer than the line budget) sliced
the word into groups of maxLen code points. maxLen is a visible-column budget,
so a run of wide characters (CJK / fullwidth / emoji, 2 columns each) produced
lines up to twice the budget — e.g. a 24-char CJK word at maxWidth 20 emitted a
40-column line.

Accumulate grapheme visible width (the same unit visibleWidth uses) and start a
new segment when the next grapheme would exceed maxLen, so every wrapped segment
fits the budget; a single grapheme wider than the budget still emits, preserving
progress. ASCII wrapping is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added size: XS r: too-many-prs Auto-close: author has more than twenty active PRs. labels Jun 25, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 20 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

@ly-wang19

Copy link
Copy Markdown
Contributor Author

Reopening — I've trimmed my active-PR queue back under the repo limit. This fix is self-contained and verified (reproduce → fix → fail-before-test → green CI), with full proof in the PR body; no live-channel environment needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

r: too-many-prs Auto-close: author has more than twenty active PRs. size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant