mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Suppress repeated image paste presses in legacy terminals (#51411)
## Why Legacy terminals report key auto-repeat as new presses, which can trigger repeated image pastes when presses accumulate during a slow paste. ## What changed Suppress repeated `Ctrl+V` and `Alt+V` image paste presses within a sliding 250 ms window, refreshed after each suppressed press and after a paste completes. Clear suppression on other ordinary key presses or key release events so a fresh paste can proceed immediately. ## Testing Add regression coverage for repeated presses, window expiry, and resetting suppression on another key press or a key release. Update the shortcut remapping test to release the key between image pastes. GitOrigin-RevId: 45abc056e5bda47d1c9704702e369e63dcce29db
This commit is contained in:
@@ -595,6 +595,8 @@ pub(crate) struct ChatWidget {
|
||||
pending_stream_consolidations: usize,
|
||||
/// Copy feedback is discarded with its originating conversation.
|
||||
pending_clipboard: Option<clipboard::PendingCopy>,
|
||||
/// Legacy terminals report auto-repeat as new presses; suppress the burst after a slow paste.
|
||||
suppress_image_paste_until: Instant,
|
||||
copy_last_response_binding: Vec<KeyBinding>,
|
||||
running_commands: HashMap<String, RunningCommand>,
|
||||
collab_agent_metadata: HashMap<ThreadId, AgentMetadata>,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
use super::*;
|
||||
use crate::clipboard_copy::worker::CopyResult;
|
||||
|
||||
pub(super) const IMAGE_PASTE_REPEAT_WINDOW: Duration = Duration::from_millis(/*millis*/ 250);
|
||||
|
||||
pub(super) enum PendingCopy {
|
||||
Message(u64, String),
|
||||
Selection(u64),
|
||||
@@ -28,6 +30,7 @@ impl ChatWidget {
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.suppress_image_paste_until = Instant::now() + IMAGE_PASTE_REPEAT_WINDOW;
|
||||
}
|
||||
|
||||
pub(super) fn show_clipboard_flash(&mut self, result: &CopyResult) {
|
||||
|
||||
@@ -179,6 +179,7 @@ impl ChatWidget {
|
||||
plan_stream_controller: None,
|
||||
pending_stream_consolidations: 0,
|
||||
pending_clipboard: None,
|
||||
suppress_image_paste_until: Instant::now(),
|
||||
copy_last_response_binding,
|
||||
running_commands: HashMap::new(),
|
||||
collab_agent_metadata: HashMap::new(),
|
||||
|
||||
@@ -161,13 +161,24 @@ impl ChatWidget {
|
||||
} if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
&& c.eq_ignore_ascii_case(&'v') =>
|
||||
{
|
||||
if Instant::now() < self.suppress_image_paste_until {
|
||||
self.suppress_image_paste_until =
|
||||
Instant::now() + clipboard::IMAGE_PASTE_REPEAT_WINDOW;
|
||||
return KeyEventAction::None;
|
||||
}
|
||||
self.suppress_image_paste_until =
|
||||
Instant::now() + clipboard::IMAGE_PASTE_REPEAT_WINDOW;
|
||||
return KeyEventAction::PasteImage;
|
||||
}
|
||||
other if other.kind == KeyEventKind::Press => {
|
||||
self.suppress_image_paste_until = Instant::now();
|
||||
self.bottom_pane.clear_quit_shortcut_hint();
|
||||
self.quit_shortcut_expires_at = None;
|
||||
self.quit_shortcut_key = None;
|
||||
}
|
||||
other if other.kind == KeyEventKind::Release => {
|
||||
self.suppress_image_paste_until = Instant::now();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2511,10 +2511,15 @@ async fn slash_keymap_invalid_args_show_usage() {
|
||||
async fn copy_shortcut_can_be_remapped() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
for modifiers in [KeyModifiers::CONTROL, KeyModifiers::ALT] {
|
||||
let paste = KeyEvent::new(KeyCode::Char('v'), modifiers);
|
||||
assert_matches!(
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('v'), modifiers)),
|
||||
chat.handle_key_event(paste),
|
||||
crate::chatwidget::KeyEventAction::PasteImage
|
||||
);
|
||||
chat.handle_key_event(KeyEvent {
|
||||
kind: KeyEventKind::Release,
|
||||
..paste
|
||||
});
|
||||
}
|
||||
let mut keymap_config = chat.config_ref().tui_keymap.clone();
|
||||
keymap_config.global.copy = Some(codex_config::types::KeybindingsSpec::One(
|
||||
@@ -2543,6 +2548,29 @@ async fn copy_shortcut_can_be_remapped() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_image_paste_ignores_buffered_press_until_other_input() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let paste = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL);
|
||||
|
||||
assert_matches!(chat.handle_key_event(paste), KeyEventAction::PasteImage);
|
||||
assert_matches!(chat.handle_key_event(paste), KeyEventAction::None);
|
||||
|
||||
chat.suppress_image_paste_until = Instant::now() - Duration::from_secs(/*secs*/ 1);
|
||||
assert_matches!(chat.handle_key_event(paste), KeyEventAction::PasteImage);
|
||||
|
||||
chat.suppress_image_paste_until = Instant::now() + Duration::from_secs(/*secs*/ 1);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('x')));
|
||||
assert_matches!(chat.handle_key_event(paste), KeyEventAction::PasteImage);
|
||||
|
||||
chat.suppress_image_paste_until = Instant::now() + Duration::from_secs(/*secs*/ 1);
|
||||
chat.handle_key_event(KeyEvent {
|
||||
kind: KeyEventKind::Release,
|
||||
..paste
|
||||
});
|
||||
assert_matches!(chat.handle_key_event(paste), KeyEventAction::PasteImage);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_shortcut_submits_markdown_and_reports_completion_once() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user