claude-code - 💡(How to fix) Fix [BUG] Renderer paints stale frame past new viewport after SIGWINCH — cumulative \x1b[1B walks exceed new row count (relative-cursor path, alt-screen)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

In an alt-screen Claude session that has accumulated meaningful context (observed: ~1 h uptime, ~4 MB cumulative output), shrinking the terminal geometry causes the post-SIGWINCH render to paint a frame taller than the new viewport. The result: split screen, top half is stale content from the prior render at the old geometry, the prompt + recent output fall past the visible viewport (often hidden behind the on-screen keyboard in remote-terminal contexts). Recovery requires killing the Claude process — Ctrl+C inside the wedged session does not unstick the renderer, even though stdin still reaches the input parser.

Root Cause

In an alt-screen Claude session that has accumulated meaningful context (observed: ~1 h uptime, ~4 MB cumulative output), shrinking the terminal geometry causes the post-SIGWINCH render to paint a frame taller than the new viewport. The result: split screen, top half is stale content from the prior render at the old geometry, the prompt + recent output fall past the visible viewport (often hidden behind the on-screen keyboard in remote-terminal contexts). Recovery requires killing the Claude process — Ctrl+C inside the wedged session does not unstick the renderer, even though stdin still reaches the input parser.

Code Example

\x1b[2C  \x1b[1B  \x1b[7C  \x1b[1B  \x1b[3C  \x1b[1B  \x1b[2C  \x1b[1B
  ↑        ↑        ↑        ↑        ↑        ↑        ↑        ↑
CUF 2    CUD 1    CUF 7    CUD 1    CUF 3    CUD 1    CUF 2    CUD 1

---

// Absolute positioning (alt-screen path): row clamped to q (viewport rows)
let g = Math.min(Math.max(h.y + 1, 1), q);
let c = Math.min(Math.max(h.x + 1, 1), $);
G.push({ type: "stdout", content: sR(g, c) }); // CUP

// Relative positioning (between cells in a single render pass)
G.push({ type: "stdout", content: BZH(deltaX, deltaY) }); // CUF + CUD
RAW_BUFFERClick to expand / collapse

Version

  • Claude Code v2.1.143
  • Linux (Ubuntu 24.04, Bun-compiled native binary)
  • Opus 4.7 (1M context)
  • Reproduced over an SSH-pty-bridge transport (meshTerm), but the bug is geometry-only and should reproduce in any terminal that shrinks during a session.

Summary

In an alt-screen Claude session that has accumulated meaningful context (observed: ~1 h uptime, ~4 MB cumulative output), shrinking the terminal geometry causes the post-SIGWINCH render to paint a frame taller than the new viewport. The result: split screen, top half is stale content from the prior render at the old geometry, the prompt + recent output fall past the visible viewport (often hidden behind the on-screen keyboard in remote-terminal contexts). Recovery requires killing the Claude process — Ctrl+C inside the wedged session does not unstick the renderer, even though stdin still reaches the input parser.

Why this is a new report (not a duplicate)

Several adjacent open issues describe related symptoms but none captures this mechanism:

  • #46834 — TUI relayouts spill duplicate transcripts into scrollback. Different visual symptom; same renderer area.
  • #55762 — Feature request: force full TUI repaint after resize. Notes "Ink only redraws the current frame on SIGWINCH" — adjacent.
  • #53394 — Resize causes duplication; rewind causes freeze. Notes the "after a long session" correlation we also see.
  • #59913 — CLI hangs; UI swallows input. Explicitly states "SIGWINCH still works" during that hang — orthogonal to this report.

What's new here: the exact byte-stream pattern that causes the wedge, and a hypothesis pointing at a specific render/layout-pass ordering race rather than at the streaming path or the scrollback duplication.

Evidence (PTY byte stream from a wedged session)

Captured from the host-side PTY ring buffer immediately after a downward SIGWINCH (terminal shrunk from 40 → 23 rows while the keyboard was being presented in the remote client). Excerpt of the bytes emitted by Claude in the ~200 ms post-resize window:

\x1b[2C  \x1b[1B  \x1b[7C  \x1b[1B  \x1b[3C  \x1b[1B  \x1b[2C  \x1b[1B
  ↑        ↑        ↑        ↑        ↑        ↑        ↑        ↑
CUF 2    CUD 1    CUF 7    CUD 1    CUF 3    CUD 1    CUF 2    CUD 1

That is: only CUF (Cursor Forward) and CUD (Cursor Down) relative-movement sequences, plus interleaved SGR (\x1b[38;5;231m) color changes. Zero CUP / HVP absolute-position sequences in this window. The cumulative CUD count substantially exceeds the new row count (23) — the renderer is walking down 40 rows of frame content into a 23-row viewport.

Stack identification (from binary string-grep, since source isn't published)

The bundled binary contains:

  • reconcilerVersion:"19.2.0" — React 19.2.0 reconciler
  • __REACT_DEVTOOLS_GLOBAL_HOOK__ plumbing
  • calculateLayout, getComputedHeight, setMeasureFunc — Yoga flexbox layout

This is Ink (vadimdemedes/ink).

Hypothesis (where to look)

The cursor-emission path in the binary shows two distinct functions:

// Absolute positioning (alt-screen path): row clamped to q (viewport rows)
let g = Math.min(Math.max(h.y + 1, 1), q);
let c = Math.min(Math.max(h.x + 1, 1), $);
G.push({ type: "stdout", content: sR(g, c) }); // CUP

// Relative positioning (between cells in a single render pass)
G.push({ type: "stdout", content: BZH(deltaX, deltaY) }); // CUF + CUD

The absolute path is clamped to q (current viewport row count), so CUP sequences with row > q are not emitted — consistent with our observation. But the relative path walks deltaY between visually- distinct cells of the already-laid-out frame. If the frame was laid out at the old row count and the post-SIGWINCH render pass fires before Yoga's calculateLayout finishes re-measuring at the new row count, the relative walk accumulates deltaY for a 40-row frame emitted into a 23-row viewport.

The second render pass — after Yoga finishes — would correct it. But if the user has stopped typing and Claude has stopped streaming, no second render is scheduled. The wrongly-walked frame becomes the stuck visual.

Suggested fix area

The post-SIGWINCH render should await Yoga's layout-clean signal before emitting the next frame. Alternatively, the renderer could clamp the total walked deltaY to q-1 at emission time, or re-issue an absolute CUP at frame start to home the cursor before relative walks begin.

A stronger heuristic for the test suite: after a SIGWINCH that shrinks rows, the next frame's emitted byte stream must not contain cumulative CUD motions exceeding the new row count.

Correlations (informally observed)

The bug appears reliably on sessions older than ~30 min OR with cumulative output past ~few-MB. Short fresh sessions don't wedge. Suggests Yoga layout time or React reconcile depth grows with component count and tips past whatever timing budget the render pipeline assumes.

Reproduction (terminal-agnostic)

  1. Launch claude in a terminal sized at e.g. 80×40.
  2. Run a substantial conversation that accumulates context (multiple responses, tool calls, file reads). 30+ minutes is reliable.
  3. Shrink the terminal window to e.g. 80×23 (a keyboard appearing on a remote-terminal client; a window resize on desktop; stty rows 23 from another shell pointing at the same TTY).
  4. Observe: the prompt + recent output fall past the new visible bottom edge. Scrolling the terminal does not bring them back (the bytes were written below the viewport, not into scrollback).
  5. Recovery: kill the Claude process. claude --resume restores the session intact, confirming the in-memory conversation state isn't damaged — only the live render.

Adjacent telemetry

We've shipped a non-invasive detector on the host side (meshtermd v0.9.3+) that watches PTY output for this pattern and appends de-identified JSON records on detection — geometry numbers, byte counts, session age, no user data. Happy to share the schema or collect more samples if useful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] Renderer paints stale frame past new viewport after SIGWINCH — cumulative \x1b[1B walks exceed new row count (relative-cursor path, alt-screen)