Debounce PTY resize during drag to stop SIGWINCH-spam corrupting prompts

What the user saw: dragging a gutter filled the affected panes with
many overlapping bash prompts, some corrupted mid-print
(megaproxy@DESKTOP-megaproxy@DESKTOP-SSAQG5 etc).

Root cause: every resizePane() call sends SIGWINCH to the shell, which
makes bash redraw its prompt. The previous fix coalesced the local
xterm fit() into one per rAF, but still fired resizePane on every
rAF — 60+ SIGWINCHes per second during a drag, faster than bash can
finish one prompt redraw before the next interrupts it.

Fix: separate the two concerns. fit() + term.refresh() still run
every rAF (the visual must stay smooth). But resizePane() is
debounced to fire 150 ms after the LAST rAF — i.e. only when you
stop dragging — so bash gets one clean SIGWINCH at the final size
and produces a single tidy prompt redraw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-22 21:53:34 +01:00
parent 7d1f1f4b9a
commit a5209e08ae

View file

@ -165,12 +165,16 @@ export default function XtermPane({
if (ta) ta.addEventListener("focus", () => onFocusRef.current?.(), true);
}
// Re-fit on container resize; forward new size to the PTY. We
// coalesce multiple ResizeObserver firings into one rAF so a single
// gutter drag tick = one fit/resize/repaint, then explicitly call
// term.refresh() so the DOM renderer fully repaints (without this,
// shrinking a pane sometimes leaves stale glyphs in the freed rows).
// Re-fit on container resize. xterm.fit() + a forced refresh run
// immediately (visual must stay smooth during a drag), but the
// actual PTY resize call is debounced: every SIGWINCH makes bash
// redraw the prompt, and if we send 60+ of them per second during a
// gutter drag, the redraws corrupt each other and the terminal
// fills with garbled half-prompts. The debounce means the PTY
// hears about resizes ~150 ms after you stop dragging, at the
// final size — bash gets a single clean redraw.
let resizeRaf: number | null = null;
let resizePtyTimer: number | null = null;
ro = new ResizeObserver(() => {
if (resizeRaf != null) return;
resizeRaf = requestAnimationFrame(() => {
@ -178,10 +182,14 @@ export default function XtermPane({
if (!term) return;
try {
fit.fit();
if (paneId != null) {
void resizePane(paneId, term.cols, term.rows);
}
term.refresh(0, term.rows - 1);
if (resizePtyTimer != null) clearTimeout(resizePtyTimer);
resizePtyTimer = window.setTimeout(() => {
resizePtyTimer = null;
if (paneId != null && term) {
void resizePane(paneId, term.cols, term.rows);
}
}, 150);
} catch (e) {
console.warn("resize failed", e);
}