Windows: stray blinking cursor at the status line / mid-transcript — frames show the cursor before moving it, in separate writes
What version of Codex CLI is running?
Reproduced against codex-cli 0.145.0-alpha.2 and 0.148.0; the responsible code is unchanged on main (codex-rs/tui/src/custom_terminal.rs, try_draw_with_size).
What platform is your computer?
Windows 11 26200 (also reported on Windows 10 / 11 by downstream users; the mechanism is Windows-specific)
What issue are you seeing?
The Windows "extra blinking cursor" family — a second caret blinking next to the Working… spinner or right after the model/cwd status text while a turn runs. Same phenomenon class as #9081 (closed) and #32546 (open), plus downstream terminal reports (e.g. openwong2kim/wmux#929, where I maintain the terminal and did the byte-level analysis below).
What is actually happening — measurements
I captured codex's post-ConPTY output from byte 0 (node-pty, 120×30, both the in-box conhost and the newest OpenConsole conpty.dll), replayed it through a VT emulator, and sampled the cursor at every ?2026l frame end:
- Of 336 re-emitted sync frames in one session, 9 end with the cursor visible on the status row (right after
… · ~), 125 end visible mid-transcript, the rest at the composer. Every frame ends?25h. - Chunk-timestamped capture: those stray states persist 17–23 ms on a fast machine before the correction arrives — 1–2 rendered frames — and a full video frame (≥42 ms) on a slower reporter's machine. Quasi-periodic flashes at the same two positions read as a second blinking caret.
- Frame analysis of a downstream user's screen recording: whenever the stray caret is lit, the composer caret is dark — one cursor, teleporting; never two.
Representative re-emitted frame tail (in-box conhost, annotated):
… gpt-5.6-sol medium fast · ~ ESC[K ESC[?25h ESC[?2026l ← frame CLOSES, cursor visible at end of status text
ESC[?25l ESC[m ESC[28;3H ESC[?25h ESC[?2026h … ← restore to the composer arrives in the NEXT emission
Why this is a codex bug and not (only) a terminal/ConPTY bug
try_draw_with_size ends every frame with:
Some(position) => {
self.set_cursor_style(cursor_style)?;
self.show_cursor()?; // own flushed write → cursor VISIBLE, still parked at the last diff write
self.set_cursor_position(position)?; // own flushed write → only now moved to the composer
}
show_cursor and set_cursor_position are each an immediately-flushed write. Between those two writes the client-visible state is cursor shown, positioned wherever the frame's last diff write left it — end of the status line during a status tick, the transcript during streaming. On Linux/macOS the two writes coalesce in practice and nobody sees the window. On Windows, ConPTY interprets and re-emits output on its own schedule, and the conhost→terminal pipe chunk-splits arbitrarily, so that window regularly becomes a real, rendered frame in the hosting terminal — with the ?2026l already sent, so synchronized output cannot mask it (this is why the "upgrade to Windows Terminal ≥ 1.24" resolution of #9081 does not close the class: these are complete, well-bracketed frames whose end state is wrong from the terminal's point of view; I also measured conhost decoupling the brackets from the repaint entirely — empty ?2026h?2026l pairs with the paint outside — so bracket discipline alone cannot be relied on downstream).
A minimal ConPTY relay experiment supports the ordering account: a synthetic client that writes each frame as one write (move-then-show inside the bracket) comes through with 154/154 frame ends at the requested position, on both conpty backends. codex's multi-write tail is what leaves the window.
Fix
Swap the order: move the cursor to its requested position while it is still hidden, then show it. The cursor then only ever becomes visible at its final position, regardless of how ConPTY or the pipe slices the stream. PR attached with a regression test (CaptureBackend now records real DECTCEM/CUP bytes so the order is assertable).
This also shrinks the hide/show churn window that #32546 describes in WezTerm — the show now lands as late as possible, adjacent to the frame close.
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
The fix and its regression test are ready as a branch — PR creation on this repo appears restricted to org members (all recent PRs are
app/copyberry/-oaistaff), so linking it here instead:Branch / commit: https://github.com/openwong2kim/codex/commit/56f5d121 (
openwong2kim/codex, branchfix/cursor-move-before-show, on top of today'smain)The functional change is one reorder in
codex-rs/tui/src/custom_terminal.rs::try_draw_with_size:plus a regression test: the tests'
CaptureBackendnow records real DECTCEM/CUP bytes for cursor operations, andterminal_draw_moves_cursor_before_showing_itasserts the move precedes the show in the emitted stream.cargo test -p codex-tui custom_terminal: 12 passed. Happy to adjust anything if a maintainer wants a different shape — or feel free to apply the patch directly; attribution is not important, closing the cursor window is.