Fix TerminalView: URL detection, event listener leak, resize throttle
- Fix broken URL accumulator by using TextDecoder instead of raw Uint8Array concatenation that produced numeric strings - Fix event listener memory leak by using aborted flag pattern to ensure cleanup runs even if listen() promises haven't resolved - Throttle ResizeObserver with requestAnimationFrame to prevent hammering the backend during window resize Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,7 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
// Fix: buffer recent output, strip ANSI codes, and after a short
|
// Fix: buffer recent output, strip ANSI codes, and after a short
|
||||||
// debounce check for a URL that spans multiple lines. When found,
|
// debounce check for a URL that spans multiple lines. When found,
|
||||||
// write a single clean clickable copy to the terminal.
|
// write a single clean clickable copy to the terminal.
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
let outputBuffer = "";
|
let outputBuffer = "";
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
@@ -117,14 +118,14 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle backend output -> terminal
|
// Handle backend output -> terminal
|
||||||
let unlistenOutput: (() => void) | null = null;
|
let aborted = false;
|
||||||
let unlistenExit: (() => void) | null = null;
|
|
||||||
|
|
||||||
onOutput(sessionId, (data) => {
|
const outputPromise = onOutput(sessionId, (data) => {
|
||||||
|
if (aborted) return;
|
||||||
term.write(data);
|
term.write(data);
|
||||||
|
|
||||||
// Accumulate for URL detection
|
// Accumulate for URL detection (data is a Uint8Array, so decode it)
|
||||||
outputBuffer += data;
|
outputBuffer += textDecoder.decode(data);
|
||||||
// Cap buffer size to avoid memory growth
|
// Cap buffer size to avoid memory growth
|
||||||
if (outputBuffer.length > 8192) {
|
if (outputBuffer.length > 8192) {
|
||||||
outputBuffer = outputBuffer.slice(-4096);
|
outputBuffer = outputBuffer.slice(-4096);
|
||||||
@@ -132,27 +133,37 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
if (debounceTimer) clearTimeout(debounceTimer);
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(flushUrlBuffer, 150);
|
debounceTimer = setTimeout(flushUrlBuffer, 150);
|
||||||
}).then((unlisten) => {
|
}).then((unlisten) => {
|
||||||
unlistenOutput = unlisten;
|
if (aborted) unlisten();
|
||||||
|
return unlisten;
|
||||||
});
|
});
|
||||||
|
|
||||||
onExit(sessionId, () => {
|
const exitPromise = onExit(sessionId, () => {
|
||||||
|
if (aborted) return;
|
||||||
term.write("\r\n\x1b[33m[Session ended]\x1b[0m\r\n");
|
term.write("\r\n\x1b[33m[Session ended]\x1b[0m\r\n");
|
||||||
}).then((unlisten) => {
|
}).then((unlisten) => {
|
||||||
unlistenExit = unlisten;
|
if (aborted) unlisten();
|
||||||
|
return unlisten;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle resize
|
// Handle resize (throttled via requestAnimationFrame to avoid excessive calls)
|
||||||
|
let resizeRafId: number | null = null;
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (resizeRafId !== null) return;
|
||||||
|
resizeRafId = requestAnimationFrame(() => {
|
||||||
|
resizeRafId = null;
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
resize(sessionId, term.cols, term.rows);
|
resize(sessionId, term.cols, term.rows);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
resizeObserver.observe(containerRef.current);
|
resizeObserver.observe(containerRef.current);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
aborted = true;
|
||||||
if (debounceTimer) clearTimeout(debounceTimer);
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
inputDisposable.dispose();
|
inputDisposable.dispose();
|
||||||
unlistenOutput?.();
|
outputPromise.then((fn) => fn?.());
|
||||||
unlistenExit?.();
|
exitPromise.then((fn) => fn?.());
|
||||||
|
if (resizeRafId !== null) cancelAnimationFrame(resizeRafId);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
term.dispose();
|
term.dispose();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user