Skip to content

Inline images duplicate every transcript row below them into native scrollback on --resume #6775

Description

@turygo

Summary

Any inline image in the transcript causes every row below it to be painted twice into the terminal's native scrollback when the session is re-rendered (omp --resume).

The renderer first paints the whole transcript with a text placeholder ( [Image: image/webp]) and commits the tail to native scrollback. Only afterwards does it transmit the real image over the Kitty graphics protocol and repaint everything from the image position downward. The first copy is already in native scrollback and cannot be erased, so the user sees the tool blocks, the todo card and the entire final assistant message a second time.

Environment

  • omp 17.1.5 (installed via bun global, dist/cli.js)
  • macOS 25.5.0, arm64 (Apple M3 Pro)
  • Ghostty 1.3.2 — but the repro below runs in a raw pty.fork() with TERM=xterm-256color, replayed through pyte, so it is not Ghostty-specific; it needs only the Kitty graphics path to be taken.
  • terminal.showImages at its default (true).

Reproduction

Fully self-contained, no private data. ~30 s, one cheap model call.

mkdir -p /tmp/omp-imgdup && cd /tmp/omp-imgdup

# 1. an image big enough that encoding is deferred (a 320x200 PNG is NOT enough)
python3 - <<'PY'
import random, struct, zlib
random.seed(1); w, h = 1080, 1920
raw = b"".join(b"\x00" + bytes(random.getrandbits(8) for _ in range(w*3)) for _ in range(h))
def chunk(t, d):
    c = t + d
    return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c) & 0xffffffff)
open("shot.png", "wb").write(
    b"\x89PNG\r\n\x1a\n"
    + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
    + chunk(b"IDAT", zlib.compress(raw, 6))
    + chunk(b"IEND", b""))
PY

# 2. build a session: read the image, then emit 40 lines of text below it
omp -p --model anthropic/claude-haiku-4.5 --no-title --no-skills --no-rules \
    --auto-approve --session-dir ./sess \
    "Use the read tool on ./shot.png, then in your final reply print a numbered list from 1 to 40, each line exactly 'line NN: alpha bravo charlie delta echo'. Use no other tool."

# 3. re-render it in a terminal shorter than the transcript
omp --resume ./sess/*.jsonl --no-title --no-tools

Scroll up: line 01line 40 each appear twice.

Scripted check (raw PTY 100x30, replayed through pyte.HistoryScreen):

total rendered rows: 58619
'line NN: alpha bravo charlie delta echo' rows: 40 distinct, every one counted exactly 2x

Observed vs expected

rendered rows line 40 occurrences
default 58619 2
--config with terminal: {showImages: false} 1087 1

Expected: one copy.

Root cause evidence

Byte offsets in the raw PTY stream of a real --resume (176x58) of a session whose only image sits 148 rows before the end:

 214866   ' [Image: image/webp]' placeholder painted
 245493   last paragraph of the final assistant message — 1st copy
          (already committed to native scrollback)
 247812   first Kitty graphics escape \x1b_G  (195 chunks follow)
1076889   last paragraph of the final assistant message — 2nd copy

The duplicated span is exactly the 148 rows that follow the image placeholder: the Read <file> tool block, a bash tool block, the Todo card, and the whole final assistant message. Deterministic — reproduced on every run, at 100x30 and 176x58.

The session .jsonl itself is clean (each paragraph stored once), and the same transcript with the image record removed renders once at 80x24 / 120x30 / 160x45 / 176x58 / 200x50. The image is the only trigger.

Related but distinct: #5766 (stale Kitty graphics when disabling inline images).

Workaround

# ~/.omp/agent/config.yml
terminal:
  showImages: false

Suggested fix direction

The transcript is committed to native scrollback before the image payload is resolved, so the post-image repaint has nothing it can legally rewrite. Either hold the commit boundary above the first unresolved image until its payload is ready, or reserve the placeholder's final row count up front so the later paint is an in-place update rather than a re-emit.

Not verified

  • Whether the same duplication occurs during a live turn (image tool result arriving mid-stream, text streaming below it). My live repro attempts hit provider 403s; only the --resume path is confirmed.
  • Non-Kitty image protocols (iTerm2, sixel) were not tested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triagedtuiTerminal UI rendering and displayuxUser experience improvementswontfixThis will not be worked on

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions