Skip to content

fix(tui): measure display width so CJK output cannot overflow the terminal - #133

Open
zintown wants to merge 1 commit into
itayinbarr:mainfrom
zintown:fix/cjk-display-width
Open

zintown wants to merge 1 commit into
itayinbarr:mainfrom
zintown:fix/cjk-display-width

Conversation

@zintown

@zintown zintown commented Sep 24, 2026

Copy link
Copy Markdown

Problem

little-coder crashes whenever a custom widget or the sub-coder result panel renders East Asian (CJK) text. This is the #48 / #51 crash again, now triggered by wide characters:

pi exiting due to uncaughtException:
Error: Rendered line 147 exceeds terminal width (245 > 243).
This is likely caused by a custom TUI component not truncating its output.
    at TUI.doRender (.../pi-tui/dist/tui.js:1257:23)

The debug log shows the offending line is a sub-coder report written in Chinese. Its words are also split at odd places (skil l, resear ch, R EADME), because of the 32-char token chunking.

Cause

  • .pi/extensions/_shared/width.ts: visibleWidth() counts one column per code point, and truncateLineToWidth() advances one column per code point. Its comment notes that wide CJK/emoji are under-counted. CJK ideographs and fullwidth forms take two terminal columns.
  • .pi/extensions/subagent/index.ts: wrapPlainText() / wrapLine() measure String.length. A Chinese sentence rarely has spaces to break at, so a line that fits by character count renders nearly twice as wide.

The 2-column safety margin absorbs an emoji or two. It cannot absorb a paragraph of Chinese.

How to reproduce

  1. Run little-coder with any model and ask it to dispatch sub-coders that answer in Chinese, for example:

    用两个 sub-coder 分别调研 github.com/pans0020/research-writing 和另一个仓库,用中文写一段报告,包含链接和文件名。

  2. When a sub-coder returns a report line that fits the terminal width in characters but not in columns, pi aborts with the error above. A long mixed Chinese/ASCII line, such as a paragraph with a GitHub URL and file names, triggers it quickly on a ~240-column terminal. Narrower terminals need less text.

Without a model, the new unit test reproduces the crash condition directly. It is built from the exact line in the crash log:

git checkout main -- .pi/extensions/_shared/width.ts .pi/extensions/subagent/index.ts   # old code
npx vitest run .pi/extensions/subagent/cjk-width-repro.test.ts                          # 4 of 5 fail: lines wider than the width

The test measures columns with its own small table, independent of the code under test. It therefore fails on the old code instead of agreeing with the old width function.

Fix

  • width.ts: add charWidth(). It returns 2 for East Asian Wide/Fullwidth ranges and common emoji blocks, 0 for combining marks, zero-width characters and variation selectors, and 1 otherwise. visibleWidth() and truncateLineToWidth() now use it, so every widget that relies on them (tracker, plan-mode, progress bar, branding, …) truncates by columns.
  • subagent/index.ts: wrap by display width. A word wider than the line, such as a space-free CJK run, is hard-split by width. Nothing is dropped; the full report is still shown across wrapped lines.

Tests

  • _shared/width.test.ts: CJK, fullwidth and combining-mark widths, plus CJK truncation staying within the column budget.
  • subagent/cjk-width-repro.test.ts: the crashing line at widths 243/120/80/40 (every rendered line fits width - 1, and all content survives), plus a 300-char space-free CJK run.
  • npx vitest run .pi/extensions/_shared .pi/extensions/subagent: 111 passed, 2 skipped. npx tsc --noEmit: clean.
  • The full suite has 4 failures that also fail on unmodified main on this machine. They are the network-dependent Wikipedia live-integration tests, and llama-cpp-provider/swap-reprobe (a local server already listens on port 8080 here).

Tested on Linux, Node 24.21, little-coder 1.20.0, against a local Qwen3.8-27B served through llama.cpp. The crash stopped after the patch.

…minal

_shared/width.ts counted every code point as one column, and the
sub-coder result panel wrapped by String.length. East Asian wide
characters take two columns, so a Chinese sub-coder report that fit by
char count rendered wider than the terminal and pi-tui aborted the
session: "Rendered line 147 exceeds terminal width (245 > 243)".
This is the itayinbarr#48/itayinbarr#51 crash again, now for wide characters.

- width.ts: add charWidth() (2 for East Asian Wide/Fullwidth and emoji,
  0 for combining marks and zero-width characters, 1 otherwise) and use
  it in visibleWidth() and truncateLineToWidth(), which every custom
  widget relies on.
- subagent: wrap report lines by display width and hard-split words wider
  than the line (space-free CJK runs), instead of by String.length.
- Tests: width cases for CJK, fullwidth and combining marks, plus a
  regression built from the crashing line that measures columns
  independently of the code under test; it fails without this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant