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?
- Start the Codex TUI (e.g. run
codex). - Submit any prompt so the agent starts working.
- Press
Escto interrupt the current turn. - Immediately submit another message (press Enter to send).
- 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" />
2 Comments
A proposed fix was submitted in this PR.
Fresh diagnosis against
mainat 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
submit_pending_steers_after_interrupt.on_interrupted_turncallsfinalize_turn. This clears the active lifecycle and makes the bottom pane idle.user_turn_pending_start = true, correctly recording that work has been submitted but has not emittedTurnStarted.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.user_turn_pending_startset whilebottom_pane.is_task_running()remains false.TurnStartedinvokeson_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
TurnStartedarrives, 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:
prepare_local_op_submission.submit_op.prepare_local_op_submissionruns before a direct channel send is known to have succeeded.AppEventSender::sendlogs and swallows enqueue failures, so callers cannot distinguish accepted dispatch from rejection.handle_turn_start_rejectionfinalizes shared turn state rather than releasing state owned specifically by the rejected attempt.Required invariant
Once a start-producing operation is accepted by its local dispatch boundary, the TUI should remain task-running until either:
TurnStartedconsumes the pending reservation, orA 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, andRunUserShellCommand.Transport side effects should occur only after local acceptance:
submit_opsuccessfully sends the command.AppEvent::CodexOppath.Suggested regression coverage
A deterministic test does not need timing or a real terminal:
TurnStarted.Workingrow and one footer.TurnStartedand verify that the bottom-pane height does not change.