mirror of
https://github.com/openai/codex.git
synced 2026-04-28 16:45:54 +00:00
267 lines
9.1 KiB
Markdown
267 lines
9.1 KiB
Markdown
# PR #1599: Implement redraw debounce
|
|
|
|
- URL: https://github.com/openai/codex/pull/1599
|
|
- Author: aibrahim-oai
|
|
- Created: 2025-07-17 17:18:15 UTC
|
|
- Updated: 2025-07-17 19:55:03 UTC
|
|
- Changes: +49/-7, Files changed: 5, Commits: 6
|
|
|
|
## Description
|
|
|
|
## Summary
|
|
- debouce redraw events so repeated requests don't overwhelm the terminal
|
|
- add `RequestRedraw` event and schedule redraws after 100ms
|
|
|
|
## Testing
|
|
- `cargo clippy --tests`
|
|
- `cargo test` *(fails: Sandbox Denied errors in landlock tests)*
|
|
|
|
------
|
|
https://chatgpt.com/codex/tasks/task_i_68792a65b8b483218ec90a8f68746cd8
|
|
|
|
## Full Diff
|
|
|
|
```diff
|
|
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
|
|
index ac69bef2e9..397c0dc9a4 100644
|
|
--- a/codex-rs/tui/src/app.rs
|
|
+++ b/codex-rs/tui/src/app.rs
|
|
@@ -18,8 +18,15 @@ use crossterm::event::KeyEvent;
|
|
use crossterm::event::MouseEvent;
|
|
use crossterm::event::MouseEventKind;
|
|
use std::path::PathBuf;
|
|
+use std::sync::Arc;
|
|
+use std::sync::Mutex;
|
|
use std::sync::mpsc::Receiver;
|
|
use std::sync::mpsc::channel;
|
|
+use std::thread;
|
|
+use std::time::Duration;
|
|
+
|
|
+/// Time window for debouncing redraw requests.
|
|
+const REDRAW_DEBOUNCE: Duration = Duration::from_millis(100);
|
|
|
|
/// Top-level application state: which full-screen view is currently active.
|
|
#[allow(clippy::large_enum_variant)]
|
|
@@ -46,6 +53,9 @@ pub(crate) struct App<'a> {
|
|
|
|
file_search: FileSearchManager,
|
|
|
|
+ /// True when a redraw has been scheduled but not yet executed.
|
|
+ pending_redraw: Arc<Mutex<bool>>,
|
|
+
|
|
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
|
/// after dismissing the Git-repo warning.
|
|
chat_args: Option<ChatWidgetArgs>,
|
|
@@ -70,6 +80,7 @@ impl App<'_> {
|
|
) -> Self {
|
|
let (app_event_tx, app_event_rx) = channel();
|
|
let app_event_tx = AppEventSender::new(app_event_tx);
|
|
+ let pending_redraw = Arc::new(Mutex::new(false));
|
|
let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone());
|
|
|
|
// Spawn a dedicated thread for reading the crossterm event loop and
|
|
@@ -83,7 +94,7 @@ impl App<'_> {
|
|
app_event_tx.send(AppEvent::KeyEvent(key_event));
|
|
}
|
|
crossterm::event::Event::Resize(_, _) => {
|
|
- app_event_tx.send(AppEvent::Redraw);
|
|
+ app_event_tx.send(AppEvent::RequestRedraw);
|
|
}
|
|
crossterm::event::Event::Mouse(MouseEvent {
|
|
kind: MouseEventKind::ScrollUp,
|
|
@@ -152,6 +163,7 @@ impl App<'_> {
|
|
app_state,
|
|
config,
|
|
file_search,
|
|
+ pending_redraw,
|
|
chat_args,
|
|
}
|
|
}
|
|
@@ -162,6 +174,29 @@ impl App<'_> {
|
|
self.app_event_tx.clone()
|
|
}
|
|
|
|
+ /// Schedule a redraw if one is not already pending.
|
|
+ #[allow(clippy::unwrap_used)]
|
|
+ fn schedule_redraw(&self) {
|
|
+ {
|
|
+ #[allow(clippy::unwrap_used)]
|
|
+ let mut flag = self.pending_redraw.lock().unwrap();
|
|
+ if *flag {
|
|
+ return;
|
|
+ }
|
|
+ *flag = true;
|
|
+ }
|
|
+
|
|
+ let tx = self.app_event_tx.clone();
|
|
+ let pending_redraw = self.pending_redraw.clone();
|
|
+ thread::spawn(move || {
|
|
+ thread::sleep(REDRAW_DEBOUNCE);
|
|
+ tx.send(AppEvent::Redraw);
|
|
+ #[allow(clippy::unwrap_used)]
|
|
+ let mut f = pending_redraw.lock().unwrap();
|
|
+ *f = false;
|
|
+ });
|
|
+ }
|
|
+
|
|
pub(crate) fn run(
|
|
&mut self,
|
|
terminal: &mut tui::Tui,
|
|
@@ -169,10 +204,13 @@ impl App<'_> {
|
|
) -> Result<()> {
|
|
// Insert an event to trigger the first render.
|
|
let app_event_tx = self.app_event_tx.clone();
|
|
- app_event_tx.send(AppEvent::Redraw);
|
|
+ app_event_tx.send(AppEvent::RequestRedraw);
|
|
|
|
while let Ok(event) = self.app_event_rx.recv() {
|
|
match event {
|
|
+ AppEvent::RequestRedraw => {
|
|
+ self.schedule_redraw();
|
|
+ }
|
|
AppEvent::Redraw => {
|
|
self.draw_next_frame(terminal)?;
|
|
}
|
|
@@ -249,7 +287,7 @@ impl App<'_> {
|
|
Vec::new(),
|
|
));
|
|
self.app_state = AppState::Chat { widget: new_widget };
|
|
- self.app_event_tx.send(AppEvent::Redraw);
|
|
+ self.app_event_tx.send(AppEvent::RequestRedraw);
|
|
}
|
|
SlashCommand::ToggleMouseMode => {
|
|
if let Err(e) = mouse_capture.toggle() {
|
|
@@ -336,7 +374,7 @@ impl App<'_> {
|
|
args.initial_images,
|
|
));
|
|
self.app_state = AppState::Chat { widget };
|
|
- self.app_event_tx.send(AppEvent::Redraw);
|
|
+ self.app_event_tx.send(AppEvent::RequestRedraw);
|
|
}
|
|
GitWarningOutcome::Quit => {
|
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
|
diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
|
|
index fd6b2479ee..3aaa789760 100644
|
|
--- a/codex-rs/tui/src/app_event.rs
|
|
+++ b/codex-rs/tui/src/app_event.rs
|
|
@@ -8,6 +8,10 @@ use crate::slash_command::SlashCommand;
|
|
pub(crate) enum AppEvent {
|
|
CodexEvent(Event),
|
|
|
|
+ /// Request a redraw which will be debounced by the [`App`].
|
|
+ RequestRedraw,
|
|
+
|
|
+ /// Actually draw the next frame.
|
|
Redraw,
|
|
|
|
KeyEvent(KeyEvent),
|
|
diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs
|
|
index e4ea1d3823..2a91655cc5 100644
|
|
--- a/codex-rs/tui/src/bottom_pane/mod.rs
|
|
+++ b/codex-rs/tui/src/bottom_pane/mod.rs
|
|
@@ -212,7 +212,7 @@ impl BottomPane<'_> {
|
|
}
|
|
|
|
pub(crate) fn request_redraw(&self) {
|
|
- self.app_event_tx.send(AppEvent::Redraw)
|
|
+ self.app_event_tx.send(AppEvent::RequestRedraw)
|
|
}
|
|
|
|
/// Returns true when a popup inside the composer is visible.
|
|
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
|
|
index 860439ffb6..7c825acd41 100644
|
|
--- a/codex-rs/tui/src/chatwidget.rs
|
|
+++ b/codex-rs/tui/src/chatwidget.rs
|
|
@@ -431,7 +431,7 @@ impl ChatWidget<'_> {
|
|
}
|
|
|
|
fn request_redraw(&mut self) {
|
|
- self.app_event_tx.send(AppEvent::Redraw);
|
|
+ self.app_event_tx.send(AppEvent::RequestRedraw);
|
|
}
|
|
|
|
pub(crate) fn add_diff_output(&mut self, diff_output: String) {
|
|
diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs
|
|
index f9b71a23cb..dda61d0bd0 100644
|
|
--- a/codex-rs/tui/src/status_indicator_widget.rs
|
|
+++ b/codex-rs/tui/src/status_indicator_widget.rs
|
|
@@ -65,7 +65,7 @@ impl StatusIndicatorWidget {
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
counter = counter.wrapping_add(1);
|
|
frame_idx_clone.store(counter, Ordering::Relaxed);
|
|
- app_event_tx_clone.send(AppEvent::Redraw);
|
|
+ app_event_tx_clone.send(AppEvent::RequestRedraw);
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
## Review Comments
|
|
|
|
### codex-rs/tui/src/app.rs
|
|
|
|
- Created: 2025-07-17 18:27:52 UTC | Link: https://github.com/openai/codex/pull/1599#discussion_r2214016700
|
|
|
|
```diff
|
|
@@ -162,17 +174,39 @@ impl<'a> App<'a> {
|
|
self.app_event_tx.clone()
|
|
}
|
|
|
|
+ /// Schedule a redraw if one is not already pending.
|
|
+ #[allow(clippy::unwrap_used)]
|
|
```
|
|
|
|
> Can you move this to just above where you use it like on 180?
|
|
|
|
- Created: 2025-07-17 18:31:36 UTC | Link: https://github.com/openai/codex/pull/1599#discussion_r2214023745
|
|
|
|
```diff
|
|
@@ -162,17 +174,39 @@ impl<'a> App<'a> {
|
|
self.app_event_tx.clone()
|
|
}
|
|
|
|
+ /// Schedule a redraw if one is not already pending.
|
|
+ #[allow(clippy::unwrap_used)]
|
|
+ fn schedule_redraw(&self) {
|
|
+ let mut flag = self.pending_redraw.lock().unwrap();
|
|
+ if *flag {
|
|
+ return;
|
|
+ }
|
|
+ *flag = true;
|
|
+ let tx = self.app_event_tx.clone();
|
|
+ let pending = Arc::clone(&self.pending_redraw);
|
|
+ thread::spawn(move || {
|
|
+ thread::sleep(REDRAW_DEBOUNCE);
|
|
+ tx.send(AppEvent::Redraw);
|
|
+ #[allow(clippy::unwrap_used)]
|
|
+ let mut f = pending.lock().unwrap();
|
|
+ *f = false;
|
|
+ });
|
|
```
|
|
|
|
> This is slightly better because it results in holding the lock for a shorter amount of time.
|
|
>
|
|
> That extra level of scoping around the use of `self.pending_redraw.lock()` and `flag` ensures that after `*flag = true`, the lock is dropped.
|
|
>
|
|
> Also, `Arc::clone()` is less canonical than just invoking `.clone()`, in my experience.
|
|
>
|
|
> ```suggestion
|
|
> {
|
|
> #[allow(clippy::unwrap_used)]
|
|
> let mut flag = self.pending_redraw.lock().unwrap();
|
|
> if *flag {
|
|
> return;
|
|
> }
|
|
> *flag = true;
|
|
> }
|
|
>
|
|
> let tx = self.app_event_tx.clone();
|
|
> let pending_redraw = &self.pending_redraw.clone();
|
|
> thread::spawn(move || {
|
|
> thread::sleep(REDRAW_DEBOUNCE);
|
|
> tx.send(AppEvent::Redraw);
|
|
> #[allow(clippy::unwrap_used)]
|
|
> let mut f = pending.lock().unwrap();
|
|
> *f = false;
|
|
> });
|
|
> ``` |