fix(tui): stop losing queued messages and dropping the wrong steer on flush (#398 follow-ups)#411
Conversation
… flush Follow-ups to #398 (chat.flush_queued_messages), from the close analysis of the superseded #399. Bug 1 (data loss): flush_queued_messages drained the rejected-steer and queued-message queues *before* submitting and discarded the submit result. Every refusal path (unavailable thread model, blocked image attachments, closed op channel) therefore destroyed the queued follow-ups and pasted the merged text over the user's live composer draft. The merge is now built from clones, the queues are drained only once the submission is accepted, and the composer draft is snapshotted/restored around a refused submit. An accepted submit can still be re-queued at the front (session not yet configured, auth-profile switch, usage-limit self-heal), so only the originally merged entries are removed. Bug 2 (wrong entry dropped): enqueue_rejected_steer() popped the *front* pending steer, but after a merged flush the rejected steer usually sits behind older pending steers. Add enqueue_rejected_steer_matching_items(), which matches on the compare_key the steer was recorded with, and use it from the thread-routing path that has the rejected items. The notification path keeps the front-pop fallback because that error carries no items. Also: - apply MAX_USER_INPUT_TEXT_CHARS to the merged steer instead of letting an oversized merge fail server-side after the queues were drained; - render a "<binding> submit all now" hint in the queued-input preview, driven by the resolved runtime keymap (so a remapped binding shows) and re-plumbed from apply_keymap_update; - fix the two pre-existing clippy::byte_char_slices errors in core/src/unified_exec/process_manager_tests.rs that fail whole-workspace clippy -D warnings (from #327), rather than silencing them. Regression tests cover the refused-submit path (queues + draft intact), the accepted-but-requeued path, a non-front rejected steer, the no-match fallback, the length cap, and the hint rendering/visibility.
Proof the regression tests actually catch the bugsThe pre-fix behaviour was re-introduced locally (drain-before-submit + discard the submit The one that passes pre-fix — With the fix in place, all of them plus the pre-existing flush tests and the hint tests pass: Local gates on the branch: Whole-suite note: |
Blacksmith Testbox gate: PASSRun 30125143217 — Command: Result: This also confirms the 10 |
What this fixes
PR #398 landed
chat.flush_queued_messages(Shift+Enter → merge every queued follow-upinto the running turn). PR #399 was a competing implementation that was closed as
superseded, but its close analysis identified two real bugs in the code #398 actually
merged (#398's own reviewer independently flagged the first as a lower-severity concern).
Both are on
maintoday. This PR ports only the specific fixes — it does not resurrect#399.
Bug 1 — data loss on a refused flush (
codex-rs/tui/src/chatwidget/input_flow.rs)flush_queued_messagesused tostd::mem::takethe rejected-steer queues andpop_frontthe queued-message queues before submitting, then discard the returnvalue of
resubmit_queued_user_message_with_history_record.Submission can legitimately be refused:
input_submission.rseffective_mode.model().trim().is_empty()→
restore_user_message_to_composer(...), returnsfalserestore_blocked_image_submission(...),returns
falsesubmit_opfails, returnsfalseOn every one of those paths the queues had already been drained, so all queued
follow-ups were silently destroyed, and the refusal path then pasted the merged text
over whatever the user was still typing in the composer.
Fix
submission is accepted.
mention bindings, pending pastes and the caret) before submitting and restore it when the
submission is refused, so the user's in-progress draft survives instead of being clobbered.
configured, auth-profile auto-switch, usage-limit self-heal).
drain_flushed_entriestherefore removes only the originally merged entries and skips anything the submission
itself pushed on top, so the accepted replacement is never eaten.
Proven by (
codex-rs/tui/src/chatwidget/tests/review_mode.rs)unavailable_model_flush_preserves_queued_messages_and_composer_draft— fullcapture_thread_input_state()equality before/after, composer text unchanged, the"Thread model is unavailable." error is still surfaced.
failed_flush_submit_preserves_queued_messages_and_composer_draft— dropped op channel.accepted_flush_preserves_front_requeued_merged_message— the accepted-but-requeued case,asserting the merged replacement survives and the originals are gone.
Bug 2 — the wrong pending steer was dropped (
codex-rs/tui/src/chatwidget/input_restore.rs)enqueue_rejected_steer()popped the front pending steer when the server answeredActiveTurnNotSteerable. After a merged flush the rejected steer is frequently not thefront entry (older pending steers sit ahead of it), so the wrong item was requeued and the
actually-rejected one was dropped.
Fix
enqueue_rejected_steer_matching_items(&[UserInput])locates the pending steer by the samecompare_keyit was recorded with (Self::pending_steer_compare_key_from_items) and removesthat entry.
codex-rs/tui/src/app/thread_routing.rs— the path that has the rejecteditemsin hand — now uses it. The notification path inturn_runtime.rskeeps thefront-pop fallback because
AppServerCodexErrorInfo::ActiveTurnNotSteerable { turn_kind }carries no items.
Proven by
rejected_steer_is_matched_by_compare_key_not_queue_position— a merged steer queuedbehind an older pending steer; asserts the older steer stays pending and the merged one
lands in
rejected_steers_queue.rejected_steer_matching_items_reports_no_match— unknown items ⇒false(caller fallsback to the error message) and nothing is removed.
Also (same analysis)
MAX_USER_INPUT_TEXT_CHARScap on the merged steer. The merge produced one user inputwith no length check, unlike every other user-input path
(
chat_composer.rs, app-serverturn_start). The flush now rejects an oversized merge upfront with an actionable error and leaves the queues + draft intact.
Test:
oversized_flush_preserves_queued_messages_and_composer_draft.<binding> submit all nowline while a turn is running and something is actuallyflushable. The binding comes from the resolved runtime keymap
(
chat_keymap.flush_queued_messages) viaset_queued_message_flush_binding, so a remappedbinding is displayed, and it is re-plumbed from
apply_keymap_updateso a/keymapremaptakes effect without a restart.
Tests:
render_queued_message_with_remapped_flush_binding(F12 snapshot),flush_hint_is_hidden_when_flush_is_unavailable, plus the two updated chatwidgetsnapshots that now show
shift + enter submit all now.Pre-existing clippy fix (unblocks whole-workspace
clippy -D warnings)codex-rs/core/src/unified_exec/process_manager_tests.rs:151,176had twoclippy::byte_char_sliceserrors (untouched by this work, from #327) that failwhole-workspace
cargo clippy -D warningsfor everyone. Fixed properly(
[b'a', b'b', b'c']→*b"abc"), not silenced with-A.Verification
cargo fmt --check— clean (scripts/format.pyneedsuv, which is absent on thetestbox, so
cargo fmtis invoked directly).cargo clippy -p codex-tui -p codex-core --tests -- -D warnings— clean.cargo nextest run -p codex-tui— the six new regression tests pass; each was confirmedto fail against the pre-fix behaviour before the fix was applied.
blacksmith-testbox.yml) is the gate for this PR; run linked inthe thread.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.