Codex CLI in WSL: long conversations can jump viewport back to top after response

Open 💬 8 comments Opened May 16, 2026 by xiaodream551-a11y
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

I am seeing a scroll/viewport issue in Codex CLI running inside WSL. When a conversation/context becomes long, after Codex finishes answering or when I scroll downward toward the latest output, the TUI viewport can suddenly jump back to the very top of the conversation.

I am not sure whether this is specific to the alpha build I am using, but it is reproducible enough in long sessions to disrupt normal use.

Environment

  • Product: Codex CLI / TUI
  • Version: codex-cli 0.131.0-alpha.18
  • Platform: WSL2
  • Distro: Ubuntu 24.04.4 LTS (Ubuntu-24.04)
  • Kernel: 6.6.87.2-microsoft-standard-WSL2
  • Architecture: x86_64
  • Terminal env observed from WSL:
  • TERM=xterm-256color
  • WSL_DISTRO_NAME=Ubuntu-24.04

Steps to reproduce

This is not a minimal deterministic repro yet, but the pattern is:

  1. Start codex in WSL.
  2. Keep working in the same session until the conversation/context is relatively long.
  3. Let Codex produce another answer, or try to scroll downward toward the latest output in that long conversation.
  4. Observe the TUI viewport.

Expected behavior

The viewport should stay anchored near the latest output, or preserve the current scroll position while navigating the transcript.

Actual behavior

The viewport can suddenly jump back to the top of the conversation/thread. After that, I have to manually navigate back down to the latest output.

Notes

  • This is a Codex CLI / WSL report, not Codex Desktop/App.
  • It resembles long-thread scroll jump reports in the Desktop app, but this report is specifically for the terminal TUI in WSL.
  • I am using an alpha build (0.131.0-alpha.18), so it may be an alpha regression.

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #21834
  • #22816

Powered by Codex Action

etraut-openai contributor · 2 months ago

Which terminal emulator are you using?

xiaodream551-a11y · 2 months ago

I am using Windows Terminal with the Ubuntu 24.04 WSL profile.

Details from the affected session:

  • Terminal emulator: Windows Terminal
  • WSL distro: Ubuntu-24.04
  • WSL version: WSL2
  • TERM: xterm-256color
  • Codex CLI: 0.131.0-alpha.22
  • Not using Codex Desktop/App or VS Code terminal for this issue.
SnaiCrys · 1 month ago

Same problem on 0.133 and win terminal + wsl Ubuntu

wtq524176336-source · 17 days ago

Same problem on 0.142.5 and win terminal + wsl Ubuntu

martingzh · 15 days ago

Having the same issue on 0.142.5 with windows terminal and wsl Ubuntu. It is very frustrating to use the CLI when it keeps moving back to the top of the conversation.

Found a likely root cause / proposed fix.

Root cause: Codex TUI stores finalized conversation history in the terminal’s real scrollback, not in a retained widget tree. When Codex
decides transcript “resize reflow” is needed, it clears terminal scrollback and visible screen, then replays transcript rows from in-memory
HistoryCells.

The problematic trigger appears to be in codex-rs/tui/src/app/resize_reflow.rs:

```rust
let reflow_needed = self.transcript_reflow.reflow_needed_for_width(size.width);
let height_changed = size.height != last_known_screen_size.height;
let should_rebuild_transcript = reflow_needed || height_changed;


  This means a height-only terminal size change schedules a full transcript rebuild, even when width did not change and text wrapping does not
  need repair. Once scheduled, the reflow path calls clear_terminal_for_resize_replay(), which in inline mode calls
  clear_scrollback_and_visible_screen_ansi() and resets the inline viewport area.y to 0.

  That clear sequence is intentionally destructive:

  "\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H"

  It resets the scroll region/style, homes the cursor, clears the visible screen, purges scrollback, and homes again. After that, Codex
  replays retained transcript rows. In long sessions, especially with row caps, this can change the terminal’s normal scrollback contents and
  lose the user’s current scrollback/viewport anchor. The visible terminal can then appear to jump back to the top/beginning of the
  conversation.

  This does not necessarily require manually resizing the terminal. Some terminal/WSL/Windows Terminal setups can emit resize events or
  transient height jitter around focus changes, and reflow is debounced, so the destructive clear/replay may run after clicking away and
  returning. If dimensions are truly unchanged, then another caller of the same clear/replay path, such as stream-finalization repair, may be
  involved.

  Proposed fix: height-only size changes should not schedule transcript reflow. Width changes should still reflow because wrapping changes.
  Explicit source-repair paths should remain unchanged, including stream-finalization repair and backtrack rebuild. For height-only changes,
  Codex should update viewport geometry/layout/status as needed, but must not clear and replay terminal scrollback.

  Concretely:

  - Change handle_draw_size_change so should_rebuild_transcript is driven by width reflow need, not height_changed.
  - Keep refresh_status_line() and normal draw/layout handling for any size change.
  - Do not call tui.clear_pending_history_lines() for height-only changes.
  - Replace the existing height_shrink_schedules_resize_reflow test with coverage asserting height-only shrink/grow does not create pending
    reflow state.

  - Keep existing tests for width-change reflow and explicit transcript repair paths.
wtq524176336-source · 9 days ago

## Additional reproduction: native Windows Terminal + PowerShell

I can reproduce the same issue on native Windows, without WSL.

### Environment

Component Version
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Codex CLI 0.144.1
────────────────────────────── ──────────────────────────────────────────
Codex source tag inspected rust-v0.144.1
────────────────────────────── ──────────────────────────────────────────
Source commit 44918ea10c0f99151c6710411b4322c2f5c96bea
────────────────────────────── ──────────────────────────────────────────
Windows Terminal 1.24.11321.0
────────────────────────────── ──────────────────────────────────────────
PowerShell 7.6.3
────────────────────────────── ──────────────────────────────────────────
Windows Terminal historySize 99999

### Symptom

After Codex finishes a response, Windows Terminal sometimes—or in my case almost every time—jumps to the beginning/top of the conversation instead of remaining at the latest output and composer.

This is not just missing scrollback. The visible terminal viewport itself moves back to the top after the response completes.

## Root cause

The problem appears to be caused by Codex’s destructive transcript reflow path.

The completion flow is:

TurnCompleted
-> on_task_complete()
-> flush_answer_stream_with_separator()
-> controller.has_live_tail()
-> ConsolidationScrollbackReflow::Required
-> finish_required_stream_reflow()
-> reflow_transcript_now()
-> clear_terminal_for_resize_replay()
-> clear scrollback and visible screen
-> reset viewport_area.y to 0
-> replay transcript rows

openai/codex/codex-rs/tui/src/chatwidget/protocol.rs:232 - 249

TurnCompleted calls on_task_complete():

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/chatwidget/protocol.rs#L232-L249

openai/codex/codex-rs/tui/src/chatwidget/turn_runtime.rs:92 - 130

on_task_complete() finalizes the streaming response:

self.flush_answer_stream_with_separator();

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/chatwidget/turn_runtime.rs#L92-L130

openai/codex/codex-rs/tui/src/chatwidget/streaming.rs:19 - 49

If the stream still has a live tail, Codex marks the scrollback reflow as required:

let scrollback_reflow = if controller.has_live_tail() {
crate::app_event::ConsolidationScrollbackReflow::Required
} else {
crate::app_event::ConsolidationScrollbackReflow::IfResizeReflowRan
};

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/chatwidget/streaming.rs#L19-L49

This is a common condition because streamed responses often finish with an unterminated final paragraph or another pending tail.

openai/codex/codex-rs/tui/src/app/agent_message_consolidation.rs:76 - 88

Required unconditionally calls:

self.finish_required_stream_reflow(tui)?;

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/app/agent_message_consolidation.rs#L76-L88

openai/codex/codex-rs/tui/src/app/resize_reflow.rs:271 - 282

That method immediately schedules and runs a transcript reflow:

self.schedule_immediate_resize_reflow(tui);
self.maybe_run_resize_reflow(tui)?;

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/app/resize_reflow.rs#L271-L282

openai/codex/codex-rs/tui/src/app/resize_reflow.rs:398 - 423

The reflow implementation clears the terminal before replaying the transcript:

self.clear_terminal_for_resize_replay(tui)?;

It then re-inserts all retained transcript lines:

tui.insert_history_hyperlink_lines_with_wrap_policy(
reflowed_lines,
self.history_line_wrap_policy(),
);

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/app/resize_reflow.rs#L398-L423

openai/codex/codex-rs/tui/src/app/resize_reflow.rs:235 - 245

In the normal inline TUI, this calls the destructive scrollback-clear function and explicitly resets the internal viewport to the top:

tui.terminal.clear_scrollback_and_visible_screen_ansi()?;

let mut area = tui.terminal.viewport_area;
if area.y > 0 {
area.y = 0;
tui.terminal.set_viewport_area(area);
}

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/app/resize_reflow.rs#L235-L245

openai/codex/codex-rs/tui/src/custom_terminal.rs:535 - 551

The actual ANSI sequence is:

write!(
self.backend,
"\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H"
)?;

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/custom_terminal.rs#L535-L551

This sequence:

ESC[r resets the scroll region
ESC[0m resets styles
ESC[H moves the cursor to the top-left
ESC[2J clears the visible screen
ESC[3J purges saved scrollback
ESC[H moves the cursor to the top-left again

Codex then replays the transcript from memory. On Windows Terminal, the terminal viewport can remain anchored at the beginning of the replay instead of following the newly replayed output to the
bottom. The visible result is exactly the reported jump to the top of the conversation.

This matches the investigation already posted in:

https://github.com/openai/codex/issues/22936#issuecomment-4888358077

## Secondary trigger: height-only terminal changes

There is another destructive-reflow trigger.

openai/codex/codex-rs/tui/src/app/resize_reflow.rs:291 - 314

The current code schedules a complete transcript rebuild for height-only changes:

let reflow_needed = self
.transcript_reflow
.reflow_needed_for_width(size.width);

let height_changed = size.height != last_known_screen_size.height;

let should_rebuild_transcript = reflow_needed || height_changed;

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/app/resize_reflow.rs#L291-L314

A height-only change does not change text wrapping, so it should not require purging and rebuilding terminal scrollback.

Windows Terminal, WSL, focus changes, tab changes, or layout changes may generate transient height changes. Because the reflow is debounced, the destructive clear/replay can appear to happen when
the response finishes, even if the underlying trigger was a temporary terminal size event.

## Proposed fix

### 1. Do not rebuild scrollback for height-only changes

The smallest low-risk change would be to make transcript reflow depend only on width-related reflow requirements:

let reflow_needed = self
.transcript_reflow
.reflow_needed_for_width(size.width);

let height_changed = size.height != last_known_screen_size.height;

// A height-only change affects layout but does not invalidate wrapping.
let should_rebuild_transcript = reflow_needed;

The existing status-line/layout refresh should still run for any size change:

if size != last_known_screen_size {
self.refresh_status_line();
}

This would ensure that height-only changes:

  • update viewport/layout state normally;
  • refresh the status line;
  • do not clear pending history lines;
  • do not emit CSI 2J or CSI 3J;
  • do not purge and replay the entire transcript.

Suggested regression tests:

height-only shrink does not schedule transcript reflow
height-only growth does not schedule transcript reflow
height-only changes still refresh layout/status state
width changes still schedule transcript reflow

### 2. Do not perform a full destructive replay for every completed live tail

controller.has_live_tail() currently turns a normal response completion into:

ConsolidationScrollbackReflow::Required

That eventually purges and rebuilds the entire terminal scrollback even when the terminal width has not changed.

The stream-finalization path should distinguish between:

A. A real width change occurred during streaming
B. A responsive/table render actually requires historical rows to be replaced
C. Only the final uncommitted live tail needs to be appended

For case C, Codex should:

  1. Consolidate the transient AgentMessageCells into the source-backed AgentMarkdownCell in memory.
  2. Append only the finalized, not-yet-committed tail to terminal history.
  3. Avoid calling clear_terminal_for_resize_replay().
  4. Preserve the terminal’s existing scrollback and viewport anchor.

A full destructive replay should be reserved for cases where previously committed rows actually need to be reformatted, such as a real terminal width change.

Conceptually:

if resize_reflow_actually_ran_during_stream || finalized_content_requires_replacement {
// Existing full source-backed replay path.
self.finish_required_stream_reflow(tui)?;
} else {
// Commit only the remaining finalized tail.
self.insert_finalized_stream_tail(tui, deferred_history_cell)?;
self.maybe_finish_stream_reflow(tui)?;
}

The exact implementation can reuse the existing deferred_history_cell, which already represents the finalized tail that was intentionally not written before consolidation.

Suggested regression tests:

turn completion with a plain live tail does not emit CSI 3J
turn completion with unchanged terminal width preserves existing scrollback
turn completion remains anchored at the latest output
width change during streaming still performs source-backed reflow
responsive table finalization still produces the correct final rendering

### 3. Restore a temporary supported escape hatch

terminal_resize_reflow was introduced in:

https://github.com/openai/codex/pull/18575

The feature gate was later removed and the behavior made always-on in:

https://github.com/openai/codex/pull/27794

As of 0.144.1, this means:

[features]
terminal_resize_reflow = false

is ignored.

Until destructive replay is reliable across Windows Terminal and other terminal implementations, it would be useful to restore a supported configuration option such as:

[tui]
terminal_resize_reflow = false

This would provide an immediate compatibility fallback without requiring users to downgrade Codex.

## Why the existing workarounds do not solve it

### codex --no-alt-screen

This does not address this bug because the destructive operation happens in the normal inline-buffer branch:

if tui.is_alt_screen_active() {
tui.terminal.clear_visible_screen()?;
} else {
tui.terminal.clear_scrollback_and_visible_screen_ansi()?;
}

Disabling the alternate screen still leaves Codex on the else branch that emits CSI 3J.

### terminal_resize_reflow = false

This is ignored in current releases because the feature was changed to always-on.

### terminal_resize_reflow_max_rows = 0

This only disables the replay row limit. It does not disable transcript reflow or the destructive clear/replay sequence. It may make the operation slower in long sessions.

## Additional Windows Terminal impact

openai/codex/codex-rs/tui/src/resize_reflow_cap.rs:19 - 29

Codex hardcodes a 9001-row replay cap for Windows Terminal:

const WINDOWS_TERMINAL_RESIZE_REFLOW_MAX_ROWS: usize = 9_001;

https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/tui/src/resize_reflow_cap.rs#L19-L29

My Windows Terminal is configured with:

"historySize": 99999

Codex does not read that configured value. Therefore, after purging the terminal’s real scrollback, Codex may replay only the newest 9001 rendered rows even though the terminal is configured to
retain much more history.

This is not the direct cause of the viewport jump, but it increases the impact of the destructive reflow.

## Temporary user workaround

On Windows Terminal, the default shortcut to return to the latest terminal output is:

Ctrl+Shift+End

This recovers from the jump but does not fix the underlying issue.

For an A/B comparison without replacing the globally installed Codex version, a version from before the resize-reflow implementation can be launched with:

npm exec --yes --package=@openai/codex@0.125.0 -- codex

This should only be used as a diagnostic comparison, not as a recommended long-term downgrade.

## Summary

The main issue is not normal terminal scrolling. Codex intentionally:

homes the cursor
clears the visible screen
purges real terminal scrollback
resets its internal viewport to y = 0
replays the retained transcript

The Windows Terminal viewport is not reliably restored to the latest output afterward.

The preferred fix is to avoid destructive full-transcript replay unless previously committed rows truly need to be reformatted. Height-only changes and normal live-tail completion should not purge
global terminal scrollback.

SimonPurdie · 5 days ago

Adding another reproduction

  • Codex CLI: codex-cli 0.144.4
  • Platform: Windows 11 Home 25H2, build 26200.8655, x64
  • WSL platform: Linux 6.6.87.2-microsoft-standard-WSL2
  • WSL: 2.6.3.0
  • Distro: Ubuntu 24.04.3 LTS
  • Terminal: Windows Terminal 1.24.11321.0
  • TERM=xterm-256color
  • Multiplexer: none

I can reproduce this in long Codex sessions in Windows Terminal via WSL.

The most disruptive case for me is Plan mode. If I start reading the generated plan while Codex is still writing it, then when the plan completes and Codex asks whether I want to proceed, the viewport jumps all the way back to the top of the conversation.

That makes Plan mode awkward for its intended use: reviewing the proposed work before approving it. I lose my place exactly when I need to make a decision, and have to scroll back down through a long transcript to continue reviewing.