Skip to content

fix: stop terminal tearing on Claude no-flicker repaints (v0.4.6) - #180

Merged
gbasin merged 5 commits into
masterfrom
fix/sync-frame-tearing
Jul 30, 2026
Merged

fix: stop terminal tearing on Claude no-flicker repaints (v0.4.6)#180
gbasin merged 5 commits into
masterfrom
fix/sync-frame-tearing

Conversation

@gbasin

@gbasin gbasin commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Fixes #158.

Root cause

Claude Code's no-flicker renderer wraps its fullscreen repaints in DEC 2026 synchronized-update markers (ESC[?2026h … ESC[?2026l). tmux honors them internally (MODE_SYNC) but strips them from client output unless the attached client advertises the sync terminal feature — which agentboard's TERM=xterm-256color pty client did not. Repaints then reach the browser as unmarked fragments (PTY reads cap at 2–4 KB, so a realistic multi-KB frame always splits), and xterm.js 5.5 — which has no DEC 2026 support — freely paints between fragments: transient garbage rows, worst at the left edge, self-healing once the stream idles. Copy-mode scrolling is the same mechanism via tmux's full-redraw path, which is why the reporter saw it both while scrolling and while streaming, and why /tui default or AGENTBOARD_CLAUDE_NO_FLICKER=0 made it disappear.

Every link was verified empirically: claude 2.1.220 emits the markers (captured via pipe-pane); tmux drops them for a plain client and emits them with the feature enabled (0 vs 18 wrapped frames in a controlled capture); 25 KB frames split across ~24 PTY reads in 18/18 samples.

Fix

  1. tmux -T sync at attach (local pty proxy + SSH proxy): per-client flag, no server-option mutation, so users' own tmux clients are untouched. Version-gated on tmux ≥ 3.2 (where -T exists — it aborts the attach on older versions); older tmux keeps today's behavior. The SSH proxy probes tmux -V over the existing command channel, so it works under any remote login shell. AGENTBOARD_TMUX_SYNC=0 opts out (documented in README).
  2. xterm.js 5.5 → 6.0: 6.0.0 implements DEC mode 2026 (Add synchronized output support (DEC mode 2026) xtermjs/xterm.js#5453) — the renderer holds painting between the markers and flushes each frame atomically, regardless of how the bytes were chunked across WebSocket messages. Addons bumped to the 6.0-era releases (search 0.16, webgl 0.19, progress 0.2). No agentboard code changes were required by the upgrade; none of the removed 6.0 APIs (windowsMode, fastScrollModifier, canvas addon, overviewRulerWidth) were in use.

Verification

  • Unit tests: attach args include -T sync on tmux ≥ 3.2 and omit it on 3.1c; tmuxSupportsClientFeatures covers letter suffixes, two-digit minors, and versionless dev builds.
  • Browser e2e against an isolated tmux server + built client: 58 ?2026h/l pairs observed arriving in the browser's WebSocket frames end-to-end; a 22-row color-cycling frame emitter rendered with a uniform frame number on every row (a torn paint shows interleaved frame numbers); wheel-scroll entered tmux copy-mode and agentboard's "Jump to bottom" flow worked under xterm 6.0's redesigned viewport; zero console/page errors, WebGL renderer active.
  • bun run lint && bun run typecheck && bun run test green (723 tests).

Notes for review

  • The xterm 6.0 viewport/scrollbar redesign is the main regression surface — desktop wheel-scroll is e2e-verified, but a quick manual pass on mobile touch scrolling before release would be prudent.
  • Pipe-pane mode passes the app's own markers through untouched, so it benefits from the xterm 6.0 upgrade alone; no tmux change needed there.

gbasin added 4 commits July 30, 2026 14:40
Claude Code's no-flicker renderer wraps its fullscreen repaints in
\x1b[?2026h/l synchronized-update markers. tmux strips them unless the
attached client advertises the sync terminal feature, so repaints reached
xterm.js as unmarked fragments (PTY reads cap at 2-4KB) and tore mid-frame
while streaming or scrolling (#158).

Attach with tmux -T sync (per-client, no server option mutation) from both
the local PTY proxy and the SSH proxy. Version-gate on tmux >= 3.2 where -T
exists; older tmux keeps today's behavior. AGENTBOARD_TMUX_SYNC=0 opts out.
xterm.js 6.0.0 implements DEC mode 2026 (xtermjs/xterm.js#5453): the
renderer holds painting between \x1b[?2026h/l, so tmux frames that arrive
split across PTY reads and WebSocket messages paint atomically. Together
with the -T sync attach flag this stops Claude no-flicker repaints from
tearing while streaming or scrolling (#158).

Also bumps addon-search 0.16, addon-webgl 0.19, addon-progress 0.2 to the
6.0-era releases. No API changes were needed; typecheck/lint/tests/build
all pass unchanged.
@gbasin gbasin changed the title fix: stop terminal tearing on Claude no-flicker repaints (tmux -T sync + xterm.js 6.0) fix: stop terminal tearing on Claude no-flicker repaints (v0.4.6) Jul 30, 2026
- SSH: fetch the remote tmux version via new-session -P -F '#{version}'
  in the existing create round-trip instead of a dedicated tmux -V probe;
  only the duplicate-session recovery path still probes. Removes the extra
  per-attach SSH handshake (ControlMaster=no makes each command a fresh
  connection).
- Pty: log terminal_sync_probe_failed when the tmux -V probe fails, matching
  the observability convention of the sibling doStart() tmux calls.
- Fail closed on an empty version string: pre-2.4 tmux expands #{version}
  to nothing and must not get -T sync (which would abort its attach).
- AGENTBOARD_TMUX_SYNC is now read per call (syncFeatureEnabled()) so the
  opt-out is testable.
- Tests: SSH attach gains explicit coverage (sync flag present for 3.4,
  omitted for 3.1c, duplicate-session fallback probe, probe-failure
  degradation, env opt-out) plus a Pty opt-out test.
@gbasin
gbasin merged commit cfc3267 into master Jul 30, 2026
6 checks passed
@gbasin
gbasin deleted the fix/sync-frame-tearing branch July 30, 2026 19:52
gbasin added a commit that referenced this pull request Jul 30, 2026
Fixes the black border around the terminal view introduced by the
xterm.js 6.0 upgrade in #180, reported on mobile Safari immediately
after v0.4.6.

## Root cause

Agentboard gives `.xterm` an 8px padding ring. Through xterm.js 5.5, the
library painted `.xterm-viewport` (which spans the full box, padding
included) with the theme background via an inline style on every theme
change, so the ring matched the terminal (`#2d2d2d`). xterm.js 6.0's
viewport rework applies the theme background to its new
scrollable-element node instead — leaving the viewport with
`xterm.css`'s default `background-color: #000`. Result: an 8px
pure-black frame around the terminal content, most visible on mobile
where the terminal fills the screen.

Verified by DOM measurement under iPhone emulation: `.xterm-viewport`
computed `rgb(0,0,0)` spanning 390×713 while `.xterm-screen` sat 8px
inset painting the theme color.

## Fix

- `styles/index.css`: `.xterm .xterm-viewport { background-color:
transparent !important }` — let the container show through.
- `Terminal.tsx`: the terminal container div now carries
`terminalTheme.background` (reactive to theme switches), so the padding
ring always matches the terminal content.

After the fix, the same measurement shows the viewport transparent over
a `rgb(45,45,45)` container, and the mobile screenshot renders
edge-to-edge theme background with no border.

## Notes

- `bun run lint && bun run typecheck && bun run test` green.
- Includes the version bump to 0.4.7 for release-on-merge.
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.

Scrolling corrupts text (0.3.3)

1 participant