mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Flush expired paste bursts before handling Enter (#49810)
## Why Without a UI flush tick, expired paste detection can cause Enter to be treated as part of a paste instead of submitting buffered input. ## What changed Flush due characters and paste buffers before classifying Enter during submission, including in Vim insert mode. Preserve the buffered text when submission is blocked for a parent-owned thread. ## Testing Add regression tests for held characters and buffered bursts with and without Vim mode and parent-owned threads, plus a snapshot test for Enter submitting expired input in Vim insert mode. GitOrigin-RevId: 1d0d309b57e9d2151e0d9c1774a3401d05413d3c
This commit is contained in:
committed by
copyberry
parent
2fde968de8
commit
cca440697f
@@ -234,31 +234,20 @@
|
||||
//!
|
||||
//! # Non-bracketed Paste Bursts
|
||||
//!
|
||||
//! On some terminals (especially on Windows), pastes arrive as a rapid sequence of
|
||||
//! `KeyCode::Char`, `KeyCode::Enter`, and `KeyCode::Tab` key events instead of a single paste event.
|
||||
//!
|
||||
//! To avoid misinterpreting these bursts as real typing (and to prevent transient UI effects like
|
||||
//! shortcut overlays toggling on a pasted `?`), we feed text-producing character events (plain,
|
||||
//! Shift, or Windows AltGr) into
|
||||
//! [`PasteBurst`](super::paste_burst::PasteBurst), which buffers bursts and later flushes them
|
||||
//! through [`ChatComposer::handle_paste`].
|
||||
//! Some terminals, especially Windows, deliver pastes as rapid `KeyCode::Char`/`Enter`/`Tab` events.
|
||||
//! [`PasteBurst`](super::paste_burst::PasteBurst) buffers text-producing keys (plain, Shift, or
|
||||
//! Windows AltGr) and integrates buffered text through the composer, avoiding transient shortcut UI.
|
||||
//! It briefly holds the first ASCII char to detect a burst. Non-ASCII chars appear immediately
|
||||
//! for IME responsiveness, but still participate in burst detection.
|
||||
//! Parent views must keep flushing editors that lose focus while input is buffered; a hidden
|
||||
//! editor's pending burst can otherwise keep the shared draw loop waiting indefinitely.
|
||||
//!
|
||||
//! The burst detector intentionally treats ASCII and non-ASCII differently:
|
||||
//!
|
||||
//! - ASCII: we briefly hold the first fast char (flicker suppression) until we know whether the
|
||||
//! stream is paste-like.
|
||||
//! - non-ASCII: we do not hold the first char (IME input would feel dropped), but we still allow
|
||||
//! burst detection for actual paste streams.
|
||||
//!
|
||||
//! The burst detector can also be disabled (`disable_paste_burst`), which bypasses the state
|
||||
//! machine and treats the key stream as normal typing. When toggling from enabled → disabled, the
|
||||
//! composer flushes/clears any in-flight burst state so it cannot leak into subsequent input.
|
||||
//! Submission flushes expired characters and buffers before classifying Enter, independent of
|
||||
//! UI flush ticks. `disable_paste_burst` bypasses detection; setting it flushes and clears in-flight state.
|
||||
//! Mouse edits flush pending typing; selection and copy behavior lives in [`mouse`]. Confirmed
|
||||
//! copies clear the selection while preserving the draft and cursor.
|
||||
//!
|
||||
//! For the detailed burst state machine, see `codex-rs/tui/src/bottom_pane/paste_burst.rs`.
|
||||
//! See `codex-rs/tui/src/bottom_pane/paste_burst.rs` for the detailed state machine.
|
||||
//!
|
||||
//! # PasteBurst Integration Points
|
||||
//!
|
||||
@@ -3163,6 +3152,8 @@ impl ChatComposer {
|
||||
should_queue: bool,
|
||||
now: Instant,
|
||||
) -> (InputResult, bool) {
|
||||
self.handle_paste_burst_flush(now);
|
||||
|
||||
if !should_queue && self.handle_paste_enter(now) {
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
@@ -5056,6 +5047,10 @@ mod agents_navigation_tests;
|
||||
#[path = "chat_composer_effort_tests.rs"]
|
||||
mod effort_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "chat_composer_enter_tests.rs"]
|
||||
mod enter_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "chat_composer/embedded_input_tests.rs"]
|
||||
mod embedded_input_tests;
|
||||
|
||||
@@ -91,6 +91,7 @@ impl ChatComposer {
|
||||
///
|
||||
/// - UI ticks via [`ChatComposer::flush_paste_burst_if_due`], so held first-chars can render.
|
||||
/// - Input handling via [`ChatComposer::handle_input_basic`], so a due burst does not lag.
|
||||
/// - Submission, before deciding whether Enter belongs to an active paste burst.
|
||||
pub(super) fn handle_paste_burst_flush(&mut self, now: Instant) -> bool {
|
||||
match self.draft.paste_burst.flush_if_due(now) {
|
||||
FlushResult::Paste(pasted) => {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Enter must expire paste detection even when no UI flush tick has run.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::InputResult;
|
||||
use super::PasteBurst;
|
||||
use super::tests::new_test_composer;
|
||||
use super::tests::snapshot_composer_state_with_width;
|
||||
|
||||
#[test]
|
||||
fn enter_expires_paste_state_without_a_ui_tick() {
|
||||
for vim_enabled in [false, true] {
|
||||
for parent_owned in [false, true] {
|
||||
for text in ["x", "hi"] {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
composer.set_vim_enabled(vim_enabled);
|
||||
composer.draft.textarea.enter_vim_insert_mode();
|
||||
if parent_owned {
|
||||
composer.set_parent_owned_thread();
|
||||
}
|
||||
let now = Instant::now();
|
||||
for ch in text.chars() {
|
||||
composer.handle_input_basic_with_time(
|
||||
KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
|
||||
now,
|
||||
);
|
||||
}
|
||||
assert!(composer.is_in_paste_burst());
|
||||
assert_eq!(composer.current_text(), "");
|
||||
|
||||
let result = composer.handle_submission_with_time(
|
||||
/*should_queue*/ false,
|
||||
now + PasteBurst::recommended_active_flush_delay(),
|
||||
);
|
||||
let expected = if parent_owned {
|
||||
InputResult::ParentOwnedInputBlocked
|
||||
} else {
|
||||
InputResult::Submitted {
|
||||
text: text.to_owned(),
|
||||
text_elements: Vec::new(),
|
||||
}
|
||||
};
|
||||
assert_eq!(result, (expected, true));
|
||||
assert_eq!(
|
||||
composer.current_text(),
|
||||
if parent_owned { text } else { "" }
|
||||
);
|
||||
assert!(!composer.is_in_paste_burst());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_enter_submits_expired_vim_insert_input() {
|
||||
snapshot_composer_state_with_width(
|
||||
"vim_enter_after_expired_paste",
|
||||
/*width*/ 60,
|
||||
/*enhanced_keys_supported*/ true,
|
||||
|composer| {
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
composer.draft.textarea.enter_vim_insert_mode();
|
||||
let typed_at = Instant::now() - Duration::from_secs(1);
|
||||
composer.handle_input_basic_with_time(
|
||||
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE),
|
||||
typed_at,
|
||||
);
|
||||
assert_eq!(
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
|
||||
(
|
||||
InputResult::Submitted {
|
||||
text: "x".to_owned(),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
assert_eq!(composer.current_text(), "");
|
||||
},
|
||||
);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ? for shortcuts 100% context left | Vim: Insert "
|
||||
Reference in New Issue
Block a user