From dc3b864a6709d452da11b6da8d5d352c71112bb9 Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Sun, 1 Mar 2026 18:59:17 +0530 Subject: [PATCH] fix(terminal): UTF-8 safe OSC 52 decode, cancel RAF loop on cleanup - Use TextDecoder to properly decode UTF-8 from base64 clipboard data instead of raw atob() which only handles Latin-1 - Track requestAnimationFrame IDs and cancel them on effect cleanup to prevent callbacks firing on disposed terminal/WebSocket Co-Authored-By: Claude Opus 4.6 --- .../web/src/components/DirectTerminal.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx index 3a1c9c130..a05325565 100644 --- a/packages/web/src/components/DirectTerminal.tsx +++ b/packages/web/src/components/DirectTerminal.tsx @@ -155,7 +155,12 @@ export function DirectTerminal({ if (parts.length < 2) return false; const b64 = parts[parts.length - 1]; try { - navigator.clipboard?.writeText(atob(b64)).catch(() => {}); + // Decode base64 → binary string → Uint8Array → UTF-8 text + // atob() alone only handles Latin-1; TextDecoder is needed for UTF-8 + const binary = atob(b64); + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); + const text = new TextDecoder().decode(bytes); + navigator.clipboard?.writeText(text).catch(() => {}); } catch { // Ignore decode errors } @@ -272,8 +277,11 @@ export function DirectTerminal({ let resizeAttempts = 0; const maxAttempts = 60; + let cancelled = false; + let rafId = 0; const resizeTerminal = () => { + if (cancelled) return; resizeAttempts++; // Get container dimensions @@ -287,7 +295,7 @@ export function DirectTerminal({ if (!isFullscreenTarget && resizeAttempts < maxAttempts) { // Container hasn't reached target size yet, try again - requestAnimationFrame(resizeTerminal); + rafId = requestAnimationFrame(resizeTerminal); return; } @@ -307,13 +315,16 @@ export function DirectTerminal({ }; // Start resize polling - requestAnimationFrame(resizeTerminal); + rafId = requestAnimationFrame(resizeTerminal); // Also try on transitionend const handleTransitionEnd = (e: TransitionEvent) => { + if (cancelled) return; if (e.target === container.parentElement) { resizeAttempts = 0; - setTimeout(() => requestAnimationFrame(resizeTerminal), 50); + setTimeout(() => { + if (!cancelled) rafId = requestAnimationFrame(resizeTerminal); + }, 50); } }; @@ -322,15 +333,19 @@ export function DirectTerminal({ // Backup timers in case RAF polling doesn't work const timer1 = setTimeout(() => { + if (cancelled) return; resizeAttempts = 0; resizeTerminal(); }, 300); const timer2 = setTimeout(() => { + if (cancelled) return; resizeAttempts = 0; resizeTerminal(); }, 600); return () => { + cancelled = true; + cancelAnimationFrame(rafId); parent?.removeEventListener("transitionend", handleTransitionEnd); clearTimeout(timer1); clearTimeout(timer2);