fix(terminal): wrap long wide-char words by visible width, not code-p… · openclaw/openclaw@f857e8d
ly-wang19
·
2026-06-27
·
via Recent Commits to openclaw:main
File tree
packages/terminal-core/src
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | // Terminal Core module implements note behavior. |
2 | 2 | import { AsyncLocalStorage } from "node:async_hooks"; |
3 | 3 | import { note as clackNote } from "@clack/prompts"; |
4 | | -import { visibleWidth } from "./ansi.js"; |
| 4 | +import { splitGraphemes, visibleWidth } from "./ansi.js"; |
5 | 5 | import { stylePromptTitle } from "./prompt-style.js"; |
6 | 6 | import { normalizeLowercaseStringOrEmpty } from "./string.js"; |
7 | 7 | |
@@ -26,10 +26,23 @@ function splitLongWord(word: string, maxLen: number): string[] {
|
26 | 26 | if (maxLen <= 0) { |
27 | 27 | return [word]; |
28 | 28 | } |
29 | | -const chars = Array.from(word); |
| 29 | +// maxLen is a visible-column budget, so accumulate grapheme visible width (CJK/emoji count as 2 |
| 30 | +// columns) instead of code-point count; otherwise a wide-char run overflows the line by up to 2x. |
30 | 31 | const parts: string[] = []; |
31 | | -for (let i = 0; i < chars.length; i += maxLen) { |
32 | | -parts.push(chars.slice(i, i + maxLen).join("")); |
| 32 | +let current = ""; |
| 33 | +let currentWidth = 0; |
| 34 | +for (const grapheme of splitGraphemes(word)) { |
| 35 | +const width = visibleWidth(grapheme); |
| 36 | +if (current && currentWidth + width > maxLen) { |
| 37 | +parts.push(current); |
| 38 | +current = ""; |
| 39 | +currentWidth = 0; |
| 40 | +} |
| 41 | +current += grapheme; |
| 42 | +currentWidth += width; |
| 43 | +} |
| 44 | +if (current) { |
| 45 | +parts.push(current); |
33 | 46 | } |
34 | 47 | return parts.length > 0 ? parts : [word]; |
35 | 48 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -383,4 +383,16 @@ describe("wrapNoteMessage", () => {
|
383 | 383 | expect(wrapNoteMessage(new Error("boom"), { maxWidth: 20, columns: 80 })).toBe("Error: boom"); |
384 | 384 | expect(wrapNoteMessage({ message: "boom" }, { maxWidth: 20, columns: 80 })).toBe(""); |
385 | 385 | }); |
| 386 | + |
| 387 | +it("keeps wrapped lines within the visible-column budget for wide (CJK) words", () => { |
| 388 | +// A long CJK run with no separators reaches splitLongWord; each fullwidth char is 2 columns, |
| 389 | +// so splitting by code-point count would emit lines up to 2x the budget. |
| 390 | +const input = "東京特許許可局長今日休暇許可局長今日休暇東京特許"; |
| 391 | +const lines = wrapNoteMessage(input, { maxWidth: 20, columns: 80 }).split("\n"); |
| 392 | + |
| 393 | +for (const line of lines) { |
| 394 | +expect(visibleWidth(line)).toBeLessThanOrEqual(20); |
| 395 | +} |
| 396 | +expect(lines.join("")).toBe(input); |
| 397 | +}); |
386 | 398 | }); |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。