Skip to content
Open
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
19 changes: 9 additions & 10 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ use super::footer::reset_mode_after_activity;
use super::footer::toggle_shortcut_mode;
use super::paste_burst::CharDecision;
use super::paste_burst::PasteBurst;
use crate::bottom_pane::paste_burst::FlushResult;
use crate::bottom_pane::prompt_args::expand_custom_prompt;
use crate::bottom_pane::prompt_args::expand_if_numeric_with_positional_args;
use crate::bottom_pane::prompt_args::parse_slash_name;
Expand Down Expand Up @@ -1047,15 +1046,12 @@ impl ChatComposer {
}

fn handle_paste_burst_flush(&mut self, now: Instant) -> bool {
match self.paste_burst.flush_if_due(now) {
FlushResult::Paste(pasted) => {
self.handle_paste(pasted);
true
}
FlushResult::Typed(ch) => {
if let Some(text) = self.paste_burst.flush_if_due(now) {
// Distinguish between single-char typed input and paste based on length
if text.chars().count() == 1 {
// Mirror insert_str() behavior so popups stay in sync when a
// pending fast char flushes as normal typed input.
self.textarea.insert_str(ch.to_string().as_str());
self.textarea.insert_str(&text);
// Keep popup sync consistent with key handling: prefer slash popup; only
// sync file popup when slash popup is NOT active.
self.sync_command_popup();
Expand All @@ -1064,9 +1060,12 @@ impl ChatComposer {
} else {
self.sync_file_search_popup();
}
true
} else {
self.handle_paste(text);
}
FlushResult::None => false,
true
} else {
false
}
}

Expand Down
35 changes: 20 additions & 15 deletions codex-rs/tui/src/bottom_pane/paste_burst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use std::time::Instant;
// Heuristic thresholds for detecting paste-like input bursts.
// Detect quickly to avoid showing typed prefix before paste is recognized
const PASTE_BURST_MIN_CHARS: u16 = 3;
const PASTE_BURST_CHAR_INTERVAL: Duration = Duration::from_millis(8);
// Use a slightly larger interval to reduce test flakiness on slower machines
// and better reflect typical rapid key repeat rates while still catching
// paste-like bursts quickly.
const PASTE_BURST_CHAR_INTERVAL: Duration = Duration::from_millis(16);
const PASTE_ENTER_SUPPRESS_WINDOW: Duration = Duration::from_millis(120);

#[derive(Default)]
Expand Down Expand Up @@ -35,12 +38,6 @@ pub(crate) struct RetroGrab {
pub grabbed: String,
}

pub(crate) enum FlushResult {
Paste(String),
Typed(char),
None,
}

impl PasteBurst {
/// Recommended delay to wait between simulated keypresses (or before
/// scheduling a UI tick) so that a pending fast keystroke is flushed
Expand All @@ -49,7 +46,9 @@ impl PasteBurst {
/// Primarily used by tests and by the TUI to reliably cross the
/// paste-burst timing threshold.
pub fn recommended_flush_delay() -> Duration {
PASTE_BURST_CHAR_INTERVAL + Duration::from_millis(1)
// Add a small guard band above the interval to account for
// coarse timers and scheduler jitter on slower machines.
PASTE_BURST_CHAR_INTERVAL + Duration::from_millis(8)
}

/// Entry point: decide how to treat a plain char with current timing.
Expand Down Expand Up @@ -101,24 +100,24 @@ impl PasteBurst {
/// now emit that char as normal typed input.
///
/// Returns None if the timeout has not elapsed or there is nothing to flush.
pub fn flush_if_due(&mut self, now: Instant) -> FlushResult {
pub fn flush_if_due(&mut self, now: Instant) -> Option<String> {
let timed_out = self
.last_plain_char_time
.is_some_and(|t| now.duration_since(t) > PASTE_BURST_CHAR_INTERVAL);
if timed_out && self.is_active_internal() {
self.active = false;
let out = std::mem::take(&mut self.buffer);
FlushResult::Paste(out)
Some(out)
} else if timed_out {
// If we were saving a single fast char and no burst followed,
// flush it as normal typed input.
if let Some((ch, _at)) = self.pending_first_char.take() {
FlushResult::Typed(ch)
Some(ch.to_string())
} else {
FlushResult::None
None
}
} else {
FlushResult::None
None
}
}

Expand Down Expand Up @@ -182,8 +181,14 @@ impl PasteBurst {
) -> Option<RetroGrab> {
let start_byte = retro_start_index(before, retro_chars);
let grabbed = before[start_byte..].to_string();
let looks_pastey =
grabbed.chars().any(char::is_whitespace) || grabbed.chars().count() >= 16;
// Consider it paste-like if content obviously looks like a paste
// (contains whitespace or is long), OR if we saw a rapid sequence of
// at least two prior chars (retro_chars >= 2). The latter ensures that
// fast bursts starting from an empty buffer remain hidden until flush,
// matching UX expectations and tests.
let looks_pastey = grabbed.chars().any(char::is_whitespace)
|| grabbed.chars().count() >= 16
|| retro_chars >= 2;
if looks_pastey {
// Note: caller is responsible for removing this slice from UI text.
self.begin_with_retro_grabbed(grabbed.clone(), now);
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/tui/src/terminal_palette.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use crate::color::perceptual_distance;
use ratatui::style::Color;

/// When false, disables terminal color requery on focus to prevent OSC
/// query responses from bleeding into the input stream.
const ENABLE_COLOR_REQUERY: bool = false;

/// Returns the closest color to the target color that the terminal can display.
pub fn best_color(target: (u8, u8, u8)) -> Color {
let Some(color_level) = supports_color::on_cached(supports_color::Stream::Stdout) else {
Expand All @@ -26,7 +30,9 @@ pub fn best_color(target: (u8, u8, u8)) -> Color {
}

pub fn requery_default_colors() {
imp::requery_default_colors();
if ENABLE_COLOR_REQUERY {
imp::requery_default_colors();
}
}

#[derive(Clone, Copy)]
Expand Down
Loading