chore: introduce AppEventSender to help fix clippy warnings and update to Rust 1.87 (#948)

Moving to Rust 1.87 introduced a clippy warning that
`SendError<AppEvent>` was too large.

In practice, the only thing we ever did when we got this error was log
it (if the mspc channel is closed, then the app is likely shutting down
or something, so there's not much to do...), so this finally motivated
me to introduce `AppEventSender`, which wraps
`std::sync::mpsc::Sender<AppEvent>` with a `send()` method that invokes
`send()` on the underlying `Sender` and logs an `Err` if it gets one.

This greatly simplifies the code, as many functions that previously
returned `Result<(), SendError<AppEvent>>` now return `()`, so we don't
have to propagate an `Err` all over the place that we don't really
handle, anyway.

This also makes it so we can upgrade to Rust 1.87 in CI.
This commit is contained in:
Michael Bolin
2025-05-15 14:50:30 -07:00
committed by GitHub
parent ec5e82b77c
commit 3fdf9df133
14 changed files with 149 additions and 242 deletions

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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;
/// Toplevel application state which fullscreen view is currently active.
@@ -26,7 +26,7 @@ enum AppState {
}
pub(crate) struct App<'a> {
app_event_tx: Sender<AppEvent>,
app_event_tx: AppEventSender,
app_event_rx: Receiver<AppEvent>,
chat_widget: ChatWidget<'a>,
app_state: AppState,
@@ -40,6 +40,7 @@ impl App<'_> {
initial_images: Vec<std::path::PathBuf>,
) -> 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<AppEvent> {
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);
}
}
}

View File

@@ -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<AppEvent>,
}
impl AppEventSender {
pub(crate) fn new(app_event_tx: Sender<AppEvent>) -> 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}");
}
}
}

View File

@@ -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<ApprovalRequest>,
app_event_tx: Sender<AppEvent>,
app_event_tx: AppEventSender,
}
impl ApprovalModalView<'_> {
pub fn new(request: ApprovalRequest, app_event_tx: Sender<AppEvent>) -> 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<AppEvent>> {
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 {

View File

@@ -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<AppEvent>>;
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 {

View File

@@ -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<CommandPopup>,
app_event_tx: Sender<AppEvent>,
app_event_tx: AppEventSender,
}
impl ChatComposer<'_> {
pub fn new(has_input_focus: bool, app_event_tx: Sender<AppEvent>) -> 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();

View File

@@ -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<Box<dyn BottomPaneView<'a> + 'a>>,
app_event_tx: Sender<AppEvent>,
app_event_tx: AppEventSender,
has_input_focus: bool,
is_task_running: bool,
}
pub(crate) struct BottomPaneParams {
pub(crate) app_event_tx: Sender<AppEvent>,
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<InputResult, SendError<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
pub(crate) fn request_redraw(&self) {
self.app_event_tx.send(AppEvent::Redraw)
}

View File

@@ -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<AppEvent>, 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<AppEvent>> {
Ok(())
}
fn update_status_text(&mut self, text: String) -> ConditionalUpdate {
self.update_text(text);
ConditionalUpdate::NeedsRedraw

View File

@@ -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<AppEvent>,
app_event_tx: AppEventSender,
codex_op_tx: UnboundedSender<Op>,
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<AppEvent>,
app_event_tx: AppEventSender,
initial_prompt: Option<String>,
initial_images: Vec<PathBuf>,
) -> 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<AppEvent>> {
pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
// Special-case <Tab>: 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<AppEvent>> {
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<PathBuf>,
) -> std::result::Result<(), SendError<AppEvent>> {
fn submit_user_message_with_images(&mut self, text: String, image_paths: Vec<PathBuf>) {
let mut items: Vec<InputItem> = 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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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.

View File

@@ -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));
}
});
}

View File

@@ -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<AppEvent>,
app_event_tx: AppEventSender,
scroll_delta: Arc<AtomicI32>,
timer_scheduled: Arc<AtomicBool>,
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<AppEvent>) -> 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);

View File

@@ -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<AppEvent>,
_app_event_tx: AppEventSender,
}
impl StatusIndicatorWidget {
/// Create a new status indicator and start the animation timer.
pub(crate) fn new(app_event_tx: Sender<AppEvent>, 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);
}
});
}

View File

@@ -7,8 +7,6 @@
//! driven workflow a fullyfledged 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<AppEvent>,
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<AppEvent>) -> 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 dont 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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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<AppEvent>> {
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();