TUI: Enter can be downgraded to "Queued follow-up inputs" during a long-running goal until restart
What version of Codex CLI is running?
codex-cli 0.147.0
What subscription do you have?
ChatGPT subscription (the exact tier is not exposed by the CLI in this environment)
Which model were you using?
gpt-5.6-sol, xhigh reasoning effort
What platform is your computer?
Linux 4.12.14-120-default x86_64 x86_64
What terminal emulator and version are you using (if applicable)?
xterm-256color over SSH, inside tmux 3.7b
Codex doctor report
Sanitized summary:
{
"version": "0.147.0",
"platform": "linux-x86_64",
"runtime": "standalone",
"install": "consistent",
"config": "loaded",
"auth": "configured",
"state_databases": "healthy",
"websocket": "connected (HTTP 101 Switching Protocols)",
"notes": [
"optional MCP configuration issues",
"some rollout files missing from the state DB",
"one HTTP reachability check failed, although the active goal continued streaming and running tools"
]
}
Paths, thread IDs, and host-specific details were removed.
What issue are you seeing?
During a long-running /goal turn, pressing Enter on a follow-up message can incorrectly place it under:
Queued follow-up inputs
↳ <message>
shift + ← edit last queued message
The expected Enter behavior during an active turn is the pending-steer path:
Messages to be submitted after next tool call
(press esc to interrupt and send immediately)
Tab is supposed to create a queued follow-up. In the broken state, Enter behaves semantically like Tab.
The queued message remains local and is not added to the rollout or message history, even after multiple tool-call boundaries. Exiting the entire TUI with Ctrl+C and starting Codex again clears the problem.
This is not a terminal key translation issue:
- a live
straceat the tmux server's PTY read point captured the physical key packet as0x0d(carriage return / Enter), not0x09(Tab) - there were no tmux Enter/Tab remappings
[tui.keymap]had no custom bindings- the built-in keymap is Enter = submit and Tab = queue
What steps can reproduce the bug?
This was observed in a long-lived TUI process and has not yet been reduced to a deterministic fresh-session reproducer:
- Start Codex CLI in tmux.
- Start a long-running
/goal. - Let the goal run through many model/tool boundaries.
- In the observed session, collaboration/settings selections had also changed (Plan → Default, then reasoning effort to xhigh) before the failure. This may be a contributing trigger.
- While the goal is actively running, type a normal follow-up such as
report progress percentage. - Press Enter.
- Observe that the message appears under
Queued follow-up inputsinstead ofMessages to be submitted after next tool call. - Let several tool calls complete. The message remains queued and does not enter the rollout.
- Exit the entire TUI with Ctrl+C and restart Codex. Enter resumes the expected behavior.
A useful diagnostic distinction is that the terminal really delivers Enter (0x0d); the conversion to a queued follow-up happens inside the TUI.
What is the expected behavior?
While an agent turn is active:
- Enter should create a pending steer and display
Messages to be submitted after next tool call. - Tab should create
Queued follow-up inputs. - A stale TUI state flag should not silently downgrade Enter to a queued next-turn message.
- If immediate steering is temporarily unavailable, the UI should explain why rather than presenting Enter and Tab as equivalent.
- Restarting the TUI should not be required to restore Enter behavior.
Additional information
This looks distinct from:
- #15842, where a message correctly enters the pending-steer UI but becomes stuck there
- #37974, where queued follow-ups are stranded after a failed prompt-edit branch
In this case the initial classification itself is wrong: a real Enter submission is placed directly into Queued follow-up inputs.
Based on local code inspection, a possible path is a stale submission-suppression state (for example suppress_queue_autosend) after a settings/modal transition. InputResult::Submitted can then fail the should_submit_now check and fall through to queue_user_message, producing exactly the observed UI. This is an inference, not a confirmed internal-state dump.
The same TUI process had successfully handled an earlier Enter steer, and no code path should re-enable startup queue_submissions after the session is configured. The failure appeared later in the long-running goal and disappeared only after restarting the TUI.
1 Comment
Your
suppress_queue_autosendinference checks out onmain@ 1f41cc5d92 — and the flag's lifecycle has a structural hole that produces exactly a sticky, restart-only-recoverable downgrade.The downgrade itself is your hypothesis, verbatim. Enter's
InputResult::Submittedgoes throughshould_submit_now, which requires!self.input_queue.suppress_queue_autosend; when that fails, the submission falls through toqueue_user_message— i.e. Enter becomes semantically Tab:https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/chatwidget/input_flow.rs#L32-L53
And while the flag is set,
maybe_send_next_queued_inputreturns early (#L138-L140), which is why the queued message never drains across tool-call boundaries either — both halves of your report from one bit of state.How the flag gets stuck. It's set by
defer_input_until_settings_appliedwhenever a settings modal/popup is active (#L84-L88). The only clear path is an event round-trip:on_modal_or_popup_closed→SettingsSelectionClosed→SettingsSelectionSettled, which resets the flag only if no modal/popup is active at settle time (app/event_dispatch.rs#L1377-L1386). Two weaknesses:on_modal_or_popup_closedis invoked only from the key-event handling paths (chatwidget/interaction.rs#L25-L35,#L415-L426— both gated on a key event having just closed the popup). A modal dismissed by anything other than a key — a bottom-pane view replaced by turn/goal activity, a popup closed as a side effect of an AppEvent, a selection whose action swaps the view internally — never fires the close hook, so the settle event never happens and nothing else in the codebase ever clears the flag. That's the restart-only stickiness, and it fits your observed trigger: settings selections (Plan → Default, then reasoning effort) made while a goal was actively driving the bottom pane.no_modal_or_popup_active()guard makes clearing dependent on event-loop timing when popups chain (close A → action opens B before A's settle lands). Chained closes normally re-send and recover, but it narrows to the same failure whenever the last close is a non-key dismissal.Fix shape:
handle_composer_input_result) whenno_modal_or_popup_active(), and on turn-lifecycle boundaries. The invariant "suppression never outlives the modal that set it" then can't be violated by any dismissal path, present or future.should_submit_nowfails for suppression reasons, the queue header could say why ("waiting for settings to apply") — that would have made this diagnosable from the UI instead of requiring an strace to rule out key translation.Regression test shape: set the flag via
defer_input_until_settings_applied, dismiss the modal through a non-key path (replace the bottom-pane view programmatically), then feed anInputResult::Submittedand assert it submits as a steer rather than landing inqueued_user_messages.