Compare commits

...

1 Commits

Author SHA1 Message Date
Ahmed Ibrahim
97415e6493 Decouple paste burst timeout from draw
Shift paste-burst flushing to a timer-driven app event so multiline pastes no longer rely on draw polling to exit burst mode.

Co-authored-by: Codex <noreply@openai.com>
2026-03-03 12:04:32 -08:00
5 changed files with 57 additions and 36 deletions

View File

@@ -1899,12 +1899,6 @@ impl App {
self.render_transcript_once(tui);
}
self.chat_widget.maybe_post_pending_notification(tui);
if self
.chat_widget
.handle_paste_burst_tick(tui.frame_requester())
{
return Ok(AppRunControl::Continue);
}
// Allow widgets to process any pending timers before rendering.
self.chat_widget.pre_draw_tick();
tui.draw(
@@ -2151,6 +2145,9 @@ impl App {
AppEvent::CommitTick => {
self.chat_widget.on_commit_tick();
}
AppEvent::PasteBurstTimeoutTick { token } => {
self.chat_widget.handle_paste_burst_timeout_tick(token);
}
AppEvent::CodexEvent(event) => {
self.enqueue_primary_event(event).await?;
}

View File

@@ -174,6 +174,9 @@ pub(crate) enum AppEvent {
StartCommitAnimation,
StopCommitAnimation,
CommitTick,
PasteBurstTimeoutTick {
token: u64,
},
/// Update the current reasoning effort in the running app and widget.
UpdateReasoningEffort(Option<ReasoningEffort>),

View File

@@ -370,7 +370,7 @@ impl BottomPane {
// We need three pieces of information after routing the key:
// whether Esc completed the view, whether the view finished for any
// reason, and whether a paste-burst timer should be scheduled.
let (ctrl_c_completed, view_complete, view_in_paste_burst) = {
let (ctrl_c_completed, view_complete, _view_in_paste_burst) = {
let last_index = self.view_stack.len() - 1;
let view = &mut self.view_stack[last_index];
let prefer_esc =
@@ -390,16 +390,9 @@ impl BottomPane {
if ctrl_c_completed {
self.view_stack.pop();
self.on_active_view_complete();
if let Some(next_view) = self.view_stack.last()
&& next_view.is_in_paste_burst()
{
self.request_redraw_in(ChatComposer::recommended_paste_flush_delay());
}
} else if view_complete {
self.view_stack.clear();
self.on_active_view_complete();
} else if view_in_paste_burst {
self.request_redraw_in(ChatComposer::recommended_paste_flush_delay());
}
self.request_redraw();
InputResult::None
@@ -430,9 +423,6 @@ impl BottomPane {
if needs_redraw {
self.request_redraw();
}
if self.composer.is_in_paste_burst() {
self.request_redraw_in(ChatComposer::recommended_paste_flush_delay());
}
input_result
}
}
@@ -928,10 +918,6 @@ impl BottomPane {
self.frame_requester.schedule_frame();
}
pub(crate) fn request_redraw_in(&self, dur: Duration) {
self.frame_requester.schedule_frame_in(dur);
}
// --- History helpers ---
pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) {

View File

@@ -157,6 +157,7 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::Wrap;
use tokio::sync::mpsc::UnboundedSender;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::debug;
use tracing::warn;
@@ -212,6 +213,7 @@ use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::BottomPane;
use crate::bottom_pane::BottomPaneParams;
use crate::bottom_pane::CancellationEvent;
use crate::bottom_pane::ChatComposer;
use crate::bottom_pane::CollaborationModeIndicator;
use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::DOUBLE_PRESS_QUIT_SHORTCUT_ENABLED;
@@ -609,6 +611,8 @@ pub(crate) struct ChatWidget {
thread_name: Option<String>,
forked_from: Option<ThreadId>,
frame_requester: FrameRequester,
paste_burst_timeout_tick_generation: u64,
pending_paste_burst_timeout_tick: Option<u64>,
// Whether to include the initial welcome banner on session configured
show_welcome_banner: bool,
// One-shot tooltip override for the primary startup session.
@@ -2916,6 +2920,8 @@ impl ChatWidget {
forked_from: None,
queued_user_messages: VecDeque::new(),
queued_message_edit_binding,
paste_burst_timeout_tick_generation: 0,
pending_paste_burst_timeout_tick: None,
show_welcome_banner: is_first_run,
startup_tooltip_override,
suppress_session_configured_redraw: false,
@@ -3100,6 +3106,8 @@ impl ChatWidget {
plan_item_active: false,
queued_user_messages: VecDeque::new(),
queued_message_edit_binding,
paste_burst_timeout_tick_generation: 0,
pending_paste_burst_timeout_tick: None,
show_welcome_banner: is_first_run,
startup_tooltip_override,
suppress_session_configured_redraw: false,
@@ -3265,6 +3273,8 @@ impl ChatWidget {
forked_from: None,
queued_user_messages: VecDeque::new(),
queued_message_edit_binding,
paste_burst_timeout_tick_generation: 0,
pending_paste_burst_timeout_tick: None,
show_welcome_banner: false,
startup_tooltip_override: None,
suppress_session_configured_redraw: true,
@@ -3485,6 +3495,8 @@ impl ChatWidget {
InputResult::None => {}
},
}
self.schedule_paste_burst_timeout_if_needed();
}
/// Attach a local image to the composer when the active model supports image inputs.
@@ -4032,24 +4044,45 @@ impl ChatWidget {
pub(crate) fn handle_paste(&mut self, text: String) {
self.bottom_pane.handle_paste(text);
self.schedule_paste_burst_timeout_if_needed();
}
// Returns true if caller should skip rendering this frame (a future frame is scheduled).
pub(crate) fn handle_paste_burst_tick(&mut self, frame_requester: FrameRequester) -> bool {
if self.bottom_pane.flush_paste_burst_if_due() {
// A paste just flushed; request an immediate redraw and skip this frame.
self.request_redraw();
true
} else if self.bottom_pane.is_in_paste_burst() {
// While capturing a burst, schedule a follow-up tick and skip this frame
// to avoid redundant renders between ticks.
frame_requester.schedule_frame_in(
crate::bottom_pane::ChatComposer::recommended_paste_flush_delay(),
);
true
} else {
false
pub(crate) fn handle_paste_burst_timeout_tick(&mut self, token: u64) {
if self.pending_paste_burst_timeout_tick != Some(token) {
return;
}
self.pending_paste_burst_timeout_tick = None;
if !self.bottom_pane.is_in_paste_burst() {
return;
}
if self.bottom_pane.flush_paste_burst_if_due() {
self.request_redraw();
}
self.schedule_paste_burst_timeout_if_needed();
}
fn schedule_paste_burst_timeout_if_needed(&mut self) {
if !self.bottom_pane.is_in_paste_burst() {
self.pending_paste_burst_timeout_tick = None;
return;
}
if self.pending_paste_burst_timeout_tick.is_some() {
return;
}
self.paste_burst_timeout_tick_generation =
self.paste_burst_timeout_tick_generation.wrapping_add(1);
let token = self.paste_burst_timeout_tick_generation;
self.pending_paste_burst_timeout_tick = Some(token);
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
sleep(ChatComposer::recommended_paste_flush_delay()).await;
app_event_tx.send(AppEvent::PasteBurstTimeoutTick { token });
});
}
fn flush_active_cell(&mut self) {

View File

@@ -1717,6 +1717,8 @@ async fn make_chatwidget_manual(
thread_name: None,
forked_from: None,
frame_requester: FrameRequester::test_dummy(),
paste_burst_timeout_tick_generation: 0,
pending_paste_burst_timeout_tick: None,
show_welcome_banner: true,
startup_tooltip_override: None,
queued_user_messages: VecDeque::new(),