Inline TUI history inserts use region scrolls that xterm.js deletes — permanent transcript gaps in VS Code terminal and other xterm.js hosts
What version of Codex CLI is running?
0.139.0
What subscription do you have?
N/A — rendering bug, independent of subscription
Which model were you using?
N/A — model-independent (TUI rendering)
What platform is your computer?
macOS (Darwin 25.5.0, arm64)
What terminal emulator and version are you using (if applicable)?
Any xterm.js-based terminal. Reproduced in:
- VS Code integrated terminal (xterm.js)
- Our own Electron/Tauri terminal embedding xterm.js (CodePress Terminal)
Does not reproduce in iTerm2 or kitty.
What issue are you seeing?
Codex's inline TUI pushes finished transcript lines "into scrollback" with a scroll-region sequence:
ESC[1;{B}r set scroll region rows 1..B (rows below B hold the composer/status area)
ESC[{k}S scroll the region up k lines
ESC[r reset the scroll region
ESC[{C};1H ESC[J ... repaint from row C down (inside a DEC 2026 synchronized-output block)
On iTerm2/kitty, lines scrolled off the top of a row-1-anchored region are appended to scrollback, so this works as intended. xterm.js, however, only adds lines to scrollback on full-screen scrolls (scrollTop=0 AND scrollBottom=rows−1); region scrolls delete the lines outright.
Codex believes those lines are safely in terminal history and never repaints them, so the transcript develops permanent gaps in xterm.js hosts. A window resize "repairs" the damage because it forces Codex's full re-render — which is the smoking gun that the content was lost by the emulator, not by Codex's own state.
We root-caused this from per-tab raw PTY captures: a paragraph that vanished from the buffer disappeared at exactly such a region-scroll frame, with zero ED2/ED3/RIS anywhere before it in the stream.
What steps can reproduce the bug?
With Codex:
- Open a VS Code integrated terminal (or any xterm.js-based terminal).
- Run
codex(default inline TUI; no flags needed). - Have a conversation long enough that finished transcript lines get pushed out of the live region.
- Scroll up: earlier transcript lines are missing (gaps), not in scrollback.
- Resize the window: the missing content reappears (full re-render).
Standalone repro of the emulator divergence (no Codex needed):
seq 1 100; printf '\e[1;10r\e[5S\e[r'
Scroll up afterwards: in iTerm2/kitty the 5 lines that left the top of the region are in scrollback; in VS Code's integrated terminal they are deleted.
What is the expected behavior?
Transcript lines pushed out of the live region should survive in the terminal's scrollback in xterm.js-based terminals, the same as they do in iTerm2/kitty.
Suggested fix: when inserting history lines on the normal buffer, emit a full-screen scroll instead of a region scroll — reset the region, move the cursor to the bottom row, emit k linefeeds, then repaint the composer/status rows (all inside the existing DEC 2026 block). xterm.js records full-screen scrolls in scrollback, and the follow-up repaint Codex already issues covers the shifted rows.
Additional information
We ship a Codex-detecting rewrite at the PTY→xterm.js boundary in our terminal that performs exactly this transform:
ESC[1;{B}r ESC[{k}S ESC[r → ESC[r ESC[999;1H \n×k ESC[H
(cursor ends at home either way, matching DECSTBM semantics). Verified by replaying a captured Codex session through @xterm/headless with and without the rewrite: 46 history lines rescued, 0 of 35 final visible screen rows differed — i.e. the final screen is byte-identical because Codex's own synchronized repaint covers the rows shifted by the full-screen scroll. That patch works as a downstream mitigation, but every xterm.js embedder (VS Code included) hits this, so it seems worth fixing at the source.
Related but distinct: #14277 and xtermjs/xterm.js#5745 concern --no-alt-screen plus ED2/ED3 purges; this issue is the default inline TUI's history-insert path. Arguably xterm.js could also start scrollback-pushing row-1-anchored region scrolls (matching iTerm2/kitty), but a Codex-side fix reaches users much faster than an xterm.js + VS Code + embedder upgrade chain.
5 Comments
This issue has persisted for many versions, severely impacting the user experience. It even led me to abandon the CLI and switch to a VS Code plugin, but the Claude Code CLI doesn't have this problem. I don't know how they manage to do it.
Additional data point from Linux / GNOME Terminal. This may be a related case rather than the exact same xterm.js root cause, but the user-visible failure mode is very similar.
Environment:
Observed behavior:
What I have not yet confirmed in this environment:
codex --no-alt-screenchanges the behavior.Why I am adding this here:
This is confirmed on
codex-cli 0.139.0, and the symptom matches the “transcript gaps / visually missing previous content” class of TUI issues described in this thread. If this GNOME Terminal / VTE case is considered distinct from the xterm.js region-scroll issue, it may be worth tracking separately or linking to the broader disappearing-history issue.I am seeing a closely related scrollback/rendering symptom in Termius on iOS. I have not confirmed whether Termius uses xterm.js, and an important control test now suggests this may be a Termius-specific compatibility or resize/reflow case rather than the exact xterm.js root cause described in this issue.
Environment
codex-cli 0.144.6@openai/codexgpt-5.6-sol, reasoning effortlowLinux 7.0.0-28-generic x86_64 GNU/Linux(hostname omitted)v24.13.111.8.024 80TERM=xterm-256colorCOLORTERMunset/raw ontui.alternate_screen = "never"andcodex --no-alt-screenObserved behavior
After Codex completes a response longer than the mobile terminal viewport:
Changing the terminal height affects the result:
A related problem occurs while editing a long pasted prompt: once the prompt exceeds the mobile viewport, its beginning becomes inaccessible.
Steps to reproduce
TERM=xterm-256color.Expected behavior
Troubleshooting performed
/raw on.tui.alternate_screen = "never".codex --no-alt-screen.TERM=xterm-256color.codex resume; it showed only a summary and did not restore the missing original response.The transcript/collapsed-command-output feature appears separate and is not the source of this behavior.
Control tests
codexin the remote shell; it is not the Codex VS Code sidebar/extension UI.Because the CLI works through VS Code Remote SSH but fails through Termius/iOS, the evidence points toward an interaction between Codex's TUI rendering and Termius's handling of scroll regions, terminal-height changes, repainting, or reflow. It may be related to this issue's disappearing-history class without sharing the exact xterm.js mechanism.
Screenshots are available and can be attached manually if helpful.
I can reproduce this on Codex CLI 0.146.0 in Cursor's integrated terminal with the launch chain Windows PowerShell -> WSL ->
codex. The same WSL environment scrolls correctly when launched from Windows Terminal, which isolates the failure to the xterm.js-style host rather than WSL itself.I built and tested a prototype against
rust-v0.146.0: https://github.com/Little-Z7/codex-cursor-rich/blob/main/patches/codex-0.146.0-cursor-rich-scrollback.patchThe prototype:
running_in_vscode_terminal()host probe, so PowerShell -> WSL still detects the VS Code/Cursor host when Linux-sideTERM_PROGRAMmetadata is absent.The focused affected TUI suite passes 86/86, including escape-byte, bottom-alignment, replay, and styled-wrap regression tests. The same test step passed in GitHub Actions.
This overlaps with the prior work in #11192 and @jimbb's
ScrollbackSafeprototype linked in #14277, and I would credit that prior art in the PR. My additional focus is preserving the current rich-history path and handling the Windows PowerShell -> WSL -> Cursor detection case.Current
mainstill contains the affected partial-scroll-region path. If this approach aligns with the intended fix, would a maintainer be willing to explicitly invite a PR? I can rebase it onto currentmainand submit a minimal source-and-tests-only change, without the standalone launcher/package files or release-versionCargo.lockchurn.Independent reproduction on a different xterm.js host, with a mechanism refinement for current versions and an arithmetic check that pins the loss to a specific call site.
Environment: Orca (Electron,
@xterm/xterm6.1.0-beta.287), Windows 11,codex-cli0.146.0, spawned through node-pty with the bundled ConPTY (useConptyDll: true). Method: drive the realcodex.exethrough a PTY, capture every byte, and mirror the stream into a live@xterm/headlesswith the same options the renderer uses.The history-insert path is already scrollback-safe on 0.146.0
This issue attributes the loss to the history-insert path emitting
ESC[{k}S. That does not match what 0.146.0 actually writes. Per history line the capture shows:That is a line feed, and xterm.js does push it to scrollback — the buffer grows and
baseYadvances. In a 4-turn capture there were 356 DECSTBM sequences and only 11CSI n S, so the overwhelming majority of history arrives safely.The remaining
CSI n Scomes fromtui.rs, not from the insert pathEvery surviving SU traces to
scroll_region_up(0..area.top(), n), which ratatui expands toSetScrollRegion+ crosstermScrollUp+ResetScrollRegion. There are two call sites, both guarded byarea.bottom() > screen_size.height:draw()→scroll_region_up(0..area.top(), area.bottom() - screen_size.height)update_inline_viewport_for_resize_reflow()→scroll_region_up(0..area.top(), scroll_by)So Codex already contains the correct primitive in
insert_history.rs; the overflow path just does not reuse it. That narrows the suggested fix considerably — the insert path needs no change, only those two call sites.Arithmetic check
4-turn session, 160 history lines, no terminal resize performed by the harness:
| | |
| --- | --- |
|
CSI n Sevents | 11 (10×CSI 2S, 1×CSI 1S) || Sum of scroll amounts | 21 |
| History lines lost | 21 |
| Blank rows left in scrollback | 19 |
The sum of scroll amounts equals the lines lost exactly, and all 11 SU events are immediately preceded by a row-1-anchored region (
ESC[1;25r,ESC[1;23r) — precisely the case where those rows should have entered scrollback.The blank rows matter for triage: scrollback keeps growing, it just fills with empty lines, which is why users report "earlier output is being erased line by line" rather than "output is missing".
The trigger is viewport height change, not terminal resize — this connects #30745
A control run with zero resizes still emitted one
CSI 2Sand lost 2 lines, fired by the composer collapsing at shutdown. Conversely, driving 1695 resizes (one-column pulse, rows 30↔28, and both combined; 7 MB of repaint traffic) lost 0 lines.Loss therefore scales with conversation turns rather than output volume: 1 turn → 2 lines, 4 turns → 21 lines. A long session accumulates without bound.
This makes #30745 ("Codex TUI scrollback rows can disappear after inline viewport height changes") the same defect observed from the trigger side — that report has the right trigger but no mechanism, and this issue has the mechanism but attributes it to the wrong path.
ConPTY is not involved
Worth recording because #36474 was closed on the diagnosis that Windows discards lines scrolled out of a partial scroll region. That does not hold here.
A synthetic emitter replicating
insert_history.rsbyte-for-byte, which never emits SU, produced zeroCSI Ssequences after passing through system ConPTY, node-pty's bundledconpty.dll, and winpty. ConPTY does not synthesise SU — system ConPTY consumes the DECSTBM entirely and replays it as a bottom-of-screen\n, and the bundled DLL passes it through nearly verbatim. All three preserved 60/60 history lines.So the SU originates in Codex, and the discard is standard xterm.js behaviour on every platform, not a Windows-specific one.
xterm.js behaviour, for reference
Scroll region
ESC[1;25r, scrolled by 2, rows 1-25 pre-filled with markers,@xterm/headless6.1.0-beta.287:| Operation | Buffer length | Markers surviving |
| --- | --- | --- |
|
\r\nat the bottom margin | 30 → 32 | 25/25 ||
ESC D(IND) at the bottom margin | 30 → 32 | 25/25 ||
CSI 2 S| 30 → 30 | 23/25 ||
CSI 2 T| 30 → 30 | 23/25 ||
CSI 2 M| 30 → 30 | 23/25 |The buffer failing to grow is the reliable tell that rows were discarded rather than scrolled.
On the downstream mitigation
We prototyped the host-side workaround described in this thread and can confirm it works, with two caveats worth passing on to anyone else implementing it. It must fall through to the default handler both on the alternate screen and whenever the top margin is not row 1, since discarding is correct in those cases. And the scroll amount must be clamped to the region height — an unclamped
CSI 999Sfaithfully scrolls 999 times and floods scrollback with blank rows.Cursor position after the rewrite is identical to the default handler in every case we tested.
That said, a Codex-side fix in those two
tui.rscall sites is the smaller and more correct change, since the correct technique already exists in the same crate.