You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Stale cell data visible on new rows after scrolling with default cursor style #138
When repeatedly writing escape-heavy output (SGR sequences, 256-color, truecolor) to the terminal followed by scrolling, new rows can display stale cell data from previously used page memory. This manifests as content from old rows appearing horizontally merged into current rows.
Root cause
In Screen.zig, cursorDownScroll() only clears new row cells when the cursor has a non-default background color (bg_color != .none). When programs reset attributes with ESC[0m (which sets bg_color = .none), pages.grow() extends existing pages by appending rows whose cell memory still contains data from previously erased rows. These stale cells are never cleared and become visible on lines that aren't fully overwritten (e.g., empty lines from bare \r\n sequences with default cursor style).
Symptoms
Content from previous rows appears merged horizontally into current rows
Bug is transient — self-corrects on the next write to that row
Bug is periodic — frequency depends on column width (e.g., ~every 11 writes at cols=160)
Affects all column widths at different frequencies
Reproduction
Create a terminal (any reasonable size, e.g. 160x39)
Repeatedly write escape-heavy output containing SGR color sequences followed by ESC[0m resets
After enough repetitions to trigger pages.grow(), inspect viewport rows
Some rows will contain horizontally concatenated content from two different terminal lines
Expected behavior
New rows created during scrolling should always have clean/empty cells regardless of cursor style.
Just wanted to let you know: this issue is fixed in diegosouzapw/ghostty-web v0.4.0 (see commit). cursorDownScroll in Screen.zig now unconditionally clears new rows, eliminating stale cell data visible after scroll with default cursor style.
If you're looking for a fork with this and many other upstream fixes, feel free to give it a try!
I'm also building OmniRoute, an open project that provides free LLM model access — I plan to use ghostty-web there, so the more reliable it gets, the better. 🙏 Contributions welcome!
Classification — bug. No status:could-not-reproduce outcome: the behavior reproduced locally.
Summary — Repeated escape-heavy writes followed by scrolling can expose stale cell contents in newly grown rows when the cursor style is back at the default background after ESC[0m. The visible symptom is row text that intermittently contains horizontally merged/stale content from previous rows. This matches the issue description and is reproducible against the current checkout.
Evidence / reproduction — I reproduced this with a local WASM build from the current repository state.
Environment / setup used:
bun install
zig 0.15.2 via mise; then PATH="$(mise where zig@0.15.2)/bin:$PATH" ./scripts/build-wasm.sh
The test constructs a 160x39 terminal, repeatedly writes a large escape-heavy payload containing SGR resets, and compares the viewport text after each write to the first stable baseline. Result:
Targeted reproduction output
Corrupt at reps: [2, 7, 12, 14, 19, 24, 27, 29]
error: expect(received).toBe(expected)
Expected: 0
Received: 8
(fail) Viewport row-merge bug > viewport text is stable after repeated writes
scrollback=10000: corrupt at [2, 7, 12, 14]
scrollback=1000000: corrupt at [2, 7, 12, 14]
scrollback=50000000: corrupt at [2, 7, 12, 14]
Original: 101479 bytes, patched: 177107 bytes
With workaround: corrupt at [] (0/30)
5 pass
2 fail
Ran 7 tests across 1 files.
The passing checks are also useful: the corruption was non-consecutive/transient, independent of the tested scrollback limits, and getViewport() and getLine() agreed at the corrupt state. That narrows the failure to the terminal/WASM state rather than the TypeScript viewport extraction layer. Replacing ESC[0m with an explicit reset plus background color (ESC[0;48;2;10;10;10m) avoided the corruption in the same test loop, which supports the default-background branch as the trigger.
Relevant code / likely cause — The current ghostty/src/terminal/Screen.zig implementation only clears the newly grown row when the cursor background is non-default:
// Clear the new row so it gets our bg color. We only do this// if we have a bg color at all.if (self.cursor.style.bg_color!=.none) {
constpage: *Page=&page_pin.node.data;
self.clearCells(
page,
self.cursor.page_row,
page.getCells(self.cursor.page_row),
);
}
After ESC[0m, self.cursor.style.bg_color is .none, so pages.grow() can leave the new row's cell memory uncleared. If the following output does not fully overwrite that row, stale cells can remain visible. The workaround result above is consistent with this branch: keeping an explicit background color causes row clearing and prevents the corruption.
Recommendation — Treat this as a confirmed bug and proceed with an implementation fix. The smallest code-level direction is to clear newly grown rows unconditionally in cursorDownScroll() while preserving the later background-fill behavior for styled rows. PR fix: always clear new row cells during scroll to prevent stale data #134 already contains this shape of fix plus regression coverage and should be reviewed or used as the implementation starting point. The regression test should stay focused on stable viewport text after repeated escape-heavy writes and on the default-background ESC[0m case.
Adjacent issues — I searched for duplicate issue/PR text around stale cells, row merge, scrolling, and cursorDownScroll. No duplicate issue was found. The related open implementation PR is fix: always clear new row cells during scroll to prevent stale data #134. No adjacent issue was created.
Generated with mux • Model: openai:gpt-5.5 • Thinking: xhigh
Activity
fix: clear stale ghostty-web canvas on new terminal open
fix: viewport corruption and stale cell data from page memory reuse (#19
diegosouzapw commentedon May 24, 2026
Hi @darrinm! 👋
Just wanted to let you know: this issue is fixed in diegosouzapw/ghostty-web v0.4.0 (see commit).
cursorDownScrollinScreen.zignow unconditionally clears new rows, eliminating stale cell data visible after scroll with default cursor style.If you're looking for a fork with this and many other upstream fixes, feel free to give it a try!
I'm also building OmniRoute, an open project that provides free LLM model access — I plan to use ghostty-web there, so the more reliable it gets, the better. 🙏 Contributions welcome!
ThomasK33 commentedon Jun 26, 2026
Note
This triage report is AI-generated using Mux
Classification —
bug. Nostatus:could-not-reproduceoutcome: the behavior reproduced locally.Summary — Repeated escape-heavy writes followed by scrolling can expose stale cell contents in newly grown rows when the cursor style is back at the default background after
ESC[0m. The visible symptom is row text that intermittently contains horizontally merged/stale content from previous rows. This matches the issue description and is reproducible against the current checkout.Evidence / reproduction — I reproduced this with a local WASM build from the current repository state.
Environment / setup used:
bun installzig 0.15.2viamise; thenPATH="$(mise where zig@0.15.2)/bin:$PATH" ./scripts/build-wasm.shbun test lib/triage-138-viewport-row-merge.test.ts.The test constructs a
160x39terminal, repeatedly writes a large escape-heavy payload containing SGR resets, and compares the viewport text after each write to the first stable baseline. Result:Targeted reproduction output
The passing checks are also useful: the corruption was non-consecutive/transient, independent of the tested scrollback limits, and
getViewport()andgetLine()agreed at the corrupt state. That narrows the failure to the terminal/WASM state rather than the TypeScript viewport extraction layer. ReplacingESC[0mwith an explicit reset plus background color (ESC[0;48;2;10;10;10m) avoided the corruption in the same test loop, which supports the default-background branch as the trigger.Relevant code / likely cause — The current
ghostty/src/terminal/Screen.zigimplementation only clears the newly grown row when the cursor background is non-default:After
ESC[0m,self.cursor.style.bg_coloris.none, sopages.grow()can leave the new row's cell memory uncleared. If the following output does not fully overwrite that row, stale cells can remain visible. The workaround result above is consistent with this branch: keeping an explicit background color causes row clearing and prevents the corruption.Recommendation — Treat this as a confirmed bug and proceed with an implementation fix. The smallest code-level direction is to clear newly grown rows unconditionally in
cursorDownScroll()while preserving the later background-fill behavior for styled rows. PR fix: always clear new row cells during scroll to prevent stale data #134 already contains this shape of fix plus regression coverage and should be reviewed or used as the implementation starting point. The regression test should stay focused on stable viewport text after repeated escape-heavy writes and on the default-backgroundESC[0mcase.Adjacent issues — I searched for duplicate issue/PR text around stale cells, row merge, scrolling, and
cursorDownScroll. No duplicate issue was found. The related open implementation PR is fix: always clear new row cells during scroll to prevent stale data #134. No adjacent issue was created.Generated with
mux• Model:openai:gpt-5.5• Thinking:xhighfix: keep terminals showing output after a ghostty WASM trap