From 4bd63595766fb394685e5790f8a40f878575ce73 Mon Sep 17 00:00:00 2001 From: Terry Carson YM Date: Sat, 2 May 2026 14:56:59 +0800 Subject: [PATCH] fix: Korean/CJK IME input and rendering in Sidebar Terminal Fixes #1272 This commit addresses three separate Korean/CJK bugs in the Sidebar Terminal: **Bug 1 - IME Input**: Korean text typed via IME composition was not reaching the PTY correctly. Added compositionstart/compositionend event listeners to suppress partial jamo fragments and only send the final composed string. **Bug 2a - Font Rendering**: Added CJK monospace font fallbacks ("Noto Sans Mono CJK KR", "Malgun Gothic") to both the xterm.js fontFamily config and the CSS --font-mono variable. This ensures consistent cell-width calculations for Korean characters. **Bug 2b - UTF-8 Boundary Detection**: Added buffering logic to prevent multi-byte UTF-8 characters (Korean is 3 bytes) from being split across WebSocket chunks. This follows the same pattern as PR #1007 which fixed the sidebar-agent path, but extends it to the terminal-agent path. Special thanks to @ldybob for the excellent root cause analysis and proposed solutions in issue #1272. Tested on WSL2 + Windows 11 with Korean IME. --- browse/src/terminal-agent.ts | 20 +++++++++++++++++++- extension/sidepanel-terminal.js | 20 +++++++++++++++++++- extension/sidepanel.css | 2 +- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/browse/src/terminal-agent.ts b/browse/src/terminal-agent.ts index 9ebc8cbbf2..dd422caba3 100644 --- a/browse/src/terminal-agent.ts +++ b/browse/src/terminal-agent.ts @@ -361,8 +361,26 @@ function buildServer() { // Binary input. Lazy-spawn claude on the first byte. if (!session.spawned) { session.spawned = true; + // UTF-8 boundary detection to prevent splitting multi-byte characters (issue #1272). + // Buffer incomplete UTF-8 sequences until the next chunk completes them. + let leftover = Buffer.alloc(0); const proc = spawnClaude(session.cols, session.rows, (chunk) => { - try { ws.sendBinary(chunk); } catch {} + const combined = Buffer.concat([leftover, Buffer.from(chunk)]); + // Find the last index where a UTF-8 codepoint ends. Look back at most 3 bytes. + let safeEnd = combined.length; + for (let i = combined.length - 1; i >= Math.max(0, combined.length - 3); i--) { + const b = combined[i]; + if ((b & 0x80) === 0) { safeEnd = i + 1; break; } // ASCII + if ((b & 0xC0) === 0x80) continue; // continuation byte + const expected = (b & 0xE0) === 0xC0 ? 2 : (b & 0xF0) === 0xE0 ? 3 : 4; + safeEnd = (combined.length - i >= expected) ? combined.length : i; + break; + } + const flush = combined.slice(0, safeEnd); + leftover = combined.slice(safeEnd); + if (flush.length) { + try { ws.sendBinary(flush); } catch {} + } }); if (!proc) { try { diff --git a/extension/sidepanel-terminal.js b/extension/sidepanel-terminal.js index e301d085cc..dc3a0cd75b 100644 --- a/extension/sidepanel-terminal.js +++ b/extension/sidepanel-terminal.js @@ -154,7 +154,7 @@ function ensureXterm() { if (term) return; term = new Terminal({ - fontFamily: '"JetBrains Mono", "SF Mono", Menlo, monospace', + fontFamily: '"JetBrains Mono", "SF Mono", Menlo, "Noto Sans Mono CJK KR", "Malgun Gothic", monospace', fontSize: 13, theme: { background: '#0a0a0a', foreground: '#e5e5e5' }, cursorBlink: true, @@ -196,7 +196,25 @@ }); ro.observe(els.mount); + // IME composition handling for Korean/CJK input (issue #1272). + // Suppress partial jamo during composition; only send the final + // composed string on compositionend. Without this, Korean IME + // sends fragmented input or doubles characters. + let composing = false; + const ta = term.textarea; + if (ta) { + ta.addEventListener('compositionstart', () => { composing = true; }); + ta.addEventListener('compositionend', (e) => { + composing = false; + if (e.data && ws && ws.readyState === WebSocket.OPEN) { + ws.send(new TextEncoder().encode(e.data)); + } + }); + } + + term.onData((data) => { + if (composing) return; // suppress partial input events during IME composition if (ws && ws.readyState === WebSocket.OPEN) { ws.send(new TextEncoder().encode(data)); } diff --git a/extension/sidepanel.css b/extension/sidepanel.css index 8813a0d07e..d83486e6c2 100644 --- a/extension/sidepanel.css +++ b/extension/sidepanel.css @@ -38,7 +38,7 @@ /* Typography */ --font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', 'Noto Sans Mono CJK KR', 'Malgun Gothic', monospace; /* Radius */ --radius-sm: 4px;