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