From 302290d489b5d88c568c4f21bff1ab3d05368004 Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Sat, 7 Feb 2026 20:48:09 -0800 Subject: [PATCH] shelley: fix double encoding of UTF-8 in terminal output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit phil: This is one of those amazing things that Sketch ran into, and six months later, the agents are still writing this one not quite right. Had to do some handholding here. Prompt: Fix https://github.com/boldsoftware/shelley/issues/79 The terminal widget was using atob() to decode base64 data from the WebSocket, which returns a binary string where each byte becomes a separate character. When passed to xterm.js term.write(), each byte was interpreted as a Unicode code point, causing multi-byte UTF-8 sequences (like box-drawing characters ├──) to render as mojibake. Fix by decoding the base64 data into a Uint8Array and passing that to term.write(), which correctly handles raw bytes and decodes UTF-8 properly. Fixes https://github.com/boldsoftware/shelley/issues/79 Co-authored-by: Shelley --- ui/src/components/TerminalWidget.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/src/components/TerminalWidget.tsx b/ui/src/components/TerminalWidget.tsx index a5c843f874899b837df71189f009b846ae64caf6..223c258e99bfbd3ece325330b883701947217248 100644 --- a/ui/src/components/TerminalWidget.tsx +++ b/ui/src/components/TerminalWidget.tsx @@ -4,6 +4,17 @@ import { FitAddon } from "@xterm/addon-fit"; import { WebLinksAddon } from "@xterm/addon-web-links"; import "@xterm/xterm/css/xterm.css"; +function base64ToUint8Array(base64String: string): Uint8Array { + // This isn't yet available in Chrome, but Safari has it! + // @ts-expect-error Uint8Array.fromBase64 is a newer API + if (Uint8Array.fromBase64) { + // @ts-expect-error Uint8Array.fromBase64 is a newer API + return Uint8Array.fromBase64(base64String); + } + const binaryString = atob(base64String); + return Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); +} + interface TerminalWidgetProps { command: string; cwd: string; @@ -425,9 +436,7 @@ export default function TerminalWidget({ try { const msg = JSON.parse(event.data); if (msg.type === "output" && msg.data) { - // Decode base64 data - const decoded = atob(msg.data); - term.write(decoded); + term.write(base64ToUint8Array(msg.data)); // Track line count for auto-sizing lineCountRef.current = term.buffer.active.length; } else if (msg.type === "exit") {