The previous 'stabilize useMutation results' fix addressed one cause
of /shell/resize being spammed (the mutation object identity), but
not the actual root cause: a closed loop between xterm's onResize
callback, ResizeObserver, and the backend.
The full loop:
1. ResizeObserver fires
2. fitAddon.fit() runs
3. fit() calls terminal.resize(cols, rows)
4. terminal.onResize(cb) fires the listener
5. cb calls resize.mutate() — POSTs to /shell/resize
6. server calls pty.Setsize
7. bash receives SIGWINCH and redraws the prompt
8. the new output writes to the terminal, which can shift a
scrollbar / padding by a few pixels
9. ResizeObserver fires again
-> repeat
Fixes applied to TerminalPanel's terminal-init useEffect:
- Drop the terminal.onResize subscription entirely. Per the xterm
+ FitAddon pattern, the right way to push size to the backend
is: from the ResizeObserver path, call fit(), then read
terminal.cols/rows synchronously and call resize.mutate().
- Dedupe by (cols, rows) — if the size hasn't changed since the
last reported size, skip the mutation.
- Dedupe the ResizeObserver callback by container
getBoundingClientRect — sub-pixel changes (e.g. a scrollbar
appearing) won't trigger a redundant fit().
- Depend on the stable sendResize function (the destructured
mutate from useMutation) rather than the whole resize object,
so unrelated render churn can't tear down and re-create the
terminal.
- Run an initial fit() at mount so the backend learns the real
size before the user starts typing.
The 100ms debounce was a band-aid that didn't actually break the
loop — it only slowed it down.