Two Korean-input bugs: the Enter that submitted mid-syllable, and the PTY boundary that ate 한글
A code audit surfaced two Korean-input defects that shipped for months: 16 input handlers submitted on Enter without checking IME composition (so confirming a Korean syllable sent the form), and the PTY read loop decoded fixed 4096-byte chunks with lossy UTF-8, silently turning a 한글 char split across the boundary into U+FFFD. Fixed with a shared IME-safe Enter helper and a carry-buffer decoder.
The founder asked to "get rid of the Korean issues." An audit found two, both months old, both invisible in tests because no test typed Korean.
1. The Enter that submitted your half-typed syllable
Korean goes through an IME composition: typing 안 fires keydowns with isComposing === true, and
the Enter that CONFIRMS the syllable is itself a keydown. Sixteen input handlers checked only
e.key === "Enter" and submitted — so confirming the last syllable sent the message early and
sometimes dropped a jamo. One handler (ZoneChat) already guarded with !e.nativeEvent.isComposing;
the other sixteen didn't.
The fix is a single shared helper, isEnterKey(e), that checks composition for both React synthetic
events (nativeEvent.isComposing) and raw DOM events (isComposing), applied everywhere. For
English there is no composition, so isComposing is always false and the guard is a strict no-op —
zero behavior change for non-IME input. Making it a shared helper also stops the next new handler
from reintroducing the bug.
2. The PTY boundary that ate 한글
Terminal output streams through a PTY read loop reading fixed 4096-byte chunks and decoding each with
String::from_utf8_lossy. A 한글 syllable is 3 bytes; when one straddles the 4096 boundary, the
trailing incomplete bytes become U+FFFD () and — because nothing carried them to the next read —
were lost forever. Result: intermittent mojibake on any Korean-heavy burst (git log, cat, Claude
streaming Korean).
The fix, drain_utf8, appends each read to a pending buffer, emits the longest valid UTF-8 prefix,
and carries the incomplete trailing char to the next read. A remnant ≥4 bytes can't be one
incomplete char (max 4B), so it's genuinely invalid and dropped — pending can never grow without
bound. A boundary-sweep test cuts "안녕하세요" at every byte offset and asserts no loss and no U+FFFD.
Lesson
Any submit handler taking IME input must check composition, not just the key; any byte-stream→string decode must carry multibyte chars across chunk boundaries. Both are silent for ASCII and only bite CJK — which is exactly why they survive an English test suite. Type Korean in the test.