Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,7 @@ impl ChatComposer {
let Some(text) = self.history.on_entry_response(log_id, offset, entry) else {
return false;
};
self.textarea.set_text(&text);
self.textarea.set_cursor(0);
self.set_text_content(text);
true
}

Expand Down Expand Up @@ -316,9 +315,15 @@ impl ChatComposer {
self.sync_file_search_popup();
}

pub(crate) fn clear_for_ctrl_c(&mut self) {
pub(crate) fn clear_for_ctrl_c(&mut self) -> Option<String> {
if self.is_empty() {
return None;
}
let previous = self.current_text();
self.set_text_content(String::new());
self.history.reset_navigation();
self.history.record_local_submission(&previous);
Some(previous)
}

/// Get the current composer text.
Expand Down Expand Up @@ -896,8 +901,7 @@ impl ChatComposer {
_ => unreachable!(),
};
if let Some(text) = replace_text {
self.textarea.set_text(&text);
self.textarea.set_cursor(0);
self.set_text_content(text);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this was intentional right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but also not required, not sure if there was a reason why we were doing that manually and not doing the rest of things that set_text_content does

return (InputResult::None, true);
}
}
Expand Down Expand Up @@ -1828,6 +1832,28 @@ mod tests {
assert!(!composer.esc_backtrack_hint);
}

#[test]
fn clear_for_ctrl_c_records_cleared_draft() {
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);

composer.set_text_content("draft text".to_string());
assert_eq!(composer.clear_for_ctrl_c(), Some("draft text".to_string()));
assert!(composer.is_empty());

assert_eq!(
composer.history.navigate_up(&composer.app_event_tx),
Some("draft text".to_string())
);
}

#[test]
fn question_mark_only_toggles_on_first_char() {
use crossterm::event::KeyCode;
Expand Down
34 changes: 34 additions & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,40 @@ fn ctrl_c_shutdown_ignores_caps_lock() {
}
}

#[test]
fn ctrl_c_cleared_prompt_is_recoverable_via_history() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual();

chat.bottom_pane.insert_str("draft message ");
chat.bottom_pane
.attach_image(PathBuf::from("/tmp/preview.png"), 24, 42, "png");
let placeholder = "[preview.png 24x42]";
assert!(
chat.bottom_pane.composer_text().ends_with(placeholder),
"expected placeholder {placeholder:?} in composer text"
);

chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(chat.bottom_pane.composer_text().is_empty());
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
assert!(chat.bottom_pane.ctrl_c_quit_hint_visible());

chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
let restored_text = chat.bottom_pane.composer_text();
assert!(
restored_text.ends_with(placeholder),
"expected placeholder {placeholder:?} after history recall"
);
assert!(restored_text.starts_with("draft message "));
assert!(!chat.bottom_pane.ctrl_c_quit_hint_visible());

let images = chat.bottom_pane.take_recent_submission_images();
assert!(
images.is_empty(),
"attachments are not preserved in history recall"
);
}

#[test]
fn exec_history_cell_shows_working_then_completed() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
Expand Down
Loading