TUI redraw corrupts on lines containing VS16 emoji (➡️): width counted as 1 column, terminals render 2

Status Open
Reported on v2.1.226
Maintainer reply None cached
Activity 1 comment · opened Aug 8, 2026

Summary

Claude Code measures emoji-presentation sequences (base codepoint + variation selector U+FE0F) as 1 column, but terminals render them as 2. When a line containing one is long enough to wrap, the terminal soft-wraps it into a screen row Claude Code doesn't know exists, and every subsequent cursor-up-and-repaint lands one row off. The result is progressive screen corruption: stale ghost rows, and single characters punched into the middle of words.

The clearest trigger is ➡️ (U+27A1 U+FE0F), which the official mattpocock-skills plugin prints on every recommendation line — see skills/productivity/grilling/SKILL.md:15, ➡️ <your recommended answer>. Any narrow-ish terminal turns that skill's output into garbage.

Root cause

U+27A1 is Extended_Pictographic but not Emoji_Presentation. On its own it is a 1-column text glyph; it only becomes a 2-column emoji when U+FE0F follows. Width code that inspects the base codepoint and treats U+FE0F as a zero-width combining mark returns 1. Code that measures the whole grapheme cluster returns 2 — which is what terminals do.

❓ U+2753 is not affected, because it is East_Asian_Width=Wide on its own, so every implementation agrees it is 2. Only the VS16-dependent ones break.

Reproduction (no API call required)

The composer exercises the same width math, so this needs no request:

  1. Size a terminal to a known width (I used a 53-column pane).
  2. Run claude.
  3. Type ➡️ followed by 60 As. Do not press Enter.
  4. As a control, on a new line type XX followed by 60 As.

Reading the terminal grid back:

len=52 | ❯ ➡️AAAA…AAAA     ← ❯ + space + ➡️ + 48 A
len=51 |   XXAAAA…AAAA     ← 2 spaces + XX + 47 A   (control)

Both lines use a 2-column prefix, so the control pins Claude Code's wrap target at 51 columns (2 + 2 + 47).

On the arrow line Claude Code fitted (1) + space(1) + ➡️(w) + 48 A into that same 51-column budget, so w = 1. Had it counted 2, it would have placed 47 As, not 48.

The terminal, meanwhile, draws that line as 52 columns. Claude Code believes it emitted 51.

Actual behaviour

Scale it to a real message. With a 183-column pane and assistant text wrapped to 183, a line containing one ➡️ is physically 184 columns, soft-wraps, and consumes an extra row. From that line down, the differential repaint is off by one row and the pane fills with artifacts:

  • ghost rows holding a sparse scatter of characters from an earlier frame
  • single characters overwriting spaces mid-word — Correct me. rendered as Correat me., plus a few as plusra few, these and as thesegand

Corruption begins exactly at the first ➡️ line and worsens with each subsequent one. Lines in the same message with no VS16 emoji render perfectly, including ones far longer that wrap twice.

Expected behaviour

Width should be computed per grapheme cluster, so U+27A1 U+FE0F measures 2 columns and matches what the terminal draws.

Notes

  • Terminal width is what decides whether this is visible. At 209 columns the same message never wraps those lines and renders cleanly; at 183 it corrupts. That makes it much more likely to hit anyone running Claude Code inside a multiplexer pane (tmux, herdr, zellij) than in a full-width window.
  • I verified the two other components in my stack independently — both count ➡️ as 2 columns and agree with each other. Claude Code is the outlier.

Environment

  • Claude Code 2.1.226
  • Ubuntu 24.04 on WSL2, Windows host
  • WezTerm, JetBrains Mono with Segoe UI Emoji fallback
  • Reproduced both in a full-width terminal and in a 183-column multiplexer pane

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗