Tamil script output collapses TUI rendering (combining-mark width bug) — v2.1.220 macOS
Description
Printing Tamil script in Claude Code's chat output collapses the TUI: overlapping glyphs, misaligned columns, and a corrupted redraw of the whole conversation area. The renderer appears to assume one display column per code point, but roughly 40% of characters in running Tamil text are combining marks (vowel signs and viramas, Unicode category M) that occupy zero columns.
This is the same defect class as the reported Thai (#81183, currently open on this same version), Devanagari (#53968), and Bengali (#66586) issues, hitting Tamil on macOS.
Environment
- Claude Code v2.1.220
- macOS (Darwin 25.5.0), default terminal, zsh
Steps to reproduce
- Start
claudein any project. - Ask for any response containing a sentence of Tamil script, e.g.:
> வணக்கம், இன்று எப்படி இருக்கீர்கள்?
- Observe the rendered output and subsequent redraws.
Expected
Tamil text renders with correct column accounting (grapheme-cluster width, not per-code-point width); surrounding UI stays aligned.
Actual
Glyphs overlap, columns shift for the rest of the frame, and the redraw corrupts previously rendered lines. The same corruption occurs when viewing diffs of files containing Tamil (e.g. via the edit-preview UI), so it affects both chat output and tool-result rendering.
Notes
- Affects any script whose text is dense in combining marks (Devanagari, Arabic, Hebrew, Thai, Korean jamo behave similarly per the linked issues).
- Workaround in use: avoid emitting Indic script to the terminal entirely and use Latin transliteration in chat, which is lossy.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Adding Telugu as another data point on the same defect, plus a measurement that may help pin down what the width function should actually return.
Same environment: macOS (Darwin 25.5.0), default terminal, zsh.
Measuring three different notions of "length" over the same strings:
| string | code points | wcwidth sum | cluster count |
|---|--:|--:|--:|
|
Hello world| 11 | 11 | 11 ||
నమస్కారం| 8 | 6 | 5 ||
Hello నమస్కారం| 14 | 12 | 11 ||
తెలుగు భాష| 10 | 8 | 6 ||
కృష్ణ| 5 | 4 | 3 ||
नमस्ते(Devanagari, for comparison) | 6 | 4 | 4 |ASCII is the only row where all three agree. Every Telugu row disagrees by 1-3 columns, which matches the drift seen in padded or bordered output — the error accumulates per word, so a line mixing Telugu with English goes wrong in the middle rather than at the end.
Decomposition of
నమస్కారంisLo Lo Lo Mn Lo Mn Lo Mc. TheMnmarks contribute 0 to a wcwidth sum but do participate in a rendered cluster, which is the mismatch you described.One caveat worth flagging for whoever picks this up: grapheme-cluster count is closer but still not the rendered width for Indic scripts. UAX #29 extended grapheme clusters do not merge across a virama, so
నమస్కారంsegments into 5 clusters, while shaping shows aస్కconjunct and renders nearer 4 advances. So switching from wcwidth to UAX #29 segmentation fixes most of the drift but will not be exact for conjunct-heavy text. Terminals also differ in how they advance conjuncts, so the honest target may be "no worse than the terminal, and never overlapping" rather than an exact column count.The cheapest reproduction that does not need a specific language is to render fixed-width padded rows and look at whether the right border stays straight — ASCII rows line up, Telugu rows drift by exactly the wcwidth-vs-cluster delta above.
Here is another concrete reproduction showing how this collapses the TUI specifically inside the diff / edit preview view, causing cursor desync, ghost text, and prompt collision.
Context & Symptom
When viewing/editing a file with Tamil strings:
கள்wraps onto an orphaned line (+கள் ",) beneath the diff.\x1b[NA) lands on the wrong row. Stale text from a previous buffer (y.test.ts)or-key.test.) is stamped directly across line 166.> █ 3 import { checkoutErrorKey } ...) renders directly over the diff output instead of staying docked below.Environment: macOS (Darwin arm64), Ghostty / default terminal.
Adding Arabic on Windows as another data point, with a byte-level finding that may help pin this down.
Environment: Claude Code 2.1.278, Windows 11 (build 26200), fullscreen renderer (the classic renderer shows the same fault), an xterm.js 6 based terminal over ConPTY. ConPTY passes the output through unchanged on this build, so the bytes below are Claude Code's own.
Minimal repro, no prompt needed:
تغيّرeleven times separated by spaces. Each one carries a shadda (U+0651).تغير) and the box draws correctly.What the captured output shows: when a row is full, the renderer writes the next row straight after it with no CR LF and relies on the terminal's auto wrap. The width used for that decision (and for
CSI n Ccursor moves between cells) counts each Arabic mark U+064B to U+065F as one cell. The layout does not: a paragraph line of 92 code points holding two marks is emitted as one full row in a 90 column terminal. So any terminal that gives the marks zero width (wcwidth or grapheme clusters, including Windows Terminal) sees such a row one cell short per mark, the next row starts on the end of the previous one, and every row after it shifts. In a narrow or split pane this happens constantly, because more lines wrap.Giving the marks one cell on the terminal side fixes the wrapped rows but breaks the full paragraph rows (one extra row each, the whole frame shifts), so no terminal setting can work around it. The two counts inside Claude Code need to agree, at zero width per mark (grapheme cluster width), which is also what the Tamil and Telugu reports above ask for.
Workaround in use for now: telling the model to write Arabic without tashkeel.