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 <noreply@anthropic.com>
This commit is contained in:
suraj-markup 2026-03-01 18:59:17 +05:30
parent 0814f9515c
commit dc3b864a67
1 changed files with 19 additions and 4 deletions

View File

@ -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);