TUI: footer duplicates and “Working” status briefly disappears after interrupt + immediate resubmission

Open 💬 2 comments Opened Jan 24, 2026 by dyxushuai
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

What version of Codex is running?

codex-cli 0.89.0

What subscription do you have?

plus

Which model were you using?

gpt-5.2

What platform is your computer?

Linux 6.17.4-2-pve x86_64 unknown

What terminal emulator and version are you using (if applicable)?

wezterm

What issue are you seeing?

If I interrupt a running turn with Esc and immediately submit a new message, the bottom area of the TUI can briefly look idle. During that window, I sometimes see the footer rendered twice (two different context left percentages on separate lines), and the “Working” status line is missing until the backend catches up.

What steps can reproduce the bug?

  1. Start the Codex TUI (e.g. run codex).
  2. Submit any prompt so the agent starts working.
  3. Press Esc to interrupt the current turn.
  4. Immediately submit another message (press Enter to send).
  5. Watch the bottom area right after submission.

What is the expected behavior?

Right after the new submission, the UI should show a clear “Working” (or equivalent) status and should not render duplicate footer lines.

Additional information

Screenshot

<img width="729" height="94" alt="Image" src="https://github.com/user-attachments/assets/f31ee4a1-1eed-4105-88fb-8ffc4a60008d" />

View original on GitHub ↗

2 Comments

etraut-openai contributor · 6 months ago

A proposed fix was submitted in this PR.

astral303 · 6 days ago

Fresh diagnosis against main at 536f86e5cc9ec1ff38457d099bf320b9d08eeeba: this is a cross-platform, terminal-independent pending-turn lifecycle bug. The original Linux/WezTerm environment is consistent with the shared code path involved.

What happens

  1. The user interrupts an active turn and immediately submits another message. The TUI records that pending input should be sent after interruption through submit_pending_steers_after_interrupt.
  2. When the interrupted turn completes, on_interrupted_turn calls finalize_turn. This clears the active lifecycle and makes the bottom pane idle.
  3. The same interruption handler immediately resubmits the pending steer as a new turn.
  4. The submission path sets user_turn_pending_start = true, correctly recording that work has been submitted but has not emitted TurnStarted.
  5. update_task_running_state, however, derives the bottom-pane state only from an active turn, review mode, and MCP startup. It does not include the pending-start state.
  6. The TUI therefore has user_turn_pending_start set while bottom_pane.is_task_running() remains false.
  7. The gap ends only when TurnStarted invokes on_task_started.

The status header may already say Working, but the status row is not rendered because the bottom pane considers itself idle.

This also accounts for the footer artifact. The bottom pane temporarily loses the status row and contracts, moving the footer upward. When TurnStarted arrives, the row returns and the pane expands again. That height churn allows the previous and current footer rows to remain visible at different positions during the transition.

Why the state is fragile

Pending-start handling is split across several paths:

Required invariant

Once a start-producing operation is accepted by its local dispatch boundary, the TUI should remain task-running until either:

  • the corresponding TurnStarted consumes the pending reservation, or
  • that same dispatch attempt fails and rolls back its reservation.

A rejected attempt must not clear an older accepted pending start.

The task-running calculation should include pending-start state alongside active turns, review mode, and MCP startup. The same reservation lifecycle should cover UserTurn, Compact, Review, and RunUserShellCommand.

Transport side effects should occur only after local acceptance:

Suggested regression coverage

A deterministic test does not need timing or a real terminal:

  • Submit a turn successfully but do not emit TurnStarted.
  • Assert that pending-start and bottom-pane task-running states are both true.
  • Render a snapshot containing one Working row and one footer.
  • Emit TurnStarted and verify that the bottom-pane height does not change.
  • Cover failed direct enqueue, app-server rejection, and a failed second attempt while an older reservation remains active.