From 9fce0b6c1eb9db978ac0e14fecfb727674fb06f8 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Tue, 17 Mar 2026 13:00:29 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20UTF-8=20terminal=20rendering=20=E2=80=94?= =?UTF-8?q?=20atob()=20decodes=20as=20Latin-1,=20not=20UTF-8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit atob() returns a "binary string" where each byte is a Latin-1 char code. Multi-byte UTF-8 sequences (box-drawing, em dashes, arrows) were split into separate Latin-1 codepoints, producing mojibake. Now reconstructs the raw byte array and decodes via TextDecoder('utf-8') before writing to xterm.js. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/composables/useTerminal.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/composables/useTerminal.ts b/frontend/src/composables/useTerminal.ts index 2d3407b..2cc499e 100644 --- a/frontend/src/composables/useTerminal.ts +++ b/frontend/src/composables/useTerminal.ts @@ -114,8 +114,14 @@ export function useTerminal(sessionId: string): UseTerminalReturn { } try { - const decoded = atob(b64data); - terminal.write(decoded); + // atob() returns Latin-1 — each byte becomes a char code 0x00–0xFF. + // We must reconstruct the raw bytes, then decode as UTF-8. + const binaryStr = atob(b64data); + const bytes = new Uint8Array(binaryStr.length); + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + terminal.write(new TextDecoder().decode(bytes)); } catch { // Fallback: write raw if not valid base64 terminal.write(b64data);