perf(terminal): coalesce PTY output into one xterm.write per animation frame (v2 transport)
Problem
Addresses #2241 / #2244 for the v2 WebSocket terminal path.
Agent CLIs — Claude Code especially — emit full-screen repaints as many small PTY chunks. The v2 transport currently calls terminal.write() once per WebSocket frame with no batching anywhere in the pipeline (unlike the v1 IPC path, which at least has 16ms batching in the main process via DataBatcher). Each chunk triggers an xterm parse/render cycle, so during streaming output the renderer falls behind and scrolling turns visibly janky.
Real-world report: running Claude Code inside the Superset 2 beta, scrolling is laggy while the same session in a native GPU terminal (Warp) is smooth. Markdown/code views in Superset scroll fine — only the embedded terminal struggles, and only under streaming TUI output.
Fix
Adds a small write-coalescer module and wires it into terminal-ws-transport:
- Incoming binary PTY frames are buffered and written to xterm as one
write()perrequestAnimationFrame, aligning parse/render cost with the display refresh rate regardless of chunk count — the fix suggested in #2241, applied to the v2 path. - Ordering is preserved: control messages that write to the terminal (exit notice, invalid-payload line) and socket close flush pending bytes synchronously first.
- Memory is bounded: a 1MB pending cap flushes synchronously if rAF stalls (e.g. hidden window, where Electron throttles the renderer).
- Coalescer is recreated per
connect()and disposed (with a final flush) ondisconnect/disposeTransport.
Tests
- 7 unit tests for the coalescer (same-frame coalescing, re-scheduling, sync flush, byte cap, dispose semantics, byte-order preservation).
- 3 transport-level tests (binary frames coalesce into one write; exit notice ordering; flush on socket close).
bun test src/renderer/lib/terminal/: 234 pass / 1 fail — the failure ("recovers a half-open socket after the machine resumes from sleep") also fails on an untouched checkout ofmainin my environment, so it appears unrelated.- Biome clean.
Notes
- v1 (
useTerminalStream) renderer-side coalescing is a natural follow-up using the same module; left out to keep this reviewable. - The other half of the lag story for multi-tab users appears to be the module-level
suggestedRendererTypeWebGL fallback (one context loss permanently downgrades all terminals to the DOM renderer until restart) — happy to look at that separately if useful.
Summary by cubic
Coalesces PTY output in the v2 WebSocket terminal path into one xterm.write() per animation frame to reduce parse/render churn and smooth scrolling. Addresses #2241 / #2244 by aligning terminal work with display refresh.
- New Features
- Added a
write-coalescerthat batches incoming binary PTY frames and writes once perrequestAnimationFrame. - Preserves output order by flushing before control messages that write to the terminal (exit notice, invalid-payload line) and on socket close; does not flush for non-writing controls.
- Bounded memory with a 1 MB cap; cancels any scheduled frame on sync flush; coalescer is recreated on
connect()and disposed ondisconnect/disposeTransport.
- Added a
Summary by CodeRabbit
-
Bug Fixes
- Improved terminal PTY output handling by batching writes per animation frame, preserving output ordering during control messages and ensuring queued output flushes correctly on disconnect/close.
-
New Features
- Added a write coalescing utility to efficiently batch terminal writes and cap queued data to avoid excessive buffering.
-
Tests
- Added deterministic test suites covering write coalescing behavior and PTY output coalescing/flush ordering in the WebSocket transport.