Skip to content

fix(tui): throttle re-renders, animate spinner, debounce resize, defensive emit - #80

Merged
Exelord merged 1 commit into
mainfrom
claude/tui-perf-fixes
May 13, 2026
Merged

fix(tui): throttle re-renders, animate spinner, debounce resize, defensive emit#80
Exelord merged 1 commit into
mainfrom
claude/tui-perf-fixes

Conversation

@Exelord

@Exelord Exelord commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Re-opening the perf/stability fix that didn't make it into the
merged PR #79. Same commit, addresses your "laggy and crashy and
freezing" report.

Four real bugs in the freshly-merged Solid TUI:

  1. Reactivity firehose. pty-store called setRev() on every
    chunk. A chatty build tool (1k+ chunks/sec) triggered 1k+ Solid
    updates/sec, each re-running LogPane's memo which walked the
    entire xterm buffer. That's the lag.

    Fix: pty-store batches dirty = true on writes; a 30 Hz
    interval timer bumps the rev signal at most once per frame.
    pty-output.readLines() caches its result keyed by
    bytesWritten so an idle paint is O(1).

  2. Spinner didn't animate. The createMemo over Date.now()
    only invalidates when its dependencies change; Date.now()
    isn't reactive. The spinner was frozen at whatever frame
    happened to render last.

    Fix: new ClockProvider context owns a single 10 Hz
    setInterval that updates a tick signal. TaskList's
    spinner memo reads tick() so it animates on every interval.

  3. Resize cascaded. Every dim().width or height change
    resized every task's pty synchronously. OpenTUI can fire several
    resize events in a burst — freezing potential.

    Fix: 150 ms debounce in App.tsx — only resize ptys once
    after the resize storm settles.

  4. Crashes propagated. A bug inside the runState reducer or
    pty handler would throw out of observer.emit, crashing the
    orchestrator.

    Fix: wrap each apply() call in observer.emit with
    try/catch; write errors to stderr but keep going. Same defense
    around the keyboard handler.

Plus a side bug: xterm-headless's window/self shim ran AFTER
the static import 'xterm-headless' due to Bun ESM hoisting
node_modules imports first. Replaced with a side-effect import of
xterm-shim.ts + a require() for the actual Terminal constructor
inside createPtyOutput.

Test plan

  • bun src/bin.ts run ci — 3/3 pass (format-check + lint + 436 tests)
  • Smoke test exercises the corrected path

If it's STILL bad after this lands, the painter problems are
OpenTUI's renderer itself, not our usage. The fallback path is the
hand-rolled cell-buffer + xterm-headless approach (Option A from
docs/design/tui-rebuild.md).

https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9


Generated by Claude Code

…nsive emit

User reported the Solid TUI was "laggy and crashy and freezing". Four
real bugs:

1. **Reactivity firehose.** pty-store called setRev() on every chunk.
   A chatty build tool (1k+ chunks/sec) triggered 1k+ Solid updates/sec,
   each one re-running LogPane's memo which walked the entire xterm
   buffer. That's the lag.

   Fix: pty-store batches dirty=true on writes; a 30 Hz interval timer
   bumps the rev signal at most once per frame. pty-output caches its
   readLines() result keyed by bytesWritten so a paint without new
   bytes is O(1).

2. **Spinner didn't animate.** createMemo over `Date.now()` only
   invalidates when its dependencies change; Date.now() is not
   reactive. The spinner was frozen at whatever frame happened to
   render last.

   Fix: new ClockProvider context owns a single 10 Hz setInterval that
   updates a tick signal. TaskList's spinner memo reads tick() so it
   animates on every interval.

3. **Resize cascaded.** Every dim().width or height change resized
   every task's pty synchronously. opentui can fire several resize
   events in a burst; freezing potential.

   Fix: 150ms debounce in App.tsx — only resize ptys once after the
   resize storm settles.

4. **Crashes propagated.** A bug inside the runState reducer or pty
   handler would throw out of observer.emit, crashing the orchestrator.

   Fix: wrap each apply() call in observer.emit with try/catch;
   write errors to stderr but keep going. Same defense around the
   keyboard handler.

Also fixed an xterm-headless module-load issue: the `window`/`self`
shim ran AFTER the static `import 'xterm-headless'` due to Bun's ESM
hoisting node_modules imports first. Replaced with a side-effect
import of `xterm-shim.ts` + a `require()` for the actual Terminal
constructor inside `createPtyOutput`.

Tests: 435 → 436 (smoke test now exercises the corrected path). All
436 pass.
@Exelord
Exelord merged commit f53d199 into main May 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants