Esc while there are queued messages drops the messages back into the composer (#2687)
https://github.com/user-attachments/assets/bbb427c4-cdc7-4997-a4ef-8156e8170742
This commit is contained in:
@@ -259,7 +259,7 @@ impl ChatComposer {
|
||||
/// Replace the entire composer content with `text` and reset cursor.
|
||||
pub(crate) fn set_text_content(&mut self, text: String) {
|
||||
self.textarea.set_text(&text);
|
||||
self.textarea.set_cursor(0);
|
||||
self.textarea.set_cursor(usize::MAX);
|
||||
self.sync_command_popup();
|
||||
self.sync_file_search_popup();
|
||||
}
|
||||
|
||||
@@ -243,20 +243,51 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
// Before emitting the error message, finalize the active exec as failed
|
||||
// so spinners are replaced with a red ✗ marker.
|
||||
/// Finalize any active exec as failed, push an error message into history,
|
||||
/// and stop/clear running UI state.
|
||||
fn finalize_turn_with_error_message(&mut self, message: String) {
|
||||
// Ensure any spinner is replaced by a red ✗ and flushed into history.
|
||||
self.finalize_active_exec_cell_as_failed();
|
||||
// Emit the provided error message/history cell.
|
||||
self.add_to_history(history_cell::new_error_event(message));
|
||||
// Reset running state and clear streaming buffers.
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.running_commands.clear();
|
||||
self.stream.clear_all();
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
self.finalize_turn_with_error_message(message);
|
||||
self.request_redraw();
|
||||
|
||||
// After an error ends the turn, try sending the next queued input.
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
|
||||
/// Handle a turn aborted due to user interrupt (Esc).
|
||||
/// When there are queued user messages, restore them into the composer
|
||||
/// separated by newlines rather than auto‑submitting the next one.
|
||||
fn on_interrupted_turn(&mut self) {
|
||||
// Finalize, log a gentle prompt, and clear running state.
|
||||
self.finalize_turn_with_error_message("Tell the model what to do differently".to_owned());
|
||||
|
||||
// If any messages were queued during the task, restore them into the composer.
|
||||
if !self.queued_user_messages.is_empty() {
|
||||
let combined = self
|
||||
.queued_user_messages
|
||||
.iter()
|
||||
.map(|m| m.text.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.bottom_pane.set_composer_text(combined);
|
||||
// Clear the queue and update the status indicator list.
|
||||
self.queued_user_messages.clear();
|
||||
self.refresh_queued_user_messages();
|
||||
}
|
||||
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_plan_update(&mut self, update: codex_core::plan_tool::UpdatePlanArgs) {
|
||||
self.add_to_history(history_cell::new_plan_update(update));
|
||||
}
|
||||
@@ -913,7 +944,7 @@ impl ChatWidget {
|
||||
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
|
||||
EventMsg::TurnAborted(ev) => match ev.reason {
|
||||
TurnAbortReason::Interrupted => {
|
||||
self.on_error("Tell the model what to do differently".to_owned())
|
||||
self.on_interrupted_turn();
|
||||
}
|
||||
TurnAbortReason::Replaced => {
|
||||
self.on_error("Turn aborted: replaced by a new task".to_owned())
|
||||
|
||||
@@ -763,6 +763,45 @@ fn approval_modal_patch_snapshot() {
|
||||
assert_snapshot!("approval_modal_patch", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_restores_queued_messages_into_composer() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual();
|
||||
|
||||
// Simulate a running task to enable queuing of user inputs.
|
||||
chat.bottom_pane.set_task_running(true);
|
||||
|
||||
// Queue two user messages while the task is running.
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("first queued".to_string()));
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("second queued".to_string()));
|
||||
chat.refresh_queued_user_messages();
|
||||
|
||||
// Deliver a TurnAborted event with Interrupted reason (as if Esc was pressed).
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent {
|
||||
reason: codex_core::protocol::TurnAbortReason::Interrupted,
|
||||
}),
|
||||
});
|
||||
|
||||
// Composer should now contain the queued messages joined by newlines, in order.
|
||||
assert_eq!(
|
||||
chat.bottom_pane.composer_text(),
|
||||
"first queued\nsecond queued"
|
||||
);
|
||||
|
||||
// Queue should be cleared and no new user input should have been auto-submitted.
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"unexpected outbound op after interrupt"
|
||||
);
|
||||
|
||||
// Drain rx to avoid unused warnings.
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
}
|
||||
|
||||
// Snapshot test: ChatWidget at very small heights (idle)
|
||||
// Ensures overall layout behaves when terminal height is extremely constrained.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user