TUI: live status/spinner frames leak into scrollback (duplicated lines) — repaint rewinds past viewport top with no alternate screen
Summary
The animated status/spinner line (e.g. ✻ Reticulating… (3m 39s · ↑ 13k tokens)) is not erased on redraw. Previous frames are left behind in the scrollback, so the same spinner line appears stacked multiple times — an old timestamp frozen above the current one. Once a frame lands in scrollback it is permanent: resizing the window, refocusing, and continued redraws never clear it. Only restarting the session clears the accumulated duplicates.
Environment
- Claude Code: 2.1.181
- OS: Linux (Fedora, Wayland)
- Terminal: plain terminal emulator,
TERM=xterm-256color - Reproduces running
claudedirectly — not specific to any wrapper/IDE.
Steps to reproduce
- Use a terminal window that is not very tall (e.g. ~24–50 rows), or let a session run long.
- Start a task that streams a lot of live output (multiple tool calls + extended thinking) so the dynamic/live region grows taller than the viewport.
- Watch the status/spinner line: instead of updating in place, previous frames accumulate as duplicate lines in scrollback.
Expected: the status/spinner line updates in place; exactly one is visible.
Actual: multiple stale spinner frames remain in scrollback.
Root cause (from the raw PTY output)
The TUI repaints by rewinding with cursor-up + erase-line and never switches to the alternate screen. Escape-sequence census over one ~2 MB session's raw stdout:
alt-screen (CSI ?1049h) : 0
sync-output (CSI ?2026h) : 0
erase-display (CSI J) : 0
cursor-up (CSI nA) : 25298 (max single rewind: 53 rows)
erase-line (CSI 2K / K) : 2759
A single spinner repaint cycle from the raw stream (control chars shown literally) — note the leading ESC[53A:
ESC[53A <CR> ESC[9A ESC[38;5;231m ● ESC[3G ESC[39m ESC[K <CR> ESC[1B ESC[K <CR> ESC[1B ESC[38;5;174m · ESC[3G Orbiting…
ESC[53A rewinds the cursor 53 rows to redraw the managed region. When that region is taller than the viewport, cursor-up clamps at the top visible row — rows that have already scrolled into the scrollback cannot be addressed or erased. Each subsequent frame then appends new content at the bottom and pushes another un-erasable copy of the old content into history. With no alternate-screen buffer and no erase-display, there is no mechanism to reclaim those rows. This matches other reports of spinner/banner duplication on resize and in long sessions.
Why common mitigations don't help
- Terminal resize / refocus: cannot remove lines already committed to scrollback.
CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN/ disabling the status line: does not address rewinding past the top of the viewport.- Only restarting the session (fresh buffer) clears the accumulated duplicates.
Suggested directions
- Render the TUI in the alternate screen buffer, or
- Clamp the live/dynamic region to the viewport height (never emit cursor-up beyond the top visible row; render overflow via a scroll region), and/or
- Wrap each frame in synchronized output (
CSI ?2026h/l) and use erase-display for the managed region.
Happy to provide a longer raw PTY-log excerpt or additional captures on request.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗