Skip to content

Commit fe56f47

Browse files
Restore the just-sent prompt on Esc before the model starts responding
Match Claude Code's "treat the query as not sent" behavior in the pre- response window: while `user_turn_pending_start` is true (the user has submitted but no `TurnStarted` notification has arrived yet) the Esc handler now also pulls the last rendered user prompt back into the composer in addition to issuing the interrupt. The user can immediately edit and resubmit without losing the typed text. The history cell for the original submission is left in place — removing it would require an extra rewind-style round trip and the rendered "User ›" line accurately reflects that the user did press Enter. Mention bindings are not preserved because `UserMessageDisplay` doesn't carry them; text, attachments, and text-element ranges round-trip cleanly. Co-authored-by: Open Codex <hff582580@gmail.com>
1 parent c983fb4 commit fe56f47

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

codex-rs/tui/src/chatwidget/input_restore.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,40 @@ impl ChatWidget {
236236
);
237237
}
238238

239+
/// Roll the most recently rendered user prompt back into the composer when
240+
/// the user pressed Esc before the model started responding. Mirrors
241+
/// Claude Code's "treat the query as not sent" behavior. Returns `true`
242+
/// when there was a pending prompt to restore.
243+
pub(super) fn rollback_pending_user_message_to_composer(&mut self) -> bool {
244+
let Some(display) = self.last_rendered_user_message_display.take() else {
245+
return false;
246+
};
247+
if display.message.trim().is_empty()
248+
&& display.local_images.is_empty()
249+
&& display.remote_image_urls.is_empty()
250+
{
251+
return false;
252+
}
253+
let local_images = display
254+
.local_images
255+
.into_iter()
256+
.enumerate()
257+
.map(|(idx, path)| crate::bottom_pane::LocalImageAttachment {
258+
placeholder: codex_protocol::models::local_image_label_text(idx + 1),
259+
path,
260+
})
261+
.collect();
262+
let user_message = UserMessage {
263+
text: display.message,
264+
local_images,
265+
remote_image_urls: display.remote_image_urls,
266+
text_elements: display.text_elements,
267+
mention_bindings: Vec::new(),
268+
};
269+
self.restore_user_message_to_composer(user_message);
270+
true
271+
}
272+
239273
pub(crate) fn capture_thread_input_state(&self) -> Option<ThreadInputState> {
240274
let draft = self.bottom_pane.composer_draft_snapshot();
241275
let composer = ThreadComposerState {

codex-rs/tui/src/chatwidget/interaction.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ impl ChatWidget {
125125
&& self.bottom_pane.no_modal_or_popup_active()
126126
&& !self.should_handle_vim_insert_escape(key_event)
127127
{
128+
// Pre-response rollback: the user just submitted but the model
129+
// has not started its turn yet. Treat the query as "not sent" —
130+
// restore the prompt to the composer (Claude Code parity) before
131+
// sending the interrupt, so the user can edit and resubmit
132+
// without losing the typed text.
133+
self.rollback_pending_user_message_to_composer();
128134
self.submit_op(AppCommand::interrupt());
129135
return;
130136
}

0 commit comments

Comments
 (0)