diff --git a/src/components/XtermPane.tsx b/src/components/XtermPane.tsx index 3a6594b..242fa27 100644 --- a/src/components/XtermPane.tsx +++ b/src/components/XtermPane.tsx @@ -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); }