diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index a9121a34..c1d231f9 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.86 + - uses: dtolnay/rust-toolchain@1.87 with: components: rustfmt - name: cargo fmt @@ -60,7 +60,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.86 + - uses: dtolnay/rust-toolchain@1.87 with: targets: ${{ matrix.target }} components: clippy diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 7e9e4b46..19060307 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -74,7 +74,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.86 + - uses: dtolnay/rust-toolchain@1.87 with: targets: ${{ matrix.target }} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5cf9dae8..494e3804 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1,4 +1,5 @@ use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use crate::chatwidget::ChatWidget; use crate::git_warning_screen::GitWarningOutcome; use crate::git_warning_screen::GitWarningScreen; @@ -14,7 +15,6 @@ use crossterm::event::KeyEvent; use crossterm::event::MouseEvent; use crossterm::event::MouseEventKind; use std::sync::mpsc::Receiver; -use std::sync::mpsc::Sender; use std::sync::mpsc::channel; /// Top‑level application state – which full‑screen view is currently active. @@ -26,7 +26,7 @@ enum AppState { } pub(crate) struct App<'a> { - app_event_tx: Sender, + app_event_tx: AppEventSender, app_event_rx: Receiver, chat_widget: ChatWidget<'a>, app_state: AppState, @@ -40,6 +40,7 @@ impl App<'_> { initial_images: Vec, ) -> Self { let (app_event_tx, app_event_rx) = channel(); + let app_event_tx = AppEventSender::new(app_event_tx); let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone()); // Spawn a dedicated thread for reading the crossterm event loop and @@ -50,14 +51,10 @@ impl App<'_> { while let Ok(event) = crossterm::event::read() { match event { crossterm::event::Event::Key(key_event) => { - if let Err(e) = app_event_tx.send(AppEvent::KeyEvent(key_event)) { - tracing::error!("failed to send key event: {e}"); - } + app_event_tx.send(AppEvent::KeyEvent(key_event)); } crossterm::event::Event::Resize(_, _) => { - if let Err(e) = app_event_tx.send(AppEvent::Redraw) { - tracing::error!("failed to send resize event: {e}"); - } + app_event_tx.send(AppEvent::Redraw); } crossterm::event::Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollUp, @@ -85,10 +82,7 @@ impl App<'_> { } _ => KeyEvent::new(KeyCode::Char(ch), KeyModifiers::empty()), }; - if let Err(e) = app_event_tx.send(AppEvent::KeyEvent(key_event)) { - tracing::error!("failed to send pasted key event: {e}"); - break; - } + app_event_tx.send(AppEvent::KeyEvent(key_event)); } } _ => { @@ -124,14 +118,14 @@ impl App<'_> { /// Clone of the internal event sender so external tasks (e.g. log bridge) /// can inject `AppEvent`s. - pub fn event_sender(&self) -> Sender { + pub fn event_sender(&self) -> AppEventSender { self.app_event_tx.clone() } pub(crate) fn run(&mut self, terminal: &mut tui::Tui) -> 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::Redraw); while let Ok(event) = self.app_event_rx.recv() { match event { @@ -152,7 +146,7 @@ impl App<'_> { modifiers: crossterm::event::KeyModifiers::CONTROL, .. } => { - self.app_event_tx.send(AppEvent::ExitRequest)?; + self.app_event_tx.send(AppEvent::ExitRequest); } _ => { self.dispatch_key_event(key_event); @@ -175,12 +169,12 @@ impl App<'_> { } AppEvent::LatestLog(line) => { if matches!(self.app_state, AppState::Chat) { - let _ = self.chat_widget.update_latest_log(line); + self.chat_widget.update_latest_log(line); } } AppEvent::DispatchCommand(command) => match command { SlashCommand::Clear => { - let _ = self.chat_widget.clear_conversation_history(); + self.chat_widget.clear_conversation_history(); } SlashCommand::Quit => { break; @@ -210,17 +204,15 @@ impl App<'_> { fn dispatch_key_event(&mut self, key_event: KeyEvent) { match &mut self.app_state { AppState::Chat => { - if let Err(e) = self.chat_widget.handle_key_event(key_event) { - tracing::error!("SendError: {e}"); - } + self.chat_widget.handle_key_event(key_event); } AppState::GitWarning { screen } => match screen.handle_key_event(key_event) { GitWarningOutcome::Continue => { self.app_state = AppState::Chat; - let _ = self.app_event_tx.send(AppEvent::Redraw); + self.app_event_tx.send(AppEvent::Redraw); } GitWarningOutcome::Quit => { - let _ = self.app_event_tx.send(AppEvent::ExitRequest); + self.app_event_tx.send(AppEvent::ExitRequest); } GitWarningOutcome::None => { // do nothing @@ -231,17 +223,13 @@ 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}"); - } + self.chat_widget.handle_scroll_delta(scroll_delta); } } 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) { - tracing::error!("SendError: {e}"); - } + self.chat_widget.handle_codex_event(event); } } } diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs new file mode 100644 index 00000000..9d838273 --- /dev/null +++ b/codex-rs/tui/src/app_event_sender.rs @@ -0,0 +1,22 @@ +use std::sync::mpsc::Sender; + +use crate::app_event::AppEvent; + +#[derive(Clone, Debug)] +pub(crate) struct AppEventSender { + app_event_tx: Sender, +} + +impl AppEventSender { + pub(crate) fn new(app_event_tx: Sender) -> Self { + Self { app_event_tx } + } + + /// Send an event to the app event channel. If it fails, we swallow the + /// error and log it. + pub(crate) fn send(&self, event: AppEvent) { + if let Err(e) = self.app_event_tx.send(event) { + tracing::error!("failed to send event: {e}"); + } + } +} diff --git a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs index 71bc5d5f..ca33047b 100644 --- a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs +++ b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs @@ -1,12 +1,9 @@ -use std::sync::mpsc::SendError; -use std::sync::mpsc::Sender; - use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::widgets::WidgetRef; -use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use crate::user_approval_widget::ApprovalRequest; use crate::user_approval_widget::UserApprovalWidget; @@ -17,11 +14,11 @@ use super::BottomPaneView; pub(crate) struct ApprovalModalView<'a> { current: UserApprovalWidget<'a>, queue: Vec, - app_event_tx: Sender, + app_event_tx: AppEventSender, } impl ApprovalModalView<'_> { - pub fn new(request: ApprovalRequest, app_event_tx: Sender) -> Self { + pub fn new(request: ApprovalRequest, app_event_tx: AppEventSender) -> Self { Self { current: UserApprovalWidget::new(request, app_event_tx.clone()), queue: Vec::new(), @@ -44,14 +41,9 @@ impl ApprovalModalView<'_> { } impl<'a> BottomPaneView<'a> for ApprovalModalView<'a> { - fn handle_key_event( - &mut self, - _pane: &mut BottomPane<'a>, - key_event: KeyEvent, - ) -> Result<(), SendError> { - self.current.handle_key_event(key_event)?; + fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, key_event: KeyEvent) { + self.current.handle_key_event(key_event); self.maybe_advance(); - Ok(()) } fn is_complete(&self) -> bool { diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs index 328319e7..6abf5399 100644 --- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs +++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs @@ -1,10 +1,7 @@ +use crate::user_approval_widget::ApprovalRequest; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use std::sync::mpsc::SendError; - -use crate::app_event::AppEvent; -use crate::user_approval_widget::ApprovalRequest; use super::BottomPane; @@ -18,11 +15,7 @@ pub(crate) enum ConditionalUpdate { pub(crate) trait BottomPaneView<'a> { /// Handle a key event while the view is active. A redraw is always /// scheduled after this call. - fn handle_key_event( - &mut self, - pane: &mut BottomPane<'a>, - key_event: KeyEvent, - ) -> Result<(), SendError>; + fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, _key_event: KeyEvent) {} /// Return `true` if the view has finished and should be removed. fn is_complete(&self) -> bool { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index d68bd91d..b5647137 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -13,9 +13,8 @@ use tui_textarea::Input; use tui_textarea::Key; use tui_textarea::TextArea; -use std::sync::mpsc::Sender; - use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use super::command_popup::CommandPopup; @@ -33,11 +32,11 @@ pub enum InputResult { pub(crate) struct ChatComposer<'a> { textarea: TextArea<'a>, command_popup: Option, - app_event_tx: Sender, + app_event_tx: AppEventSender, } impl ChatComposer<'_> { - pub fn new(has_input_focus: bool, app_event_tx: Sender) -> Self { + pub fn new(has_input_focus: bool, app_event_tx: AppEventSender) -> Self { let mut textarea = TextArea::default(); textarea.set_placeholder_text("send a message"); textarea.set_cursor_line_style(ratatui::style::Style::default()); @@ -113,9 +112,7 @@ impl ChatComposer<'_> { } => { if let Some(cmd) = popup.selected_command() { // Send command to the app layer. - if let Err(e) = self.app_event_tx.send(AppEvent::DispatchCommand(*cmd)) { - tracing::error!("failed to send DispatchCommand event: {e}"); - } + self.app_event_tx.send(AppEvent::DispatchCommand(*cmd)); // Clear textarea so no residual text remains. self.textarea.select_all(); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 33b8b9ea..f73cfd36 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -6,10 +6,9 @@ use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::widgets::WidgetRef; -use std::sync::mpsc::SendError; -use std::sync::mpsc::Sender; use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use crate::user_approval_widget::ApprovalRequest; mod approval_modal_view; @@ -33,13 +32,13 @@ pub(crate) struct BottomPane<'a> { /// If present, this is displayed instead of the `composer`. active_view: Option + 'a>>, - app_event_tx: Sender, + app_event_tx: AppEventSender, has_input_focus: bool, is_task_running: bool, } pub(crate) struct BottomPaneParams { - pub(crate) app_event_tx: Sender, + pub(crate) app_event_tx: AppEventSender, pub(crate) has_input_focus: bool, } @@ -55,12 +54,9 @@ impl BottomPane<'_> { } /// Forward a key event to the active view or the composer. - pub fn handle_key_event( - &mut self, - key_event: KeyEvent, - ) -> Result> { + pub fn handle_key_event(&mut self, key_event: KeyEvent) -> InputResult { if let Some(mut view) = self.active_view.take() { - view.handle_key_event(self, key_event)?; + view.handle_key_event(self, key_event); if !view.is_complete() { self.active_view = Some(view); } else if self.is_task_running { @@ -70,31 +66,30 @@ impl BottomPane<'_> { height, ))); } - self.request_redraw()?; - Ok(InputResult::None) + self.request_redraw(); + InputResult::None } else { let (input_result, needs_redraw) = self.composer.handle_key_event(key_event); if needs_redraw { - self.request_redraw()?; + self.request_redraw(); } - Ok(input_result) + input_result } } /// Update the status indicator text (only when the `StatusIndicatorView` is /// active). - pub(crate) fn update_status_text(&mut self, text: String) -> Result<(), SendError> { + pub(crate) fn update_status_text(&mut self, text: String) { if let Some(view) = &mut self.active_view { match view.update_status_text(text) { ConditionalUpdate::NeedsRedraw => { - self.request_redraw()?; + self.request_redraw(); } ConditionalUpdate::NoRedraw => { // No redraw needed. } } } - Ok(()) } /// Update the UI to reflect whether this `BottomPane` has input focus. @@ -103,7 +98,7 @@ impl BottomPane<'_> { self.composer.set_input_focus(has_focus); } - pub fn set_task_running(&mut self, running: bool) -> Result<(), SendError> { + pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; match (running, self.active_view.is_some()) { @@ -114,13 +109,13 @@ impl BottomPane<'_> { self.app_event_tx.clone(), height, ))); - self.request_redraw()?; + self.request_redraw(); } (false, true) => { if let Some(mut view) = self.active_view.take() { if view.should_hide_when_task_is_done() { // Leave self.active_view as None. - self.request_redraw()?; + self.request_redraw(); } else { // Preserve the view. self.active_view = Some(view); @@ -131,20 +126,16 @@ impl BottomPane<'_> { // No change. } } - Ok(()) } /// Called when the agent requests user approval. - pub fn push_approval_request( - &mut self, - request: ApprovalRequest, - ) -> Result<(), SendError> { + pub fn push_approval_request(&mut self, request: ApprovalRequest) { let request = if let Some(view) = self.active_view.as_mut() { match view.try_consume_approval_request(request) { Some(request) => request, None => { - self.request_redraw()?; - return Ok(()); + self.request_redraw(); + return; } } } else { @@ -166,7 +157,7 @@ impl BottomPane<'_> { } } - pub(crate) fn request_redraw(&self) -> Result<(), SendError> { + pub(crate) fn request_redraw(&self) { self.app_event_tx.send(AppEvent::Redraw) } diff --git a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs index aa353162..d9ac57d7 100644 --- a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs +++ b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs @@ -1,15 +1,10 @@ -use std::sync::mpsc::SendError; -use std::sync::mpsc::Sender; - -use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::widgets::WidgetRef; -use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use crate::status_indicator_widget::StatusIndicatorWidget; -use super::BottomPane; use super::BottomPaneView; use super::bottom_pane_view::ConditionalUpdate; @@ -18,7 +13,7 @@ pub(crate) struct StatusIndicatorView { } impl StatusIndicatorView { - pub fn new(app_event_tx: Sender, height: u16) -> Self { + pub fn new(app_event_tx: AppEventSender, height: u16) -> Self { Self { view: StatusIndicatorWidget::new(app_event_tx, height), } @@ -30,14 +25,6 @@ impl StatusIndicatorView { } impl<'a> BottomPaneView<'a> for StatusIndicatorView { - fn handle_key_event( - &mut self, - _pane: &mut BottomPane<'a>, - _key_event: KeyEvent, - ) -> Result<(), SendError> { - Ok(()) - } - fn update_status_text(&mut self, text: String) -> ConditionalUpdate { self.update_text(text); ConditionalUpdate::NeedsRedraw diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a63f6461..17eb126f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1,7 +1,5 @@ use std::path::PathBuf; use std::sync::Arc; -use std::sync::mpsc::SendError; -use std::sync::mpsc::Sender; use codex_core::codex_wrapper::init_codex; use codex_core::config::Config; @@ -31,6 +29,7 @@ use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::unbounded_channel; use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use crate::bottom_pane::BottomPane; use crate::bottom_pane::BottomPaneParams; use crate::bottom_pane::InputResult; @@ -39,7 +38,7 @@ use crate::history_cell::PatchEventType; use crate::user_approval_widget::ApprovalRequest; pub(crate) struct ChatWidget<'a> { - app_event_tx: Sender, + app_event_tx: AppEventSender, codex_op_tx: UnboundedSender, conversation_history: ConversationHistoryWidget, bottom_pane: BottomPane<'a>, @@ -56,7 +55,7 @@ enum InputFocus { impl ChatWidget<'_> { pub(crate) fn new( config: Config, - app_event_tx: Sender, + app_event_tx: AppEventSender, initial_prompt: Option, initial_images: Vec, ) -> Self { @@ -77,9 +76,7 @@ impl ChatWidget<'_> { // Forward the captured `SessionInitialized` event that was consumed // inside `init_codex()` so it can be rendered in the UI. - if let Err(e) = app_event_tx_clone.send(AppEvent::CodexEvent(session_event.clone())) { - tracing::error!("failed to send SessionInitialized event: {e}"); - } + app_event_tx_clone.send(AppEvent::CodexEvent(session_event.clone())); let codex = Arc::new(codex); let codex_clone = codex.clone(); tokio::spawn(async move { @@ -92,11 +89,7 @@ impl ChatWidget<'_> { }); while let Ok(event) = codex.next_event().await { - app_event_tx_clone - .send(AppEvent::CodexEvent(event)) - .unwrap_or_else(|e| { - tracing::error!("failed to send event: {e}"); - }); + app_event_tx_clone.send(AppEvent::CodexEvent(event)); } }); @@ -114,16 +107,13 @@ impl ChatWidget<'_> { if initial_prompt.is_some() || !initial_images.is_empty() { let text = initial_prompt.unwrap_or_default(); - let _ = chat_widget.submit_user_message_with_images(text, initial_images); + chat_widget.submit_user_message_with_images(text, initial_images); } chat_widget } - pub(crate) fn handle_key_event( - &mut self, - key_event: KeyEvent, - ) -> std::result::Result<(), SendError> { + pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) { // Special-case : normally toggles focus between history and bottom panes. // However, when the slash-command popup is visible we forward the key // to the bottom pane so it can handle auto-completion. @@ -138,43 +128,31 @@ impl ChatWidget<'_> { .set_input_focus(self.input_focus == InputFocus::HistoryPane); self.bottom_pane .set_input_focus(self.input_focus == InputFocus::BottomPane); - self.request_redraw()?; - return Ok(()); + self.request_redraw(); } match self.input_focus { InputFocus::HistoryPane => { let needs_redraw = self.conversation_history.handle_key_event(key_event); if needs_redraw { - self.request_redraw()?; + self.request_redraw(); } - Ok(()) } - InputFocus::BottomPane => { - match self.bottom_pane.handle_key_event(key_event)? { - InputResult::Submitted(text) => { - self.submit_user_message(text)?; - } - InputResult::None => {} + InputFocus::BottomPane => match self.bottom_pane.handle_key_event(key_event) { + InputResult::Submitted(text) => { + self.submit_user_message(text); } - Ok(()) - } + InputResult::None => {} + }, } } - fn submit_user_message( - &mut self, - text: String, - ) -> std::result::Result<(), SendError> { + fn submit_user_message(&mut self, text: String) { // Forward to codex and update conversation history. - self.submit_user_message_with_images(text, vec![]) + self.submit_user_message_with_images(text, vec![]); } - fn submit_user_message_with_images( - &mut self, - text: String, - image_paths: Vec, - ) -> std::result::Result<(), SendError> { + fn submit_user_message_with_images(&mut self, text: String, image_paths: Vec) { let mut items: Vec = Vec::new(); if !text.is_empty() { @@ -186,7 +164,7 @@ impl ChatWidget<'_> { } if items.is_empty() { - return Ok(()); + return; } self.codex_op_tx @@ -200,48 +178,41 @@ impl ChatWidget<'_> { self.conversation_history.add_user_message(text); } self.conversation_history.scroll_to_bottom(); - - Ok(()) } - pub(crate) fn clear_conversation_history( - &mut self, - ) -> std::result::Result<(), SendError> { + pub(crate) fn clear_conversation_history(&mut self) { self.conversation_history.clear(); - self.request_redraw() + self.request_redraw(); } - pub(crate) fn handle_codex_event( - &mut self, - event: Event, - ) -> std::result::Result<(), SendError> { + pub(crate) fn handle_codex_event(&mut self, event: Event) { let Event { id, msg } = event; match msg { EventMsg::SessionConfigured(event) => { // Record session information at the top of the conversation. self.conversation_history .add_session_info(&self.config, event); - self.request_redraw()?; + self.request_redraw(); } EventMsg::AgentMessage(AgentMessageEvent { message }) => { self.conversation_history.add_agent_message(message); - self.request_redraw()?; + self.request_redraw(); } EventMsg::AgentReasoning(AgentReasoningEvent { text }) => { self.conversation_history.add_agent_reasoning(text); - self.request_redraw()?; + self.request_redraw(); } EventMsg::TaskStarted => { - self.bottom_pane.set_task_running(true)?; - self.request_redraw()?; + self.bottom_pane.set_task_running(true); + self.request_redraw(); } EventMsg::TaskComplete => { - self.bottom_pane.set_task_running(false)?; - self.request_redraw()?; + self.bottom_pane.set_task_running(false); + self.request_redraw(); } EventMsg::Error(ErrorEvent { message }) => { self.conversation_history.add_error(message); - self.bottom_pane.set_task_running(false)?; + self.bottom_pane.set_task_running(false); } EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { command, @@ -254,7 +225,7 @@ impl ChatWidget<'_> { cwd, reason, }; - self.bottom_pane.push_approval_request(request)?; + self.bottom_pane.push_approval_request(request); } EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { changes, @@ -283,8 +254,8 @@ impl ChatWidget<'_> { reason, grant_root, }; - self.bottom_pane.push_approval_request(request)?; - self.request_redraw()?; + self.bottom_pane.push_approval_request(request); + self.request_redraw(); } EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id, @@ -293,7 +264,7 @@ impl ChatWidget<'_> { }) => { self.conversation_history .add_active_exec_command(call_id, command); - self.request_redraw()?; + self.request_redraw(); } EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: _, @@ -307,7 +278,7 @@ impl ChatWidget<'_> { if !auto_approved { self.conversation_history.scroll_to_bottom(); } - self.request_redraw()?; + self.request_redraw(); } EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, @@ -317,7 +288,7 @@ impl ChatWidget<'_> { }) => { self.conversation_history .record_completed_exec_command(call_id, stdout, stderr, exit_code); - self.request_redraw()?; + self.request_redraw(); } EventMsg::McpToolCallBegin(McpToolCallBeginEvent { call_id, @@ -327,7 +298,7 @@ impl ChatWidget<'_> { }) => { self.conversation_history .add_active_mcp_tool_call(call_id, server, tool, arguments); - self.request_redraw()?; + self.request_redraw(); } EventMsg::McpToolCallEnd(McpToolCallEndEvent { call_id, @@ -336,36 +307,27 @@ impl ChatWidget<'_> { }) => { self.conversation_history .record_completed_mcp_tool_call(call_id, success, result); - self.request_redraw()?; + self.request_redraw(); } event => { self.conversation_history .add_background_event(format!("{event:?}")); - self.request_redraw()?; + self.request_redraw(); } } - Ok(()) } /// Update the live log preview while a task is running. - pub(crate) fn update_latest_log( - &mut self, - line: String, - ) -> std::result::Result<(), SendError> { + pub(crate) fn update_latest_log(&mut self, line: String) { // Forward only if we are currently showing the status indicator. - self.bottom_pane.update_status_text(line)?; - Ok(()) + self.bottom_pane.update_status_text(line); } - fn request_redraw(&mut self) -> std::result::Result<(), SendError> { - self.app_event_tx.send(AppEvent::Redraw)?; - Ok(()) + fn request_redraw(&mut self) { + self.app_event_tx.send(AppEvent::Redraw); } - pub(crate) fn handle_scroll_delta( - &mut self, - scroll_delta: i32, - ) -> std::result::Result<(), SendError> { + pub(crate) fn handle_scroll_delta(&mut self, scroll_delta: i32) { // 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 { @@ -375,8 +337,7 @@ impl ChatWidget<'_> { scroll_delta * 2 }; self.conversation_history.scroll(magnified_scroll_delta); - self.request_redraw()?; - Ok(()) + self.request_redraw(); } /// Forward an `Op` directly to codex. diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index bee6e1b7..5e3ed9b6 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -16,6 +16,7 @@ use tracing_subscriber::prelude::*; mod app; mod app_event; +mod app_event_sender; mod bottom_pane; mod chatwidget; mod cli; @@ -161,7 +162,7 @@ fn run_ratatui_app( let app_event_tx = app.event_sender(); tokio::spawn(async move { while let Some(line) = log_rx.recv().await { - let _ = app_event_tx.send(crate::app_event::AppEvent::LatestLog(line)); + app_event_tx.send(crate::app_event::AppEvent::LatestLog(line)); } }); } diff --git a/codex-rs/tui/src/scroll_event_helper.rs b/codex-rs/tui/src/scroll_event_helper.rs index c324ef20..ad3ae37e 100644 --- a/codex-rs/tui/src/scroll_event_helper.rs +++ b/codex-rs/tui/src/scroll_event_helper.rs @@ -2,16 +2,16 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; -use std::sync::mpsc::Sender; use tokio::runtime::Handle; use tokio::time::Duration; use tokio::time::sleep; use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; pub(crate) struct ScrollEventHelper { - app_event_tx: Sender, + app_event_tx: AppEventSender, scroll_delta: Arc, timer_scheduled: Arc, runtime: Handle, @@ -26,7 +26,7 @@ const DEBOUNCE_WINDOW: Duration = Duration::from_millis(100); /// window. The debounce timer now runs on Tokio so we avoid spinning up a new /// operating-system thread for every burst. impl ScrollEventHelper { - pub(crate) fn new(app_event_tx: Sender) -> Self { + pub(crate) fn new(app_event_tx: AppEventSender) -> Self { Self { app_event_tx, scroll_delta: Arc::new(AtomicI32::new(0)), @@ -68,7 +68,7 @@ impl ScrollEventHelper { let accumulated = delta.swap(0, Ordering::SeqCst); if accumulated != 0 { - let _ = tx.send(AppEvent::Scroll(accumulated)); + tx.send(AppEvent::Scroll(accumulated)); } timer_flag.store(false, Ordering::SeqCst); diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index b4444512..f9b71a23 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::sync::mpsc::Sender; use std::thread; use std::time::Duration; @@ -26,6 +25,7 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use codex_ansi_escape::ansi_escape_line; @@ -45,12 +45,12 @@ pub(crate) struct StatusIndicatorWidget { // animation thread is still running. The field itself is currently not // accessed anywhere, therefore the leading underscore silences the // `dead_code` warning without affecting behavior. - _app_event_tx: Sender, + _app_event_tx: AppEventSender, } impl StatusIndicatorWidget { /// Create a new status indicator and start the animation timer. - pub(crate) fn new(app_event_tx: Sender, height: u16) -> Self { + pub(crate) fn new(app_event_tx: AppEventSender, height: u16) -> Self { let frame_idx = Arc::new(AtomicUsize::new(0)); let running = Arc::new(AtomicBool::new(true)); @@ -65,9 +65,7 @@ impl StatusIndicatorWidget { std::thread::sleep(Duration::from_millis(200)); counter = counter.wrapping_add(1); frame_idx_clone.store(counter, Ordering::Relaxed); - if app_event_tx_clone.send(AppEvent::Redraw).is_err() { - break; - } + app_event_tx_clone.send(AppEvent::Redraw); } }); } diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index cbfccf19..6604daac 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -7,8 +7,6 @@ //! driven workflow – a fully‑fledged visual match is not required. use std::path::PathBuf; -use std::sync::mpsc::SendError; -use std::sync::mpsc::Sender; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; @@ -30,6 +28,7 @@ use tui_input::Input; use tui_input::backend::crossterm::EventHandler; use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; @@ -48,8 +47,6 @@ pub(crate) enum ApprovalRequest { }, } -// ────────────────────────────────────────────────────────────────────────── - /// Options displayed in the *select* mode. struct SelectOption { label: &'static str, @@ -102,7 +99,7 @@ enum Mode { /// A modal prompting the user to approve or deny the pending request. pub(crate) struct UserApprovalWidget<'a> { approval_request: ApprovalRequest, - app_event_tx: Sender, + app_event_tx: AppEventSender, confirmation_prompt: Paragraph<'a>, /// Currently selected index in *select* mode. @@ -124,7 +121,7 @@ pub(crate) struct UserApprovalWidget<'a> { const BORDER_LINES: u16 = 2; impl UserApprovalWidget<'_> { - pub(crate) fn new(approval_request: ApprovalRequest, app_event_tx: Sender) -> Self { + pub(crate) fn new(approval_request: ApprovalRequest, app_event_tx: AppEventSender) -> Self { let input = Input::default(); let confirmation_prompt = match &approval_request { ApprovalRequest::Exec { @@ -225,15 +222,14 @@ impl UserApprovalWidget<'_> { /// Process a key event originating from crossterm. As the modal fully /// captures input while visible, we don’t need to report whether the event /// was consumed—callers can assume it always is. - pub(crate) fn handle_key_event(&mut self, key: KeyEvent) -> Result<(), SendError> { + pub(crate) fn handle_key_event(&mut self, key: KeyEvent) { match self.mode { - Mode::Select => self.handle_select_key(key)?, - Mode::Input => self.handle_input_key(key)?, + Mode::Select => self.handle_select_key(key), + Mode::Input => self.handle_input_key(key), } - Ok(()) } - fn handle_select_key(&mut self, key_event: KeyEvent) -> Result<(), SendError> { + fn handle_select_key(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Up => { if self.selected_option == 0 { @@ -241,77 +237,61 @@ impl UserApprovalWidget<'_> { } else { self.selected_option -= 1; } - return Ok(()); } KeyCode::Down => { self.selected_option = (self.selected_option + 1) % SELECT_OPTIONS.len(); - return Ok(()); } KeyCode::Char('y') => { - self.send_decision(ReviewDecision::Approved)?; - return Ok(()); + self.send_decision(ReviewDecision::Approved); } KeyCode::Char('a') => { - self.send_decision(ReviewDecision::ApprovedForSession)?; - return Ok(()); + self.send_decision(ReviewDecision::ApprovedForSession); } KeyCode::Char('n') => { - self.send_decision(ReviewDecision::Denied)?; - return Ok(()); + self.send_decision(ReviewDecision::Denied); } KeyCode::Char('e') => { self.mode = Mode::Input; - return Ok(()); } KeyCode::Enter => { let opt = &SELECT_OPTIONS[self.selected_option]; if opt.enters_input_mode { self.mode = Mode::Input; } else if let Some(decision) = opt.decision { - self.send_decision(decision)?; + self.send_decision(decision); } - return Ok(()); } KeyCode::Esc => { - self.send_decision(ReviewDecision::Abort)?; - return Ok(()); + self.send_decision(ReviewDecision::Abort); } _ => {} } - Ok(()) } - fn handle_input_key(&mut self, key_event: KeyEvent) -> Result<(), SendError> { + fn handle_input_key(&mut self, key_event: KeyEvent) { // Handle special keys first. match key_event.code { KeyCode::Enter => { let feedback = self.input.value().to_string(); - self.send_decision_with_feedback(ReviewDecision::Denied, feedback)?; - return Ok(()); + self.send_decision_with_feedback(ReviewDecision::Denied, feedback); } KeyCode::Esc => { // Cancel input – treat as deny without feedback. - self.send_decision(ReviewDecision::Denied)?; - return Ok(()); + self.send_decision(ReviewDecision::Denied); + } + _ => { + // Feed into input widget for normal editing. + let ct_event = crossterm::event::Event::Key(key_event); + self.input.handle_event(&ct_event); } - _ => {} } - - // Feed into input widget for normal editing. - let ct_event = crossterm::event::Event::Key(key_event); - self.input.handle_event(&ct_event); - Ok(()) } - fn send_decision(&mut self, decision: ReviewDecision) -> Result<(), SendError> { + fn send_decision(&mut self, decision: ReviewDecision) { self.send_decision_with_feedback(decision, String::new()) } - fn send_decision_with_feedback( - &mut self, - decision: ReviewDecision, - _feedback: String, - ) -> Result<(), SendError> { + fn send_decision_with_feedback(&mut self, decision: ReviewDecision, _feedback: String) { let op = match &self.approval_request { ApprovalRequest::Exec { id, .. } => Op::ExecApproval { id: id.clone(), @@ -329,9 +309,8 @@ impl UserApprovalWidget<'_> { // redraw after it processes the resulting state change, so we avoid // issuing an extra Redraw here to prevent a transient frame where the // modal is still visible. - self.app_event_tx.send(AppEvent::CodexOp(op))?; + self.app_event_tx.send(AppEvent::CodexOp(op)); self.done = true; - Ok(()) } /// Returns `true` once the user has made a decision and the widget no @@ -339,8 +318,6 @@ impl UserApprovalWidget<'_> { pub(crate) fn is_complete(&self) -> bool { self.done } - - // ────────────────────────────────────────────────────────────────────── } const PLAIN: Style = Style::new();