mirror of https://github.com/garrytan/gstack.git
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.
This commit is contained in:
parent
b512be7117
commit
4bd6359576
|
|
@ -361,8 +361,26 @@ function buildServer() {
|
||||||
// Binary input. Lazy-spawn claude on the first byte.
|
// Binary input. Lazy-spawn claude on the first byte.
|
||||||
if (!session.spawned) {
|
if (!session.spawned) {
|
||||||
session.spawned = true;
|
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) => {
|
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) {
|
if (!proc) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
function ensureXterm() {
|
function ensureXterm() {
|
||||||
if (term) return;
|
if (term) return;
|
||||||
term = new Terminal({
|
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,
|
fontSize: 13,
|
||||||
theme: { background: '#0a0a0a', foreground: '#e5e5e5' },
|
theme: { background: '#0a0a0a', foreground: '#e5e5e5' },
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
|
|
@ -196,7 +196,25 @@
|
||||||
});
|
});
|
||||||
ro.observe(els.mount);
|
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) => {
|
term.onData((data) => {
|
||||||
|
if (composing) return; // suppress partial input events during IME composition
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(new TextEncoder().encode(data));
|
ws.send(new TextEncoder().encode(data));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
--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 */
|
||||||
--radius-sm: 4px;
|
--radius-sm: 4px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue