Skip to content

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

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
ly-wang19:fix/note-wrap-cjk-width
Jun 27, 2026
Merged

fix(terminal): wrap long wide-char words by visible width, not code-point count#96746
vincentkoc merged 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.

@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 11:49 AM ET / 15:49 UTC.

Summary
The PR changes terminal-core note wrapping so oversized words are split by grapheme visible width and adds a CJK regression test for wrapNoteMessage.

PR surface: Source +13, Tests +12. Total +25 across 2 files.

Reproducibility: yes. source-level: call wrapNoteMessage with an unbroken CJK string at maxWidth: 20 on current main and measure each line with visibleWidth; the source still slices by code-point count. I did not execute it because this review was read-only.

Review metrics: none identified.

Root-cause cluster
Relationship: canonical
Canonical: #96746
Summary: This PR is the current open landing path for the CJK visible-width note-wrapping fix; the same-title predecessor was closed administratively, and the earlier merged note PR fixed a different Clack re-wrap failure.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • No automated repair is needed because the PR already contains the narrow code and regression-test change; maintainers only need normal review and merge gating.

Security
Cleared: The diff is limited to terminal text wrapping logic and a colocated Vitest regression test, with no dependency, workflow, secret, package, install-script, or network surface change.

Review details

Best 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 wrapNoteMessage with an unbroken CJK string at maxWidth: 20 on current main and measure each line with visibleWidth; the source still slices by code-point count. I did not execute it because this review was read-only.

Is this the best way to solve the issue?

Yes. splitLongWord is the fallback using the wrong unit, so reusing the existing splitGraphemes and visibleWidth helpers is the narrowest maintainable fix without changing Clack behavior or API surface.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against ddedf13190c1.

Label changes

Label justifications:

  • P2: This is a normal-priority terminal rendering bug with limited blast radius and focused regression coverage.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes copied before/after terminal output for the exported wrapping helpers plus fail-before regression-test evidence, which is sufficient for this pure terminal wrapping change.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied before/after terminal output for the exported wrapping helpers plus fail-before regression-test evidence, which is sufficient for this pure terminal wrapping change.
Evidence reviewed

PR surface:

Source +13, Tests +12. Total +25 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 17 4 +13
Tests 1 12 0 +12
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 29 4 +25

What I checked:

  • Current main bug path: On current main, splitLongWord still builds Array.from(word) and slices by maxLen, while wrapLine decides overflow with visibleWidth, so the fallback mixes code-point chunking with a visible-column budget. (packages/terminal-core/src/note.ts:25, ddedf13190c1)
  • Patch source shape: The PR imports splitGraphemes, accumulates visibleWidth(grapheme), and starts a new segment before adding a grapheme that would exceed maxLen. (packages/terminal-core/src/note.ts:29, d39d4a61e452)
  • Visible-width helper contract: visibleWidth is the terminal-core width model and sums widths over grapheme clusters after stripping ANSI sequences; CJK and emoji width behavior is covered in adjacent ansi tests. (packages/terminal-core/src/ansi.ts:116, ddedf13190c1)
  • Regression coverage: The PR adds a CJK regression test asserting every wrapped line is at most 20 visible columns and that joining the output lines preserves the input. (packages/terminal-core/src/table.test.ts:387, d39d4a61e452)
  • Latest release lacks the fix: Tag v2026.6.10 still contains the code-point Array.from(word) chunking implementation, so this behavior is not already shipped. (packages/terminal-core/src/note.ts:25, aa69b12d0086)
  • Related-item search: Live search found this PR and its closed same-title predecessor; no merged PR was found for the CJK visible-width splitLongWord fix.

Likely related people:

  • steipete: History shows the original Clack note wrapping helper and later sharing/refactor work on the terminal note path came from this author. (role: terminal note history contributor; confidence: medium; commits: 29884f8d6f2c, f7f5c24786ad; files: src/terminal/note.ts, packages/terminal-core/src/note.ts)
  • vignesh07: Commit 69418cca2091 added copy-sensitive token wrapping behavior in the same note-wrapping area and related TUI formatting tests. (role: adjacent wrapping contributor; confidence: medium; commits: 69418cca2091; files: src/terminal/note.ts, src/terminal/note.test.ts, src/tui/tui-formatters.ts)
  • xzh-icenter: The merged adjacent note-wrapping PR for Clack re-wrap behavior touched packages/terminal-core/src/note.ts and related terminal-core tests shortly before this PR. (role: recent adjacent area contributor; confidence: medium; commits: 90da1f07f090, b17a4ff5711c, 650423790034; files: packages/terminal-core/src/note.ts, packages/terminal-core/src/table.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 25, 2026
…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]>
@vincentkoc
vincentkoc requested a review from a team as a code owner June 27, 2026 15:38
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: matrix Channel integration: matrix channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: telegram Channel integration: telegram channel: voice-call Channel integration: voice-call app: android App: android app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime extensions: memory-core Extension: memory-core cli CLI command changes scripts Repository scripts commands Command implementations docker Docker and sandbox tooling agents Agent runtime and tooling channel: feishu Channel integration: feishu channel: irc extensions: acpx labels Jun 27, 2026
@vincentkoc
vincentkoc force-pushed the fix/note-wrap-cjk-width branch from c4fc4b5 to d39d4a6 Compare June 27, 2026 15:39
@openclaw-barnacle openclaw-barnacle Bot removed docs Improvements or additions to documentation channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: matrix Channel integration: matrix channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: telegram Channel integration: telegram channel: voice-call Channel integration: voice-call app: android App: android app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime extensions: memory-core Extension: memory-core cli CLI command changes scripts Repository scripts commands Command implementations docker Docker and sandbox tooling agents Agent runtime and tooling labels Jun 27, 2026
@vincentkoc

Copy link
Copy Markdown
Member

Merged via squash.

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

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants