Kitty graphics: images anchored to pruned scrollback rows render piled up at the top-left corner #13468
Issue DescriptionWhen the scrollback exceeds This is easy to hit in practice with TUIs that replay a large history containing inline images (e.g. resuming an AI coding-agent session): the initial frame alone can exceed the default 10MB Root cause analysis (from reading the source at
// Update any tracked pins that point to this page to point to the
// new first page to the top-left, and mark them as garbage.
const pin_keys = self.tracked_pins.keys();
for (pin_keys) |p| {
if (p.node != first) continue;
p.node = self.pages.first.?;
p.y = 0;
p.x = 0;
p.garbage = true;
}Kitty graphics placements are anchored by exactly such tracked pins ( I verified this with a storage-level test: after forcing a prune via Proposed fix — delete placements whose pin has been marked garbage, matching Kitty's behavior of dropping placements whose rows scroll out of history. I have a working patch that:
The full test suite passes ( Expected BehaviorThe image's anchor rows were pushed out of history together with the surrounding text, so the placement should be deleted and the image should no longer be rendered anywhere. This is what Kitty itself does: placements whose rows scroll off the history are dropped. Actual BehaviorThe image is glued to the top-left corner of the scrollback, drawn over the oldest retained filler lines (~line 293,000 in my run). With multiple images (the real-world case that led me here), they all stack on top of each other in that corner, overlapping unrelated text. Setting Reproduction Steps
#!/usr/bin/env bash
# kitty_prune_repro.sh
set -euo pipefail
# 1. Generate a small test image (red 200x100 PNG), base64-encoded.
b64=$(python3 - <<'PY'
import base64, zlib, struct
def chunk(t, d):
c = t + d
return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c))
w, h = 200, 100
raw = b"".join(b"\x00" + b"\xff\x00\x00" * w for _ in range(h))
png = (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))
+ chunk(b"IEND", b""))
print(base64.b64encode(png).decode())
PY
)
# 2. Display it via the Kitty graphics protocol (a=T, PNG, 20x5 cells).
printf '\x1b_Ga=T,f=100,c=20,r=5,i=42;%s\x1b\\' "$b64"
printf '\n>>> image placed above <<<\n'
# 3. Flood the scrollback well past the default 10MB scrollback-limit.
seq -f 'scrollback filler line %.0f' 1 300000
sleep infinityGhostty LogsGhostty Version(Ubuntu 26.04 distribution package. Also reproduced with a source build of OS Version InformationUbuntu 26.04 LTS (Linux only) Display ServerX11 (Linux only) Desktop Environment/Window ManagerNone (headless X11/Xvfb for the minimal repro; originally observed in a regular desktop session as well) Minimal Ghostty Configuration# No configuration required: reproduces with an empty/default configuration
# (verified with --config-default-files=false).Additional Relevant ConfigurationNo other programs are required; the reproduction script above is self-contained (bash + python3 for generating the test PNG). I acknowledge that:
AI DisclosurePer the AI usage policy: this investigation and write-up were done with substantial AI assistance (a Claude-based coding agent), driven and reviewed by me. The findings are not speculative — everything stated above was verified empirically on my machines: the reproduction script was run against the stock Ubuntu 1.3.0 package and a source build of |
Replies: 2 comments
|
For anyone interested in the concrete change, I have pushed the fix described above to a branch on my fork: Diff: main...minpeter:ghostty:fix-kitty-placement-scrollback-prune It is intentionally minimal (2 files, +115/-0):
Validation performed:
Following the contribution guidelines I am holding off on opening a PR until this is confirmed; happy to submit it (and adjust the approach, e.g. moving the cleanup out of the renderer if a different call site is preferred) once triaged. (AI disclosure: as noted in the post above, this patch was developed with AI assistance and verified by me — the regression test fails on unpatched |
|
Fixed on main |
Fixed on main