From 6a9c9f4b6c9549ac63749a8cd0708a9dd71ae22e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 24 Apr 2025 18:05:56 -0700 Subject: [PATCH 1/3] fix: add RUST_BACKTRACE=full when running `cargo test` in CI (#638) This should provide more information in the event of a failure. --- .github/workflows/rust-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index d4efca0e28..0bc3ee0ccd 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -32,7 +32,7 @@ jobs: run: cargo fmt -- --config imports_granularity=Item --check || echo "FAILED=${FAILED:+$FAILED, }cargo fmt" >> $GITHUB_ENV - name: cargo test - run: cargo test || echo "FAILED=${FAILED:+$FAILED, }cargo test" >> $GITHUB_ENV + run: RUST_BACKTRACE=full cargo test || echo "FAILED=${FAILED:+$FAILED, }cargo test" >> $GITHUB_ENV - name: cargo clippy run: cargo clippy --all-features -- -D warnings || echo "FAILED=${FAILED:+$FAILED, }cargo clippy" >> $GITHUB_ENV @@ -75,7 +75,7 @@ jobs: run: cargo fmt -- --config imports_granularity=Item --check || echo "FAILED=${FAILED:+$FAILED, }cargo fmt" >> $GITHUB_ENV - name: cargo test - run: cargo test || echo "FAILED=${FAILED:+$FAILED, }cargo test" >> $GITHUB_ENV + run: RUST_BACKTRACE=full cargo test || echo "FAILED=${FAILED:+$FAILED, }cargo test" >> $GITHUB_ENV - name: cargo clippy run: cargo clippy --all-features -- -D warnings || echo "FAILED=${FAILED:+$FAILED, }cargo clippy" >> $GITHUB_ENV From bfe6fac463474d8a0583fc03364eb9a08bd764c7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 24 Apr 2025 18:06:08 -0700 Subject: [PATCH 2/3] fix: close stdin when running an exec tool call (#636) We were already doing this in the TypeScript version, but forgot to bring this over to Rust: https://github.com/openai/codex/blob/c38c2a59c72504a72a537123dc4b6026875cdfac/codex-cli/src/utils/agent/sandbox/raw-exec.ts#L76-L78 --- codex-rs/core/src/exec.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index fe6bad548e..ae83dc84e7 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -206,9 +206,17 @@ pub async fn exec( if let Some(dir) = &workdir { cmd.current_dir(dir); } - cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); - cmd.kill_on_drop(true); - cmd.spawn()? + + // Do not create a file descriptor for stdin because otherwise some + // commands may hang forever waiting for input. For example, ripgrep has + // a heuristic where it may try to read from stdin as explained here: + // https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103 + cmd.stdin(Stdio::null()); + + cmd.stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true) + .spawn()? }; let stdout_handle = tokio::spawn(read_capped( From ea4557f9631e7829c84fd4205135fc14cb99d596 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 24 Apr 2025 20:10:52 -0700 Subject: [PATCH 3/3] feat(tui-rs): add support for mousewheel scrolling --- codex-rs/tui/src/app.rs | 34 ++++++++++++++++--- codex-rs/tui/src/app_event.rs | 4 +++ codex-rs/tui/src/chatwidget.rs | 17 ++++++++++ .../tui/src/conversation_history_widget.rs | 31 +++++++++++------ codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/tui.rs | 4 +++ 6 files changed, 77 insertions(+), 14 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 9aba46ec8f..26a7074b5b 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2,6 +2,7 @@ use crate::app_event::AppEvent; use crate::chatwidget::ChatWidget; use crate::git_warning_screen::GitWarningOutcome; use crate::git_warning_screen::GitWarningScreen; +use crate::scroll_event_helper::ScrollEventHelper; use crate::tui; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; @@ -10,6 +11,8 @@ use codex_core::protocol::SandboxPolicy; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; +use crossterm::event::MouseEvent; +use crossterm::event::MouseEventKind; use std::sync::mpsc::channel; use std::sync::mpsc::Receiver; use std::sync::mpsc::Sender; @@ -39,6 +42,7 @@ impl App<'_> { model: Option, ) -> Self { let (app_event_tx, app_event_rx) = channel(); + let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone()); // Spawn a dedicated thread for reading the crossterm event loop and // re-publishing the events as AppEvents, as appropriate. @@ -49,10 +53,21 @@ impl App<'_> { let app_event = match event { crossterm::event::Event::Key(key_event) => AppEvent::KeyEvent(key_event), crossterm::event::Event::Resize(_, _) => AppEvent::Redraw, - crossterm::event::Event::FocusGained - | crossterm::event::Event::FocusLost - | crossterm::event::Event::Mouse(_) - | crossterm::event::Event::Paste(_) => { + crossterm::event::Event::Mouse(MouseEvent { + kind: MouseEventKind::ScrollUp, + .. + }) => { + scroll_event_helper.scroll_up(); + continue; + } + crossterm::event::Event::Mouse(MouseEvent { + kind: MouseEventKind::ScrollDown, + .. + }) => { + scroll_event_helper.scroll_down(); + continue; + } + _ => { continue; } }; @@ -125,6 +140,9 @@ impl App<'_> { } }; } + AppEvent::Scroll(scroll_delta) => { + self.dispatch_scroll_event(scroll_delta); + } AppEvent::CodexEvent(event) => { self.dispatch_codex_event(event); } @@ -184,6 +202,14 @@ impl App<'_> { } } + fn dispatch_scroll_event(&mut self, scroll_delta: i32) { + if matches!(self.app_state, AppState::Chat) { + if let Err(e) = self.chat_widget.handle_scroll_delta(scroll_delta) { + tracing::error!("SendError: {e}"); + } + } + } + fn dispatch_codex_event(&mut self, event: Event) { if matches!(self.app_state, AppState::Chat) { if let Err(e) = self.chat_widget.handle_codex_event(event) { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index bb8efb8e15..932ad9c65c 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -3,7 +3,11 @@ use crossterm::event::KeyEvent; pub(crate) enum AppEvent { CodexEvent(Event), + + Scroll(i32), + Redraw, + KeyEvent(KeyEvent), /// Request to exit the application gracefully. ExitRequest, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 149cea42c4..64cb896d93 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -364,6 +364,23 @@ impl ChatWidget<'_> { Ok(()) } + pub(crate) fn handle_scroll_delta( + &mut self, + scroll_delta: i32, + ) -> std::result::Result<(), std::sync::mpsc::SendError> { + // If the user is trying to scroll exactly one line, we let them, but + // otherwise we assume they are trying to scroll in larger increments. + let magnified_scroll_delta = if scroll_delta == 1 { + 1 + } else { + // Play with this: perhaps it should be non-linear? + scroll_delta * 2 + }; + self.conversation_history.scroll(magnified_scroll_delta); + self.request_redraw()?; + Ok(()) + } + /// Forward an `Op` directly to codex. pub(crate) fn submit_op(&self, op: Op) { if let Err(e) = self.codex_op_tx.send(op) { diff --git a/codex-rs/tui/src/conversation_history_widget.rs b/codex-rs/tui/src/conversation_history_widget.rs index c8f6906169..27b5e9b3cf 100644 --- a/codex-rs/tui/src/conversation_history_widget.rs +++ b/codex-rs/tui/src/conversation_history_widget.rs @@ -40,11 +40,11 @@ impl ConversationHistoryWidget { pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) -> bool { match key_event.code { KeyCode::Up | KeyCode::Char('k') => { - self.scroll_up(); + self.scroll_up(1); true } KeyCode::Down | KeyCode::Char('j') => { - self.scroll_down(); + self.scroll_down(1); true } KeyCode::PageUp | KeyCode::Char('b') | KeyCode::Char('u') | KeyCode::Char('U') => { @@ -59,9 +59,18 @@ impl ConversationHistoryWidget { } } - fn scroll_up(&mut self) { - // If a user is scrolling up from the "stick to bottom" mode, we - // need to scroll them back such that they move just one line up. + /// Negative delta scrolls up; positive delta scrolls down. + pub(crate) fn scroll(&mut self, delta: i32) { + match delta.cmp(&0) { + std::cmp::Ordering::Less => self.scroll_up(-delta as u32), + std::cmp::Ordering::Greater => self.scroll_down(delta as u32), + std::cmp::Ordering::Equal => {} + } + } + + fn scroll_up(&mut self, num_lines: u32) { + // If a user is scrolling up from the "stick to bottom" mode, we need to + // map this to a specific scroll position so we can caluate the delta. // This requires us to care about how tall the screen is. if self.scroll_position == usize::MAX { self.scroll_position = self @@ -70,24 +79,26 @@ impl ConversationHistoryWidget { .saturating_sub(self.last_viewport_height.get()); } - self.scroll_position = self.scroll_position.saturating_sub(1); + self.scroll_position = self.scroll_position.saturating_sub(num_lines as usize); } - fn scroll_down(&mut self) { + fn scroll_down(&mut self, num_lines: u32) { // If we're already pinned to the bottom there's nothing to do. if self.scroll_position == usize::MAX { return; } let viewport_height = self.last_viewport_height.get().max(1); - let num_lines = self.num_rendered_lines.get(); + let num_rendered_lines = self.num_rendered_lines.get(); // Compute the maximum explicit scroll offset that still shows a full // viewport. This mirrors the calculation in `scroll_page_down()` and // in the render path. - let max_scroll = num_lines.saturating_sub(viewport_height).saturating_add(1); + let max_scroll = num_rendered_lines + .saturating_sub(viewport_height) + .saturating_add(1); - let new_pos = self.scroll_position.saturating_add(1); + let new_pos = self.scroll_position.saturating_add(num_lines as usize); if new_pos >= max_scroll { // Reached (or passed) the bottom – switch to stick‑to‑bottom mode diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 598d3eaf1b..7361663bb4 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -21,6 +21,7 @@ mod exec_command; mod git_warning_screen; mod history_cell; mod log_layer; +mod scroll_event_helper; mod status_indicator_widget; mod tui; mod user_approval_widget; diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 0753dcb07a..8cc54460a4 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -2,6 +2,8 @@ use std::io::stdout; use std::io::Stdout; use std::io::{self}; +use crossterm::event::DisableMouseCapture; +use crossterm::event::EnableMouseCapture; use ratatui::backend::CrosstermBackend; use ratatui::crossterm::execute; use ratatui::crossterm::terminal::disable_raw_mode; @@ -16,6 +18,7 @@ pub type Tui = Terminal>; /// Initialize the terminal pub fn init() -> io::Result { execute!(stdout(), EnterAlternateScreen)?; + execute!(stdout(), EnableMouseCapture)?; enable_raw_mode()?; set_panic_hook(); Terminal::new(CrosstermBackend::new(stdout())) @@ -31,6 +34,7 @@ fn set_panic_hook() { /// Restore the terminal to its original state pub fn restore() -> io::Result<()> { + execute!(stdout(), DisableMouseCapture)?; execute!(stdout(), LeaveAlternateScreen)?; disable_raw_mode()?; Ok(())