fix(terminal): wrap long wide-char words by visible width, not code-point count#96746
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 11:49 AM ET / 15:49 UTC. Summary PR surface: Source +13, Tests +12. Total +25 across 2 files. Reproducibility: yes. source-level: call Review metrics: none identified. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. 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 this narrow terminal-core fix after maintainer review and normal CI, preserving the existing public API and copy-sensitive-token behavior. Do we have a high-confidence way to reproduce the issue? Yes, source-level: call Is this the best way to solve the issue? Yes. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ddedf13190c1. Label changesLabel justifications:
Evidence reviewedPR surface: Source +13, Tests +12. Total +25 across 2 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
|
…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]>
c4fc4b5 to
d39d4a6
Compare
|
Merged via squash.
|
What Problem This Solves
wrapNoteMessage(packages/terminal-core/src/note.ts) wraps note/diagnostic text to a visible-column budget — every fit decision inwrapLineis made withvisibleWidth(). But itssplitLongWordfallback (used when a single word is longer than the line budget, e.g. an unbroken CJK run) sliced the word into groups ofmaxLencode points viaArray.from(word). SincemaxLenis 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 atmaxWidth: 20emitted a single 40-column line.Why This Change Was Made
splitLongWordwas the lone place that measured in code points while the rest ofwrapLinemeasures visible columns. The fix accumulates grapheme visible width (the samesplitGraphemes/visibleWidthdecomposition the module already uses) and starts a new segment when the next grapheme would exceedmaxLen, 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:corepass; the fullpackages/terminal-core/src/table.test.tssuite passes with a new CJK regression test, and reverting the fix makes that test fail (fail-before).Real behavior proof
Behavior addressed:
wrapNoteMessageemitted wrapped lines up to twice the visible-column budget for long wide-character words, becausesplitLongWordsplit by code-point count rather than visible width.Real environment tested: Ran the exported
wrapNoteMessageandvisibleWidthfrompackages/terminal-core/src/{note,ansi}.tsviatsxon Node v22.22.1 (no mocks), before and after the patch, plus the colocated Vitest suite vianode scripts/run-vitest.mjs.Exact steps or command run after this patch:
wrapNoteMessage("東京特許許可局長今日休暇許可局長今日休暇東京特許", { maxWidth: 20, columns: 80 }).split("\n"), assertingvisibleWidth(line) <= 20for each line; thennode scripts/run-vitest.mjs packages/terminal-core/src/table.test.ts,node scripts/run-oxlint.mjs,pnpm tsgo:core.Evidence after fix:
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
visibleWidthmeasurement.