--resume session preview renders corrupted when the preview is taller than the terminal

Status Open
Reported on v2.1.259
Maintainer reply None cached
Activity 0 comments · opened Sep 3, 2026

What happens

In the claude --resume picker, Space opens a preview of the highlighted session. When the rendered preview is taller than the terminal viewport, it is drawn with fragments of unrelated transcript lines fused onto the same row, and with stale body text left beside the footer.

A 15 message synthetic session at 100x30:

 24|⏺ [13] The quick brown fox jumps over the lazy dog while there
 25|  maintainer reviews a very long diagnostic line that nobody
 26|  bothered to wrap at any sensible width, and it keeps goingle
 27|⏺ [15] The quick brown fox jumps over the lazy dog while ther
 28|────────────────────────────────────────────────────────────────
 29|  16s ago · 16 messages · mainible width, and it keeps goingre
 30|  Enter to resume · Esc to cancel would use. The quick brown

Rows 24, 26 and 27 end in fragments of other lines (there, goingle, ther), and both footer rows carry leftover body text.

The same session, previewed in a viewport tall enough to hold the whole thing, renders correctly:

217|────────────────────────────────────────────────────────────────
218|  16s ago · 16 messages · main
219|  Enter to resume · Esc to cancel

Any session long enough to be worth previewing overflows a normal terminal, so in practice the preview is unreadable for the sessions people want it for.

Reproduction

Interactively: run claude --resume, highlight a session whose preview is longer than your terminal, press Space.

Deterministically, with the script below:

python3 repro.py 100 30     # preview overflows the viewport, corrupted
python3 repro.py 100 250    # same session fits, clean

It creates a throwaway project holding one synthetic session, drives the picker on a pty, presses Space, prints the resulting screen through pyte, and removes both directories afterwards.

<details>
<summary>repro.py</summary>

#!/usr/bin/env python3
"""Self-contained repro: `claude --resume` session preview corrupts when it
overflows the terminal viewport.

  python3 repro-for-issue.py 100 30    # preview overflows -> corrupted
  python3 repro-for-issue.py 100 250   # same session fits  -> clean

Creates a throwaway project with one synthetic session, drives the picker on a
pty, presses Space, and prints the resulting screen (via pyte if installed).
Cleans up both directories on exit.
"""
import datetime, fcntl, json, os, pty, select, shutil, signal, struct, sys
import tempfile, termios, textwrap, time, uuid

COLS = int(sys.argv[1]) if len(sys.argv) > 1 else 100
ROWS = int(sys.argv[2]) if len(sys.argv) > 2 else 30

BODY = ("The quick brown fox jumps over the lazy dog while the maintainer reviews "
        "a very long diagnostic line that nobody bothered to wrap at any sensible "
        "width, and it keeps going well past any terminal a person would use. ") * 3

cwd = os.path.realpath(tempfile.mkdtemp(prefix="claude-preview-repro-"))
proj = os.path.expanduser("~/.claude/projects/" + cwd.replace("/", "-"))
os.makedirs(proj, exist_ok=True)

sid = str(uuid.uuid4())
now = datetime.datetime.now(datetime.timezone.utc)
stamp = lambda i: (now + datetime.timedelta(seconds=i)).isoformat().replace("+00:00", "Z")
base = dict(isSidechain=False, userType="external", entrypoint="cli", cwd=cwd,
            sessionId=sid, version="2.1.259", gitBranch="main")
recs, parent = [], str(uuid.uuid4())
recs.append({**base, "type": "user", "parentUuid": None, "uuid": parent, "timestamp": stamp(0),
             "promptId": str(uuid.uuid4()), "permissionMode": "default",
             "origin": {"kind": "human"}, "promptSource": "typed",
             "message": {"role": "user", "content": "preview repro"}})
for i in range(1, 16):
    a = str(uuid.uuid4())
    recs.append({**base, "type": "assistant", "parentUuid": parent, "uuid": a, "timestamp": stamp(i),
                 "message": {"model": "claude-opus-5", "id": f"msg_{a[:20]}", "type": "message",
                             "role": "assistant",
                             "content": [{"type": "text",
                                          "text": "\n".join(textwrap.wrap(f"[{i:02d}] " + BODY, 60))}],
                             "stop_reason": "end_turn",
                             "usage": {"input_tokens": 1, "output_tokens": 1}}})
    parent = a
with open(os.path.join(proj, f"{sid}.jsonl"), "w") as fh:
    for r in recs:
        fh.write(json.dumps(r) + "\n")

pid, fd = pty.fork()
if pid == 0:
    os.chdir(cwd)
    env = dict(os.environ)
    env["TERM"] = "xterm-256color"
    for k in ("TERM_PROGRAM", "TERM_PROGRAM_VERSION", "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"):
        env.pop(k, None)
    env["COLUMNS"], env["LINES"] = str(COLS), str(ROWS)
    os.execvpe("claude", ["claude", "--resume"], env)

fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0))
buf = bytearray()

def pump(seconds):
    end = time.time() + seconds
    while time.time() < end:
        r, _, _ = select.select([fd], [], [], 0.2)
        if r:
            try:
                d = os.read(fd, 65536)
            except OSError:
                return
            if not d:
                return
            buf.extend(d)

pump(8)
if b"trust" in bytes(buf):                 # first run in a new directory
    os.write(fd, b"\x1b[B")               # select "Yes, I trust this folder"
    pump(2)
    os.write(fd, b"\r")
    pump(6)

os.write(fd, b" ")     # Space opens the preview
pump(8)
os.kill(pid, signal.SIGKILL)
os.waitpid(pid, 0)
shutil.rmtree(proj, ignore_errors=True)
shutil.rmtree(cwd, ignore_errors=True)

try:
    import pyte
except ImportError:
    open("preview.bin", "wb").write(bytes(buf))
    sys.exit("pyte not installed; raw stream written to preview.bin "
             "(pip install pyte to see the rendered screen)")

screen = pyte.Screen(COLS, ROWS)
pyte.ByteStream(screen).feed(bytes(buf))
print(f"--- {COLS}x{ROWS} ---")
for i, line in enumerate(screen.display, 1):
    if line.strip():
        print(f"{i:3d}|{line.rstrip()}")

</details>

Variables ruled out

  • Terminal emulator: replaying the captured pty stream through pyte, a strict VT emulator, gives the same corrupted screen as Terminal.app.
  • Terminal width: reproduces at 80, 100, 134 and 200 columns, and no text run is written past the right edge.
  • Double width characters: the repro above is pure ASCII.
  • Line length: content hard wrapped at 60 columns reproduces it.
  • Version: 2.1.252, 2.1.257, 2.1.258 and 2.1.259 all show it.
  • Client side recovery: Ctrl+L and a SIGWINCH resize both leave the corruption on screen.

The fusion is already present in the emitted bytes. Tracking the cursor through the single frame that draws the preview, the app writes one word, jumps to the next column with CHA, writes the next word, and where two transcript lines meet it emits them as one continuous line. Every cell in that frame is written exactly once, so no cell is being overwritten and no stale cell is being left behind by a missing erase.

Related

  • #84281 reports the same shape one pane over, in the picker list rather than the preview, and reads it as row heights counted in logical rows instead of wrapped display lines. The case here is width independent, so it may be a second site of the same accounting rather than the same code path.
  • #91559 reports overlapping text in the transcript view after long tool output, where Ctrl+L repaints correctly. In the preview it does not.

Environment

  • Claude Code 2.1.259, native install
  • macOS 26.2 (Darwin 25.2.0), Apple Silicon
  • Terminal.app, and a pyte based harness for verification

Expected

The preview is clipped to the viewport it is drawn into.

View original on GitHub ↗