Skip to content

fix(ui): force full repaint at runtime instead of via bun patch - #114

Merged
hyldmo merged 1 commit into
mainfrom
hyldmo/render-drift-runtime
Jun 19, 2026
Merged

hyldmo merged 1 commit into
mainfrom
hyldmo/render-drift-runtime

Conversation

@hyldmo

@hyldmo hyldmo commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Follow-up to #113. #113 doesn't reach consumerspatchedDependencies is a root-project-only field, so the bun patch fixed only numux's own dev runs. A downstream repo on numux@2.17.2 still ran vanilla OpenTUI and saw the drift until its bundle was hand-edited. This moves the same fix into app code so it ships in numux's bundle and reaches every consumer.

What changed

  • Adds App.forceFullRepaints() — registers an OpenTUI frame callback that re-raises forceFullRepaintRequested every frame, so each render is a full repaint instead of the incremental cell-diff that desyncs from the host terminal's cursor (ambiguous-width glyphs / autowrap / emulator quirks → pane output smears into the sidebar, scrollbar drops).
  • Removes patchedDependencies + patches/@opentui%2Fcore@0.4.1.patch from fix(ui): force full repaint every frame to fix render drift #113.
renderer.setFrameCallback(async () => {
  r.forceFullRepaintRequested = true
})

Frame callbacks run before the native render reads the flag, and only on demand-driven frames.

Why this propagates and the patch didn't

bun patch (#113) runtime callback (this PR)
numux's own dev runs
bun run dev in a repo that installs numux ❌ vanilla OpenTUI ✅ ships in dist/bin.js
survives @opentui/core upgrade fails loud, needs re-patch works; log() warning if flag renamed

Performance (unchanged from #113's analysis)

Composition stays dirty-tracked; only the buffer→stdout emit grows. Idle = zero cost (demand-driven). FPS-capped at targetFps (30). Sustained-output worst case ≈ a few hundred KB/s up to ~1.5 MB/s for a large screen — same class as cat-ing a file; the extra parse load is on the emulator, not numux. Only bites on remote/SSH.

Caveats

  • Band-aid for an upstream bug (OpenTUI native emitter + host emulator); upstream report still pending.
  • Reaches a private OpenTUI field via cast, guarded with a log() warning so it fails safe if the flag ever disappears.

Testing

  • bun test — 699 pass, 0 fail. typecheck / lint clean.
  • Verified live: forcing the repaint eliminates the smear/scrollbar drop in a real downstream workload.

🤖 Generated with Claude Code

Replaces the bun patch from #113. patchedDependencies is a root-project
field — it does not propagate to projects that install numux as a
dependency, so the patch fixed only numux's own dev runs, not consumers
(a downstream repo on numux@2.17.2 still ran vanilla OpenTUI and saw the
drift). Move the same fix into app code so it ships in numux's bundle.

App.forceFullRepaints() registers a frame callback that re-raises the
renderer's forceFullRepaintRequested flag every frame, forcing a full
repaint instead of the incremental cell-diff that desyncs from the host
terminal's cursor (sidebar smear / scrollbar drop). Frame callbacks run
before the native render reads the flag and only on demand-driven frames
(no idle cost); composition stays dirty-tracked. Guarded with a log
warning if the flag ever disappears upstream. Removes patchedDependencies
and the patch file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hyldmo
hyldmo merged commit 07039fd into main Jun 19, 2026
2 checks passed
@hyldmo
hyldmo deleted the hyldmo/render-drift-runtime branch June 19, 2026 08:00
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.17.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

hyldmo added a commit that referenced this pull request Jun 19, 2026
…round

#114 forced a full repaint every frame to fix the render-drift smear
(right-pane output bleeding into the tab sidebar). That carries a per-frame
stdout cost, and disabling autowrap unconditionally risks off-screen
clipping for some output — so neither belongs on by default.

Replace it with an opt-in `autowrap: false` config: numux disables the
host terminal's autowrap (DECAWM, ?7l) on startup and restores it on exit.
Pane content still wraps inside its own VT grid; only the host emit-cursor
wrap (which OpenTUI never relies on) is turned off. Default leaves autowrap
enabled — today's behavior, no smear fix imposed.

The proper fix is upstream (anomalyco/opentui#1187): OpenTUI positions every
run absolutely and can disable autowrap itself, like vim/tmux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hyldmo added a commit that referenced this pull request Jun 19, 2026
…round (#115)

#114 forced a full repaint every frame to fix the render-drift smear
(right-pane output bleeding into the tab sidebar). That carries a per-frame
stdout cost, and disabling autowrap unconditionally risks off-screen
clipping for some output — so neither belongs on by default.

Replace it with an opt-in `autowrap: false` config: numux disables the
host terminal's autowrap (DECAWM, ?7l) on startup and restores it on exit.
Pane content still wraps inside its own VT grid; only the host emit-cursor
wrap (which OpenTUI never relies on) is turned off. Default leaves autowrap
enabled — today's behavior, no smear fix imposed.

The proper fix is upstream (anomalyco/opentui#1187): OpenTUI positions every
run absolutely and can disable autowrap itself, like vim/tmux.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant