[BUG] Thai output loses characters in the pty bytes themselves — TUI frame composition counts combining marks as one column (v2.1.235, Windows)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
When Claude Code answers in Thai, its TUI loses characters and composites unrelated rows on top of
one another. The important part, and the reason this is filed separately from the existing
combining-mark reports, is where the damage is: the characters are already missing from the
bytes Claude Code writes to the pty. This is not a terminal rendering artifact. It is data loss
inside frame composition, and no terminal — however it measures width — can recover it.
The trigger is a width disagreement. A Thai combining mark (U+0E31, U+0E34–U+0E3A, U+0E47–U+0E4E)
has display width 0, but frame composition appears to give it a column. Rows are then overlaid at
offsets that are too large by one column per mark, and whatever sat in the overrun is eaten before
it is ever written out.
Raw pty bytes from the capture linked below — no terminal emulator involved, no post-processing,\x1b[…H shown as written:
\x1b[16;3Hต้องกำหดฐานกำลังการผลิตปกติไว้ใไม่งั้นเดือนที่ขายน้อย \x1b[17;7Hต่หน่วยจะพุ่ง
Two rows of one sentence. What it should say isต้องกำหนดฐานกำลังการผลิตปกติไว้ ไม่งั้นเดือนที่ขายน้อย ระบบต้นทุนต่อหน่วยจะพุ่ง.
กำหนด→กำหด—นis gone from the bytes.ไว้ ไม่→ไว้ใไม่— the space is gone and a strayใfrom the overrun is left in its place.ต่อหน่วย→ต่หน่วย—อis gone, and the row is placed at column 7 (\x1b[17;7H) rather
than at the column its own text belongs to, so it abuts whatever an earlier frame left in
columns 3–6.
Another, from the same capture:
\x1b[6;3H(variance) แยกเป็นผลต่างคากับผลต่างปริมาณ
ผลต่างราคากับ → ผลต่างคากับ: รา is missing from the bytes.
Once the row arithmetic is wrong, neighbouring rows are damaged whatever their script. In the same
capture the spinner row is written as \x1b[28;3H followed by 56 spaces, which pushes✻ Worked for 55s past the 58-column margin and onto the next row, where the token and cost
readout is then drawn over it. That row holds no Thai at all.
What Should Happen?
Thai text is composed and emitted without losing characters, at any terminal width, the way English
already is. Frame composition should measure a combining mark as zero columns (grapheme-cluster
width), matching every terminal that will render it.
Error Messages/Logs
Full raw capture, exactly as the pty produced it: https://gist.github.com/plawanzz2017/20ba9ce7c7d71309883f16446073ae46
Claude Code v2.1.235, 58 columns, one Thai prompt, one Thai answer, straight off ConPTY. Control
bytes are written as \xNN escapes so the file stays readable; nothing else was changed, and the
header says how to turn it back into the exact byte stream. 3386 CSI sequences, zero DSR
(CSI n) and zero DA (CSI c) — Claude Code never asks the terminal anything, so nothing
about the terminal can be feeding it wrong numbers.
Replayed through a headless xterm at 58×30 the capture renders as:
ต้องกำหดฐานกำลังการผลิตปกติไว้ใไม่งั้นเดือนที่ขายน้อย
ระบบต่หน่วยจะพุ่ง เ
พรแล้วถ้าใช้ตัวเลขนั้นคาก็จะขึ้นราคาในจังหวะที่ตลาดแย่ดตรง
กลายเป็นวงจรมรณะ (death spiral) ที่ยราคายิ่งขายไม่ได้
Steps to Reproduce
- Start
claudein a terminal 58 columns wide. - Ask for a long Thai answer, with no tool use:
ตอบภาษาไทยยาวๆ 2 ย่อหน้า เรื่องการวางระบบต้นทุน ห้ามใช้ tool
- Watch the answer render, then compare the pty bytes with what the answer should say.
Any width from 58 to 180 reproduces the character loss. Below about 100 columns the damage also
reaches the prompt box: its border is drawn through the answer text, the input line is left
uncleared, and the box loses a border.
What has been ruled out
- The terminal emulator. The evidence above is the pty's own bytes. Whatever renders them —
xterm.js, Windows Terminal, anything — receives the same corruption.
- Terminal queries. Zero DSR and zero DA in the stream (measured, not assumed).
- Terminal identity. Setting
WT_SESSION/WT_PROFILE_ID/TERM_PROGRAM=WindowsTerminal
changes nothing.
- Width. Measured at 58, 78, 87, 100 and 180 columns. All lose characters.
- The script alone. The same prompts answered in English are clean at every one of those widths.
- Anything a terminal can do about it. Six variants of a stream-rewriting workaround were
measured, plus a full width-1 model (every Thai mark padded to its own column, matching Claude
Code's own arithmetic). None repair it, because the damage is upstream of the terminal.
Notes for whoever picks this up
Character loss clusters immediately after a Thai combining mark and takes the following character
and the space with it (ย่อหน้า เรื่อง → ย่อหน้เรื่อง, ต้นทุน ห้าม → ต้นห้าม). That is the
signature of a string overlaid at a column offset computed with the mark counted as one.
A second, older symptom from the same arithmetic, if it helps triangulate: when a cluster holding a
mark is redrawn during typing, Claude Code appends a bare backspace, as if the mark had taken a
column — BS ต่ BS, while BS ทำ (spacing vowel, width 1) gets none.
Related issues
Same defect class, none of them reporting the loss in the emitted bytes:
- #82338 — Tamil output collapses the TUI (combining-mark width), macOS, open.
- #81183 — Thai input line cursor drift, open; a regression of #22390, previously also #51444.
- #84986 — VS16 emoji width counted as 1 column, open.
- #60701 — Arabic diacritics, closed, and the one that names a mechanism:
Bun.stringWidth treats combining marks as width 1.
- #21149 — Thai output dropping vowels since v2.1.20, open.
Claude Model
Opus 5 (1M context), Claude Max
Is this a regression?
Not known to have ever worked. Related input-line symptoms regressed after #22390.
Claude Code Version
2.1.235
Platform
Windows
Operating System
Windows 11 Pro 26300
Terminal/Shell
pwsh 7, ConPTY via node-pty 1.1. TERM=xterm-256color, COLORTERM=truecolor.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗