Cursor jumps between UI regions in third-party terminals (Tabby, Wave)
What version of Codex CLI are you using?
codex-cli 0.120.0
What subscription do you have?
free
What platform is your computer?
Microsoft Windows NT 10.0.26200.0 x64 (Windows 11)
What terminal emulator and version are you using?
- Tabby (latest)
- Wave Terminal (latest)
- Windows Terminal (for comparison)
- Warp (for comparison)
- System default cmd.exe and pwsh
What issue are you seeing?
When running Codex CLI in certain third-party terminals (Tabby, Wave), the cursor rapidly jumps between multiple UI regions:
- The
Working (XXs · esc to interrupt)status line - The
> inputcommand line - The model info line (e.g.,
gpt-5.4 high · ~)
This makes it difficult to see where the actual input cursor is and is visually distracting.
What steps can reproduce the bug?
- Open Tabby or Wave terminal
- Run
codex - Send a prompt and wait for the model to process
- Observe the cursor jumping between the status line, input line, and bottom info bar
Expected behavior
The cursor should remain at the input field, as it does in stock Windows terminals, Windows Terminal, and Warp.
Additional information
| Terminal | Shell | Behavior |
|----------|-------|----------|
| System default terminal | cmd/pwsh | ✅ Normal |
| Windows Terminal | cmd/pwsh | ✅ Normal |
| Warp | cmd/pwsh | ✅ Normal |
| Tabby | cmd | ❌ Cursor jumps |
| Tabby | pwsh | ❌ Cursor jumps |
| Wave | cmd | ❌ Cursor jumps |
| Wave | pwsh | ❌ Cursor jumps |
The issue occurs in Tabby and Wave regardless of shell (cmd or pwsh). System default terminal, Windows Terminal, and Warp all work correctly. This suggests the problem is related to how Codex's TUI handles ANSI escape sequences for cursor positioning in specific terminal emulators.
This appears related to #16687 but this report adds more terminal coverage, notably that Warp works correctly while Tabby and Wave do not.
7 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I’m seeing what looks like the same issue on:
0.125.0pwsh``
toml
``[tui]
terminal_title = []
animations = false
show_tooltips = false
status_line = ["model-with-reasoning", "current-dir"]
alternate_screen = "always"
In my case the cursor visibly flickers/jumps down to the status line while I’m actively typing in the text input field, then returns to the input cursor position. It also flickers up into the output area while output is scrolling. So this is not only during long-running tool calls or streaming output; ordinary text entry can trigger it too.
animations = falsedoes not prevent it. The behavior feels like Codex’s redraw path is exposing intermediate terminal cursor moves during frame rendering, especially around the status/statusline and scrolling output areas.Expected: while typing in the composer, the hardware cursor should stay visually stable at the input insertion point. Status/model/footer redraws and output scrolling should not visibly move the cursor.
The duplicate check already points to #16687, and this appears to have the same root cause.
I’ve prepared a branch that addresses the cursor sync issue:
https://github.com/snowyukitty/codex/tree/fix-tui-cursor-sync-frame
GitHub rejected upstream PR creation from my account on
openai/codex, so I’m sharing the branch here for now.I hope this is helpful.
I adapted the existing sync-frame patch to current main and added a deterministic backend-order test:
https://github.com/Livehide/codex/tree/codex/fix-tui-cursor-sync-frame
commit: 1554e7bed8279c62bf005b211852da0a3610c267
The change writes synchronized-update markers through the same custom terminal backend as history writes, frame drawing, and final cursor positioning. The regression test asserts begin -> cell write -> final cursor position -> draw flush -> end.
Validation on Windows:
Important caveat: current main did not reproduce the footer position in three unpatched raw captures (all were 0), so I am not claiming a current-main A/B symptom delta. The branch preserves the single-writer ordering contract and carries the deterministic regression test.
GitHub rejected PR creation because openai/codex currently allows collaborators only. The implementation is adapted from snowyukitty/codex@9ef4d31b; snowysmile is credited as co-author.
Thanks for picking this up and for keeping the attribution. I rebased your adaptation onto current
main, and I think the caveat you flagged is not a gap in the measurement — it is telling us the patch cannot move this symptom on its own.Why that A/B had to come back null. BSU/ESU are advisory, and the two terminals in this report discard them. Tabby and Wave both render through xterm.js 5.x, which does not implement DEC private mode 2026 at all — Wave's own tracker has it pinned to xterm.js 5.5.0 (wavetermdev/waveterm#2787), and support only landed in xterm.js 6.0.0 via xtermjs/xterm.js#5453. Windows Terminal does implement 2026 (backported in 1.23.20211.0, stable in 1.24), and Warp and conhost repaint from their own screen buffer rather than from the byte stream as it arrives, so in all three the intermediate positions never reach the screen. That is exactly the split in the table at the top of this issue. So a patch that only changes which writer emits the markers cannot change what Tabby or Wave draw: the markers are thrown away before they mean anything.
What those terminals actually receive. A frame does not cross the pty boundary once. The terminal is
CustomTerminal<CrosstermBackend<Stdout>>, andstd::io::stdout()is aLineWriterwith a 1 KiB buffer that also flushes on every newline (library/std/src/io/stdio.rs,LineWriter::new->with_capacity(1024, ...)). History insertion emitsPrint("\r\n")per line (insert_history.rs:143,:152,:211), so a frame that pushes N history rows crosses that boundary at least N times, plus once per 1 KiB of diff. A terminal that ignores 2026 paints each of those chunks, and every chunk ends with the caret wherever the lastMoveToleft it — the footer, the status line, then the composer. That is the mechanism the terminal split points to. I could not A/B it on the affected terminals directly; I do not have Tabby or Wave installed.The part that does not depend on the terminal. Keep the caret hidden for the duration of the frame and let the frame place it. Then no chunk boundary can expose an intermediate caret position, whether or not the terminal implements 2026.
Branch, stacked on your commit so the sync-frame work and its test stay intact:
https://github.com/snowyukitty/codex/tree/fix-tui-caret-hidden-during-frame
5697cd23— your adaptation, cherry-picked onto currentmain. One conflict, in the test backend:mainnow emitsMoveTofromCaptureBackend::set_cursor_position, so I kept that write alongside your recorded event.f6166797— hide the caret across the synchronized frame.Terminal::sync_updatehides it after BSU and restores it before ESU only when the frame did not place a caret itself, sotry_draw_with_sizekeeps deciding and the pet-image draws get the caller's caret back.Two tests pin the invariant: the caret is hidden before the first cell write and shown only after the final position, and a frame that never touches the caret has it restored inside the same frame.
Validation on Windows (
x86_64-pc-windows-msvc, rustc 1.95.0):cargo test -p codex-tui --lib— 3745 passed, 3 failed. The three are the same temp-home path-format cases you hit (status::helpers::tests::compose_agents_summary_*, expecting~\AppData\...and gettingC:\Users\...); they fail identically with both files reverted tomain.RUST_MIN_STACKraised on a Windows debug build or an unrelatedapp::testscase overflows its stack. Also unchanged frommain.cargo clippy -p codex-tui --all-targets --all-features -- -D warnings— clean.cargo fmt— clean.Correcting myself on one point, since it matters to anyone reviewing this: the two commits answer different questions, but they are not separable as written.
f6166797builds on theTerminal::sync_updatethat5697cd23introduces, so taking the caret change on its own means first porting it onto the fourstdout().sync_updatecall sites intui.rs. The sync-frame refactor is a correctness improvement in its own right — one writer, ordering guaranteed structurally rather than by both paths happening to share the global stdout buffer — and worth keeping either way. The caret change is the one that should be visible on Tabby and Wave. If someone on either terminal can try the branch, that is the confirmation neither of us can produce.Still no PR from me either;
openai/codexonly takes branches from collaborators.Follow-up to my last comment, with the caret change repackaged so it can be reviewed
on its own.
A minimal branch that does not depend on the refactor. I said before that the two
commits were not separable as written, because the caret change built on the
Terminal::sync_updatethe first commit introduced. That was true of thatimplementation, not of the fix.
One commit on current
main, one line per frame plus two tests. It hides the caret atthe top of both synchronized frames, and nothing has to remember to bring it back. The
reason is worth stating precisely, because my first version of it was sloppy: it is not
that every frame ends in a draw — a frame can fail partway through. It is that
try_draw_with_sizewrites a caret decision in both of its arms, and thatTerminal::hide_cursorandTerminal::show_cursorwrite unconditionally rather thanshort-circuiting on
hidden_cursor. A frame that reaches its draw therefore ends withthe caret in the state that frame asked for, whatever preceded it. A frame that does
not reach its draw failed writing to stdout; the caret stays hidden until the next
frame that succeeds, or until
restore_commonandDrop for Terminalrestore it.This is the whole fix for what this issue reports; nothing else in my earlier branch is
required for it.
cargo test -p codex-tui --lib— 3738 passed, 3 failed (the same three temp-homepath cases; unmodified
mainreads 3736 passed, 3 failed).cargo clippy -p codex-tui --all-targets --all-features -- -D warnings— clean.A defect in the version I posted before. In
Terminal::sync_update, the caret hidethat opened the frame and the caret restore that closed it both reached
EndSynchronizedUpdatethrough?. Either failing left the begin marker unmatched, anda terminal that implements mode 2026 holds the screen until the end marker arrives — so
that failure mode is not a torn frame, it is a terminal that stops presenting, with
whatever the process writes next held along with it. That is fixed on the branch, along
with a pre-existing hole of the same shape: a panic inside a frame runs the panic hook,
and
restore_commonnever ended the frame either, so the panic message could beswallowed by it. It now emits the end marker unconditionally, which is a no-op when no
frame is open.
The stacked branch keeps the same-writer refactor plus that hardening, for anyone who
wants the structural version:
https://github.com/snowyukitty/codex/tree/fix-tui-caret-hidden-during-frame
It also covers two things the minimal branch deliberately leaves alone: the pet-image
frames, which write to the backend without hiding the caret, and the error path, where
its
sync_updaterestores the caller's caret rather than leaving it hidden. Neither isimplicated in what this issue reports.
On this issue's terminals specifically. Neither branch can be A/B'd on Tabby or
Wave from here; I do not have either installed, and I said in my last comment why the
BSU/ESU change alone could not have moved the symptom there. The caret hide is the part
that does not depend on the terminal honouring mode 2026 — or on how completely it
honours it — so it is the part worth trying. If anyone on Tabby or Wave can run the
minimal branch, that is the confirmation I still cannot produce.
I've also posted the minimal branch on #16687, where @noaho04 reported that positioning
before showing and hiding before the scrollback insertion together fixed this on
alacritty. #40166 landed the first of those two; this is the second.
I said last time that the missing piece was someone running the caret branch on Tabby or Wave, and that I could not produce that. I think I was asking for the wrong thing. The property that makes those terminals show this is that they do not implement synchronized output, and that property is reproducible here.
vt100— already a dependency ofcodex-tui, already used as the test terminal — implements no synchronized-output mode either. SoBeginSynchronizedUpdatebuys nothing against it, exactly as it buys nothing in Tabby and Wave, and it presents each piece of a frame as it arrives.Drive a real
Tui::drawinto avt100screen behind aLineWriter— the same line bufferingStdoutapplies, and the reason a frame reaches the terminal in pieces at all — and sample the caret at each write boundary. On today'smainone frame gives this:Four different places, caret visible at each. That is "the cursor rapidly jumps between multiple UI regions" as a data structure. The return to column 0 between each move is the line buffer flushing one history row at a time, which is also why streaming output is when you see it.
With the caret hidden for the frame, the same trace shows the caret in at most one place.
Branch
https://github.com/snowyukitty/codex/tree/fix/tui-caret-frame-regression-test
Two commits on
2df6705423:793aec83— the caret hide. Unchanged from what I posted before, just carried to currentmain.18b8efb7— the test, and the one thing needed to make it possible.That one thing is a defaulted backend parameter on
Tui.TuinamedCrosstermBackend<Stdout>directly, so nothing could observe what a frame wrote — which is why deleting the caret hide left the suite green. With the default,Tuistill meansTui<CrosstermBackend<Stdout>>everywhere and no call site changes; it is a struct header and fourimplheaders. The test then asserts the invariant directly, so removing either caret hide fails it.If you want only the fix, the first commit stands alone.
What this does and does not establish
It establishes that a frame drawn without the caret hidden paints a visible caret in several places on a terminal that ignores mode 2026, and that hiding it for the frame reduces that to one.
It does not establish that Tabby and Wave are fully described by "ignores mode 2026". That step is still inference, though it is the same inference that explained why my earlier BSU/ESU-only change could not have moved this. Nobody has run either branch on the terminals in this report, and that confirmation would still be worth having.
Measured on
2df6705423, Windows,cargo test -p codex-tui --lib: 3758 passed / 3 failed before, 3761 passed / 3 failed after — the three additions are this branch's own tests, and the three failures are pre-existing onmainhere (unrelated; #40289).cargo clippy -p codex-tui --all-targets --all-features -- -D warningsis clean. I have not run the suite on Linux or macOS.I still cannot open a pull request from a fork — if I have that wrong, say so and I will open one.