mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Edit queued messages with Vim history-up (#38907)
## What changed - When the composer is empty in Vim normal mode, make the configured history-up binding restore the latest queued follow-up for editing. - Remove the restored message from the queue so submitting the edited version replaces it instead of creating a duplicate. - Preserve normal history navigation when the composer has text or a popup is active, and honor remapped Vim bindings. ## Testing - Add TUI tests covering repeated edit-and-requeue cycles and a remapped history-up binding. GitOrigin-RevId: 0b04dd964d3fc6fdcd10712aa4d5fd81cbbd2255
This commit is contained in:
committed by
copyberry
parent
6c108912ee
commit
2bc43d516e
@@ -44,6 +44,11 @@
|
||||
//! - Persistent cross-session history (text-only; no element ranges or attachments).
|
||||
//! - Local in-session history (full text + text elements + local/remote image attachments).
|
||||
//!
|
||||
//! Before composer history handles Vim-normal history-up, `ChatWidget` restores the latest queued
|
||||
//! follow-up when the composer is empty and no popup is active. It removes the message from the
|
||||
//! queue before placing it in the composer, so editing and requeuing it replaces the prior
|
||||
//! revision instead of creating duplicates.
|
||||
//!
|
||||
//! When recalling a local entry, the composer rehydrates text elements and both attachment kinds
|
||||
//! (local image paths + remote image URLs).
|
||||
//! When recalling a persistent entry, only the text is restored.
|
||||
|
||||
@@ -1222,8 +1222,12 @@ impl BottomPane {
|
||||
popup_consts::standard_popup_hint_line_for_keymap(&self.keymap.list)
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_keymap(&self) -> &RuntimeKeymap {
|
||||
&self.keymap
|
||||
}
|
||||
|
||||
pub(crate) fn list_keymap(&self) -> crate::keymap::ListKeymap {
|
||||
self.keymap.list.clone()
|
||||
self.runtime_keymap().list.clone()
|
||||
}
|
||||
|
||||
/// Replace one or more active views whose IDs are in `view_ids` with a
|
||||
|
||||
@@ -110,7 +110,18 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
if key_event.kind == KeyEventKind::Press
|
||||
&& self.chat_keymap.edit_queued_message.is_pressed(key_event)
|
||||
&& (self.chat_keymap.edit_queued_message.is_pressed(key_event)
|
||||
|| (self.bottom_pane.composer_is_empty()
|
||||
&& self
|
||||
.bottom_pane
|
||||
.keymap_contexts()
|
||||
.contains(crate::keymap::KeymapContext::VimNormal)
|
||||
&& self
|
||||
.bottom_pane
|
||||
.runtime_keymap()
|
||||
.vim_normal
|
||||
.move_up
|
||||
.is_pressed(key_event)))
|
||||
&& self.has_queued_follow_up_messages()
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
{
|
||||
|
||||
@@ -1676,6 +1676,92 @@ async fn alt_up_edits_most_recent_queued_message() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn vim_normal_history_up_edits_queued_message_without_duplicating_it() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.toggle_vim_mode_and_notify();
|
||||
chat.bottom_pane.set_task_running(/*running*/ true);
|
||||
chat.bottom_pane
|
||||
.set_composer_text("first queued message".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
|
||||
for expected in ["first queued message", "first queued message edited"] {
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(chat.bottom_pane.composer_text(), expected);
|
||||
assert!(chat.input_queue.queued_user_messages.is_empty());
|
||||
assert!(
|
||||
chat.input_queue
|
||||
.queued_user_message_history_records
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text(format!("{expected} edited"), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
assert_eq!(chat.input_queue.queued_user_messages.len(), 1);
|
||||
assert_eq!(
|
||||
chat.input_queue.queued_user_message_history_records.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
chat.queued_user_message_texts(),
|
||||
vec!["first queued message edited edited".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn vim_normal_queued_message_edit_uses_remapped_history_up() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let mut keymap = crate::keymap::RuntimeKeymap::defaults();
|
||||
keymap.vim_normal.move_up = vec![crate::key_hint::plain(KeyCode::F(2))];
|
||||
chat.bottom_pane.set_keymap_bindings(&keymap);
|
||||
chat.toggle_vim_mode_and_notify();
|
||||
chat.bottom_pane.set_task_running(/*running*/ true);
|
||||
chat.input_queue
|
||||
.queued_user_messages
|
||||
.push_back(UserMessage::from("queued message").into());
|
||||
chat.input_queue
|
||||
.queued_user_message_history_records
|
||||
.push_back(UserMessageHistoryRecord::UserMessageText);
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE));
|
||||
|
||||
assert!(chat.bottom_pane.composer_is_empty());
|
||||
assert_eq!(chat.input_queue.queued_user_messages.len(), 1);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "queued message");
|
||||
assert!(chat.input_queue.queued_user_messages.is_empty());
|
||||
assert!(
|
||||
chat.input_queue
|
||||
.queued_user_message_history_records
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
let width = 80;
|
||||
let height = chat.desired_height(width);
|
||||
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height))
|
||||
.expect("create terminal");
|
||||
terminal
|
||||
.draw(|frame| chat.render(frame.area(), frame.buffer_mut()))
|
||||
.expect("render restored queued message");
|
||||
insta::assert_snapshot!(
|
||||
normalized_backend_snapshot(terminal.backend())
|
||||
.lines()
|
||||
.filter(|line| line.contains("queued message"))
|
||||
.map(|line| line.trim_matches('"').trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
@"› queued message"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unbound_queued_message_edit_does_not_fall_back_to_alt_up() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user