Legacy Windows console (conhost): SI byte from reassertTerminalModes renders as a visible glyph in the input box; ambiguous-width glyphs corrupt partial repaints
Environment: Claude Code 2.1.237 (native install), Windows 10 Pro 19045, cmd.exe in the legacy console host (conhost, not Windows Terminal), codepage 65001, HKCU\Console\VirtualTerminalLevel=1, Korean locale, CJK console font.
Bug 1: SI (0x0F) printed as a glyph at the cursor
reassertTerminalModes() writes the charset reset "\x1B(B\x0F" (ESC(B + SI) whenever the event-loop stall monitor detects a >5s stall ("likely sleep/wake") and on stdin resume.
The Win10 inbox conhost VT parser consumes ESC(B but does not implement SI/SO (LS0/LS1). The 0x0F byte falls through and is stored as a visible cell (rendered with the legacy control-glyph, a sun/bug-like character) at the current cursor position — which, when idle, is inside the prompt input box. Each sleep/wake or throttling stall adds one more glyph; they disappear on the next input-row repaint, so users see mysterious "bug emoji" accumulating while the app is idle.
Verified empirically: writing A \x1B(B \x0F B to a VT-enabled Win10 conhost yields buffer cells A, U+000F, B and a 3-column cursor advance. Writing A \x00 B yields A, B (NUL is discarded cleanly).
Suggested fix: skip the SI (or the whole charset reset) when the terminal is the legacy Windows console (no WT_SESSION, Windows, VT enabled via console API), or replace SI with nothing on win32. DECSC/DECRC + DECSTBM in the restore path were verified fine on conhost; only SI/SO leak.
Bug 2: East-Asian-ambiguous glyph widths desync cell-diff repaints
The renderer computes string widths with Bun.stringWidth(s, {ambiguousIsNarrow: true}), but legacy conhost measures each glyph against the actual console font. With a CJK console font (GulimChe and every other Korean/Japanese font), many UI symbols occupy 2 cells: · ← ↓ … ✔ ● ◆ ◇ ○ ✳ ⚠ ※, and U+FE0E (used in ·✔︎·) occupies 2 extra cells itself. Box-drawing chars stay 1 cell, so no single ambiguousIsNarrow setting can match.
Result: absolute-column partial repaints (cell diff) land N columns away from where the previous frame's text actually sits, producing mixed old/new text in status/hint lines — e.g. (shift+tab to cycle) displayed as (shift+eab to cycle), dropped or seemingly reordered characters.
Additionally U+23F5 (the ⏵⏵ auto/plan-mode indicator) appears to be counted emoji-wide (2) by Bun.stringWidth while conhost renders it 1 cell, desyncing that line even with a non-CJK font, and it has no glyph in most monospace fonts (tofu on legacy conhost, which does no font fallback for symbols).
Suggested fix: on legacy conhost, prefer full-line repaints over intra-line cell diffs (or verify real cursor advance), and/or use conservative ASCII/narrow symbols (▸ instead of ⏵, plain ✔ without VS15) when the legacy console is detected.
Workarounds found
- Bug 1: byte-patch
\x1B(B\x0F→\x1B(B\x00in the binary (NUL verified harmless on conhost). - Bug 2: switch the console font to D2Coding (narrow symbol glyphs + native Hangul) — removes almost all width mismatches on ko-KR.
---
Root-caused and verified with byte-level probes by Claude Code running on the affected setup.