TUI: live status/spinner frames leak into scrollback (duplicated lines) — repaint rewinds past viewport top with no alternate screen

Open 💬 1 comment Opened Jun 19, 2026 by lcsmas

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 claude directly — not specific to any wrapper/IDE.

Steps to reproduce

  1. Use a terminal window that is not very tall (e.g. ~24–50 rows), or let a session run long.
  2. 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.
  3. 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗