Codex CLI in WSL: long conversations can jump viewport back to top after response
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-256colorWSL_DISTRO_NAME=Ubuntu-24.04
Steps to reproduce
This is not a minimal deterministic repro yet, but the pattern is:
- Start
codexin WSL. - Keep working in the same session until the conversation/context is relatively long.
- Let Codex produce another answer, or try to scroll downward toward the latest output in that long conversation.
- 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.
8 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Which terminal emulator are you using?
I am using Windows Terminal with the Ubuntu 24.04 WSL profile.
Details from the affected session:
Same problem on 0.133 and win terminal + wsl Ubuntu
Same problem on 0.142.5 and win terminal + wsl Ubuntu
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;
## 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:
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:
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.
Adding another reproduction
codex-cli 0.144.426200.8655, x64Linux 6.6.87.2-microsoft-standard-WSL22.6.3.024.04.3 LTS1.24.11321.0TERM=xterm-256colorI 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.