chore: introduce AppEventSender to help fix clippy warnings and update to Rust 1.87

This commit is contained in:
Michael Bolin
2025-05-15 14:24:22 -07:00
parent ec5e82b77c
commit ffb04a92c9
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();