Inline TUI history inserts use region scrolls that xterm.js deletes — permanent transcript gaps in VS Code terminal and other xterm.js hosts

Open 💬 5 comments Opened Jun 11, 2026 by lightninglu10

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:

  1. Open a VS Code integrated terminal (or any xterm.js-based terminal).
  2. Run codex (default inline TUI; no flags needed).
  3. Have a conversation long enough that finished transcript lines get pushed out of the live region.
  4. Scroll up: earlier transcript lines are missing (gaps), not in scrollback.
  5. 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.

View original on GitHub ↗

5 Comments

Juddd · 2 months ago

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.

Juddd · 2 months ago

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:

codex-cli: 0.139.0
OS: Ubuntu 22.04.5 LTS (Jammy)
Kernel: Linux 5.15.0-164-generic x86_64
Desktop/session: GNOME on X11
Terminal process detected: /usr/libexec/gnome-terminal-server
TERM: xterm-256color
COLORTERM: truecolor
Shell: /bin/bash
WSL_DISTRO_NAME: unset
WT_SESSION: unset
TMUX: unset
ZELLIJ: unset
VSCODE_IPC_HOOK_CLI: unset

Observed behavior:

  • In normal multi-turn Codex CLI sessions, after sending a later prompt, parts of earlier transcript content visually disappear.
  • The missing parts are typically chunks of previous assistant/user text rather than the whole session.
  • This looks like a TUI rendering / transcript display problem, not necessarily actual model context loss.
  • It has a large UX impact because earlier reasoning and instructions become visually unavailable during long sessions.

What I have not yet confirmed in this environment:

  • Whether resizing the terminal reliably restores the missing text.
  • Whether codex --no-alt-screen changes the behavior.
  • A raw PTY capture showing the exact escape sequence frame where the text disappears.

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.

tempeduck · 1 month ago

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: codex-cli 0.144.6
  • Installation: npm package @openai/codex
  • Model: gpt-5.6-sol, reasoning effort low
  • OS: Ubuntu 26.04 LTS
  • Kernel/platform: Linux 7.0.0-28-generic x86_64 GNU/Linux (hostname omitted)
  • Node.js: v24.13.1
  • npm: 11.8.0
  • Affected terminal: Termius 7.5.0 on iPhone/iOS
  • Connection: SSH to an Ubuntu VM
  • Terminal size when collected: 24 80
  • TERM=xterm-256color
  • COLORTERM unset
  • No tmux, GNU screen, or Zellij
  • Codex raw output mode enabled with /raw on
  • Tested both tui.alternate_screen = "never" and codex --no-alt-screen

Observed behavior

After Codex completes a response longer than the mobile terminal viewport:

  • The end of the response remains visible.
  • The beginning cannot be reached using normal Termius scrollback.
  • Scrolling upward reveals terminal content from before the response instead of the missing beginning.
  • It appears that earlier response lines have been overwritten, hidden, or never committed to accessible terminal scrollback.
  • Codex completes normally, returns to the prompt, and displays its completion time, so this does not appear to be model-output truncation.

Changing the terminal height affects the result:

  • Hiding the iOS keyboard increases the terminal height and reveals more of the response.
  • Showing the keyboard reduces the height and hides part of the response again.
  • The missing beginning still cannot be recovered through normal scrollback.

A related problem occurs while editing a long pasted prompt: once the prompt exceeds the mobile viewport, its beginning becomes inaccessible.

Steps to reproduce

  1. Connect from an iPhone using Termius 7.5.0 to an Ubuntu system over SSH.
  2. Confirm TERM=xterm-256color.
  3. Run Codex CLI 0.144.6 with raw output enabled and alternate-screen mode disabled.
  4. Request a response longer than one mobile terminal viewport.
  5. Allow the response to finish.
  6. Scroll upward in Termius.
  7. Open and close the iOS keyboard to change the reported terminal height.
  8. Observe that only the later portion of the response remains accessible and older pre-response terminal content appears above it.
  9. As a separate reproduction, paste a prompt long enough to exceed the viewport and attempt to access its beginning while editing.

Expected behavior

  • The complete finalized response should remain available in normal terminal scrollback.
  • Scrolling upward should reveal the beginning of the response.
  • Terminal-height changes should reflow or redraw the viewport without losing or hiding finalized response lines.
  • All parts of a long prompt should remain accessible while editing.

Troubleshooting performed

  • Enabled /raw on.
  • Tested tui.alternate_screen = "never".
  • Tested codex --no-alt-screen.
  • Confirmed TERM=xterm-256color.
  • Confirmed no terminal multiplexer is active.
  • Reinstalled Codex CLI.
  • Removed an older duplicate launcher and confirmed that only one installation is active.
  • Attempted to compare with Codex CLI 0.130.0, but the currently used model requires a newer CLI.
  • Tried 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

  • The same Codex CLI works correctly when accessed through the VS Code integrated terminal over SSH to the same server. This still runs codex in the remote shell; it is not the Codex VS Code sidebar/extension UI.
  • The failure reproduces specifically through Termius on iOS with the same remote Codex installation and server environment.
  • Claude Code and Gemini CLI also preserve their output correctly through the affected Termius client, using the same SSH connection, Ubuntu system, terminal dimensions, and terminal settings.

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.

Little-Z7 · 26 days ago

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.patch

The prototype:

  • generalizes the existing newline-based insertion path into a scrollback-safe mode for VS Code/Cursor-like terminals;
  • uses bottom-row CRLF/full-screen scrolling for finalized rich history insertion and inline viewport growth instead of DECSTBM/reverse-index operations;
  • preserves ratatui colors and styles across pre-wrapped physical rows rather than switching to raw-output mode; and
  • reuses the existing running_in_vscode_terminal() host probe, so PowerShell -> WSL still detects the VS Code/Cursor host when Linux-side TERM_PROGRAM metadata 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 ScrollbackSafe prototype 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 main still 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 current main and submit a minimal source-and-tests-only change, without the standalone launcher/package files or release-version Cargo.lock churn.

yoke233 · 22 days ago

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/xterm 6.1.0-beta.287), Windows 11, codex-cli 0.146.0, spawned through node-pty with the bundled ConPTY (useConptyDll: true). Method: drive the real codex.exe through a PTY, capture every byte, and mirror the stream into a live @xterm/headless with 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:

ESC[1;25r        SetScrollRegion(1..area.top())  — top margin is row 1
ESC[25;1H        MoveTo(0, area.top() - 1)       — bottom margin of that region
\r\n             LF at the bottom margin
<line text>
ESC[r            ResetScrollRegion

That is a line feed, and xterm.js does push it to scrollback — the buffer grows and baseY advances. In a 4-turn capture there were 356 DECSTBM sequences and only 11 CSI n S, so the overwhelming majority of history arrives safely.

The remaining CSI n S comes from tui.rs, not from the insert path

Every surviving SU traces to scroll_region_up(0..area.top(), n), which ratatui expands to SetScrollRegion + crossterm ScrollUp + ResetScrollRegion. There are two call sites, both guarded by area.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 S events | 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 2S and 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.rs byte-for-byte, which never emits SU, produced zero CSI S sequences after passing through system ConPTY, node-pty's bundled conpty.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/headless 6.1.0-beta.287:

| Operation | Buffer length | Markers surviving |
| --- | --- | --- |
| \r\n at 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 999S faithfully 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.rs call sites is the smaller and more correct change, since the correct technique already exists in the same crate.