Skip to content

Commit 66fdd59

Browse files
chat-gui: only resize the pty once a zoom gesture actually ends
Applies to every agent equally — this is the shared terminal-view zoom code, not agent-specific. Both the touch-pinch and trackpad-wheel zoom handlers sent a resize WS message on every intermediate frame. The server force-redraws (Ctrl+L) ~100ms after each resize lands, to fix partial-redraw TUIs that only re-emit cells they think changed after a SIGWINCH. During a continuous gesture this meant dozens of staggered Ctrl+L's in flight at once, most arriving after a LATER resize had already superseded them — landing the redraw into an already-stale geometry. Reported as jitter/garbling that stayed unwieldy until a full browser refresh forced one clean repaint. Fix: resize the backend only once the gesture has actually settled. Touch has a real end (touchend/touchcancel) to hook. Wheel doesn't — a continuous trackpad pinch is just a stream of individual wheel events with no "gesture" boundary — so that path infers "ended" via a 150ms debounce (no new wheel step) before sending the resize. Both paths keep doing the live client-side fontSize/fit/refresh on every frame for smooth visual feedback; only the pty-resize (and therefore the force-redraw it triggers) is deferred to gesture-end.
1 parent 2a3f880 commit 66fdd59

1 file changed

Lines changed: 29 additions & 3 deletions

File tree

packages/llmux/src/daemon/web/server.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3578,15 +3578,29 @@ ${sessionTopbar(name, agentLabel, 'terminal')}
35783578
if (clamped === pinchState.lastApplied) return;
35793579
pinchState.lastApplied = clamped;
35803580
term.options.fontSize = clamped;
3581+
// Client-side only — resizes the canvas/cell grid for live visual
3582+
// feedback as the gesture progresses. Deliberately does NOT send a
3583+
// resize to the backend here: the pty only needs to know the FINAL
3584+
// cols/rows once the gesture settles (_pinchEnd), not on every
3585+
// intermediate frame. Sending one per frame used to mean the
3586+
// server's force-redraw-100ms-after-resize (see the ws 'resize'
3587+
// handler) fired dozens of staggered times per gesture, most of
3588+
// them landing after a LATER resize had already superseded them —
3589+
// the redraw sent the inner TUI a Ctrl+L into a geometry that was
3590+
// already stale again by the time it arrived. Reported as "jitter
3591+
// that stays unwieldy until a full browser refresh."
35813592
try { fit.fit(); } catch(_){}
35823593
try { term.write('\x1b[3J'); } catch(_){}
35833594
try { term.refresh(0, term.rows - 1); } catch(_){}
3584-
safeSend(JSON.stringify({type:'resize', cols:term.cols, rows:term.rows}));
35853595
});
35863596
}, { passive: false });
35873597
function _pinchEnd(){
35883598
if (!pinchState) return;
35893599
try { localStorage.setItem(FONT_KEY, String(term.options.fontSize)); } catch(_){}
3600+
// One resize now that the gesture has settled — the server force-
3601+
// redraws (Ctrl+L) ~100ms after this lands, landing cleanly into the
3602+
// final geometry instead of a moving target.
3603+
safeSend(JSON.stringify({type:'resize', cols:term.cols, rows:term.rows}));
35903604
pinchState = null;
35913605
}
35923606
termEl.addEventListener('touchend', function(e){ if (e.touches.length < 2) _pinchEnd(); }, { passive: true });
@@ -3599,10 +3613,24 @@ ${sessionTopbar(name, agentLabel, 'terminal')}
35993613
// the browser from page-zooming on top of us.
36003614
let wheelStepPending = false;
36013615
let wheelDeltaAccum = 0;
3616+
let wheelEndTimer = null;
3617+
// Same reasoning as the touch pinch handler above: resizing the backend
3618+
// pty on every single wheel step (a continuous trackpad pinch fires many)
3619+
// used to queue a staggered force-redraw per step, most landing after a
3620+
// later step had already superseded them. Wheel has no touchend to hook,
3621+
// so "gesture ended" is inferred by debounce — no new wheel step for
3622+
// 150ms.
3623+
const WHEEL_END_DEBOUNCE_MS = 150;
36023624
termEl.addEventListener('wheel', function(e){
36033625
if (!e.ctrlKey) return;
36043626
e.preventDefault();
36053627
wheelDeltaAccum += e.deltaY;
3628+
if (wheelEndTimer) clearTimeout(wheelEndTimer);
3629+
wheelEndTimer = setTimeout(function(){
3630+
wheelEndTimer = null;
3631+
try { localStorage.setItem(FONT_KEY, String(term.options.fontSize)); } catch(_){}
3632+
safeSend(JSON.stringify({type:'resize', cols:term.cols, rows:term.rows}));
3633+
}, WHEEL_END_DEBOUNCE_MS);
36063634
if (wheelStepPending) return;
36073635
wheelStepPending = true;
36083636
requestAnimationFrame(function(){
@@ -3619,8 +3647,6 @@ ${sessionTopbar(name, agentLabel, 'terminal')}
36193647
try { fit.fit(); } catch(_){}
36203648
try { term.write('\x1b[3J'); } catch(_){}
36213649
try { term.refresh(0, term.rows - 1); } catch(_){}
3622-
safeSend(JSON.stringify({type:'resize', cols:term.cols, rows:term.rows}));
3623-
try { localStorage.setItem(FONT_KEY, String(target)); } catch(_){}
36243650
});
36253651
}, { passive: false });
36263652

0 commit comments

Comments
 (0)