From 93de2b6d7eb450debb58284e9f5002a543fa3b08 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 29 Apr 2026 01:38:50 -0700 Subject: [PATCH 1/3] Route app events through TUI commands --- codex-rs/tui/src/app/config_persistence.rs | 8 +- codex-rs/tui/src/app/event_dispatch.rs | 6 +- codex-rs/tui/src/app/tests.rs | 5 +- codex-rs/tui/src/app_command.rs | 28 ++++++ codex-rs/tui/src/app_event.rs | 6 +- codex-rs/tui/src/app_event_sender.rs | 40 ++++---- .../tui/src/bottom_pane/approval_overlay.rs | 27 +++-- .../src/bottom_pane/chat_composer_history.rs | 18 ++-- codex-rs/tui/src/bottom_pane/mod.rs | 14 +-- .../src/bottom_pane/request_user_input/mod.rs | 21 ++-- codex-rs/tui/src/chatwidget.rs | 99 +++++++++---------- codex-rs/tui/src/chatwidget/tests.rs | 1 + .../src/chatwidget/tests/approval_requests.rs | 2 +- .../tui/src/chatwidget/tests/exec_flow.rs | 6 +- codex-rs/tui/src/chatwidget/tests/guardian.rs | 2 +- .../tui/src/chatwidget/tests/permissions.rs | 6 +- .../tui/src/chatwidget/tests/review_mode.rs | 4 +- .../src/chatwidget/tests/slash_commands.rs | 16 +-- 18 files changed, 161 insertions(+), 148 deletions(-) diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 3b5479f1d53d..b40c7fb2b43f 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -351,8 +351,8 @@ impl App { #[cfg(target_os = "windows")] { let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); - self.app_event_tx.send(AppEvent::CodexOp( - AppCommand::override_turn_context( + self.app_event_tx + .send(AppEvent::CodexOp(AppCommand::override_turn_context( /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, @@ -365,9 +365,7 @@ impl App { /*service_tier*/ None, /*collaboration_mode*/ None, /*personality*/ None, - ) - .into_core(), - )); + ))); } } diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index d4002f2453af..787d85e62efa 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -989,8 +989,7 @@ impl App { /*service_tier*/ None, /*collaboration_mode*/ None, /*personality*/ None, - ) - .into(), + ), )); self.app_event_tx.send( AppEvent::OpenWorldWritableWarningConfirmation { @@ -1015,8 +1014,7 @@ impl App { /*service_tier*/ None, /*collaboration_mode*/ None, /*personality*/ None, - ) - .into(), + ), )); self.app_event_tx .send(AppEvent::UpdateAskForApprovalPolicy(preset.approval)); diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 29f82ab85d41..eca648251c9b 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -6,6 +6,7 @@ use super::*; use crate::app_backtrack::BacktrackSelection; use crate::app_backtrack::BacktrackState; use crate::app_backtrack::user_count; +use crate::app_command::AppCommand; use crate::chatwidget::ChatWidgetInit; use crate::chatwidget::create_initial_user_message; @@ -441,12 +442,12 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm } AppEvent::SubmitThreadOp { thread_id: op_thread_id, - op: Op::UserTurn { items, .. }, + op: AppCommand::UserTurn { items, .. }, } => { assert_eq!(op_thread_id, thread_id); submitted_items = Some(items); } - AppEvent::CodexOp(Op::UserTurn { items, .. }) => { + AppEvent::CodexOp(AppCommand::UserTurn { items, .. }) => { submitted_items = Some(items); } _ => {} diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 4e0b07157a90..d60da69d1d77 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use codex_config::types::ApprovalsReviewer; use codex_protocol::approvals::ElicitationAction; +use codex_protocol::approvals::GuardianAssessmentEvent; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; @@ -104,6 +105,9 @@ pub(crate) enum AppCommand { Review { review_request: ReviewRequest, }, + ApproveGuardianDeniedAction { + event: GuardianAssessmentEvent, + }, Other(Op), } @@ -186,6 +190,9 @@ pub(crate) enum AppCommandView<'a> { Review { review_request: &'a ReviewRequest, }, + ApproveGuardianDeniedAction { + event: &'a GuardianAssessmentEvent, + }, Other(&'a Op), } @@ -450,6 +457,9 @@ impl AppCommand { Self::Shutdown => Op::Shutdown, Self::ThreadRollback { num_turns } => Op::ThreadRollback { num_turns }, Self::Review { review_request } => Op::Review { review_request }, + Self::ApproveGuardianDeniedAction { event } => { + Op::ApproveGuardianDeniedAction { event } + } Self::Other(op) => op, } } @@ -568,6 +578,9 @@ impl AppCommand { num_turns: *num_turns, }, Self::Review { review_request } => AppCommandView::Review { review_request }, + Self::ApproveGuardianDeniedAction { event } => { + AppCommandView::ApproveGuardianDeniedAction { event } + } Self::Other(op) => AppCommandView::Other(op), } } @@ -716,11 +729,26 @@ impl From for AppCommand { Op::Shutdown => Self::Shutdown, Op::ThreadRollback { num_turns } => Self::ThreadRollback { num_turns }, Op::Review { review_request } => Self::Review { review_request }, + Op::ApproveGuardianDeniedAction { event } => { + Self::ApproveGuardianDeniedAction { event } + } op => Self::Other(op), } } } +impl PartialEq for AppCommand { + fn eq(&self, other: &Op) -> bool { + self.clone().into_core() == *other + } +} + +impl PartialEq for Op { + fn eq(&self, other: &AppCommand) -> bool { + *self == other.clone().into_core() + } +} + impl From<&Op> for AppCommand { fn from(value: &Op) -> Self { Self::from(value.clone()) diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 65943ee4875d..4c351266ffe7 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -27,11 +27,11 @@ use codex_file_search::FileMatch; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; use codex_protocol::protocol::GetHistoryEntryResponseEvent; -use codex_protocol::protocol::Op; use codex_protocol::protocol::RateLimitSnapshot; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_approval_presets::ApprovalPreset; +use crate::app_command::AppCommand; use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::TerminalTitleItem; @@ -132,7 +132,7 @@ pub(crate) enum AppEvent { /// Submit an op to the specified thread, regardless of current focus. SubmitThreadOp { thread_id: ThreadId, - op: Op, + op: AppCommand, }, /// Deliver a synthetic history lookup response to a specific thread channel. @@ -182,7 +182,7 @@ pub(crate) enum AppEvent { /// Forward an `Op` to the Agent. Using an `AppEvent` for this avoids /// bubbling channels through layers of widgets. - CodexOp(Op), + CodexOp(AppCommand), /// Approve one retry of a recent auto-review denial selected in the TUI. ApproveRecentAutoReviewDenial { diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs index af95fd7decbd..e5398232208b 100644 --- a/codex-rs/tui/src/app_event_sender.rs +++ b/codex-rs/tui/src/app_event_sender.rs @@ -38,48 +38,45 @@ impl AppEventSender { } pub(crate) fn interrupt(&self) { - self.send(AppEvent::CodexOp(AppCommand::interrupt().into_core())); + self.send(AppEvent::CodexOp(AppCommand::interrupt())); } pub(crate) fn compact(&self) { - self.send(AppEvent::CodexOp(AppCommand::compact().into_core())); + self.send(AppEvent::CodexOp(AppCommand::compact())); } pub(crate) fn set_thread_name(&self, name: String) { - self.send(AppEvent::CodexOp( - AppCommand::set_thread_name(name).into_core(), - )); + self.send(AppEvent::CodexOp(AppCommand::set_thread_name(name))); } pub(crate) fn review(&self, review_request: ReviewRequest) { - self.send(AppEvent::CodexOp( - AppCommand::review(review_request).into_core(), - )); + self.send(AppEvent::CodexOp(AppCommand::review(review_request))); } pub(crate) fn list_skills(&self, cwds: Vec, force_reload: bool) { - self.send(AppEvent::CodexOp( - AppCommand::list_skills(cwds, force_reload).into_core(), - )); + self.send(AppEvent::CodexOp(AppCommand::list_skills( + cwds, + force_reload, + ))); } #[cfg_attr(target_os = "linux", allow(dead_code))] pub(crate) fn realtime_conversation_audio(&self, params: ConversationAudioParams) { - self.send(AppEvent::CodexOp( - AppCommand::realtime_conversation_audio(params).into_core(), - )); + self.send(AppEvent::CodexOp(AppCommand::realtime_conversation_audio( + params, + ))); } pub(crate) fn user_input_answer(&self, id: String, response: RequestUserInputResponse) { - self.send(AppEvent::CodexOp( - AppCommand::user_input_answer(id, response).into_core(), - )); + self.send(AppEvent::CodexOp(AppCommand::user_input_answer( + id, response, + ))); } pub(crate) fn exec_approval(&self, thread_id: ThreadId, id: String, decision: ReviewDecision) { self.send(AppEvent::SubmitThreadOp { thread_id, - op: AppCommand::exec_approval(id, /*turn_id*/ None, decision).into_core(), + op: AppCommand::exec_approval(id, /*turn_id*/ None, decision), }); } @@ -91,14 +88,14 @@ impl AppEventSender { ) { self.send(AppEvent::SubmitThreadOp { thread_id, - op: AppCommand::request_permissions_response(id, response).into_core(), + op: AppCommand::request_permissions_response(id, response), }); } pub(crate) fn patch_approval(&self, thread_id: ThreadId, id: String, decision: ReviewDecision) { self.send(AppEvent::SubmitThreadOp { thread_id, - op: AppCommand::patch_approval(id, decision).into_core(), + op: AppCommand::patch_approval(id, decision), }); } @@ -113,8 +110,7 @@ impl AppEventSender { ) { self.send(AppEvent::SubmitThreadOp { thread_id, - op: AppCommand::resolve_elicitation(server_name, request_id, decision, content, meta) - .into_core(), + op: AppCommand::resolve_elicitation(server_name, request_id, decision, content, meta), }); } } diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 123bd9e4ec5c..e8b9e2c06b16 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -46,8 +46,6 @@ use codex_protocol::protocol::ElicitationAction; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::NetworkApprovalContext; use codex_protocol::protocol::NetworkPolicyRuleAction; -#[cfg(test)] -use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -1017,6 +1015,7 @@ fn elicitation_options(keymap: &ApprovalKeymap) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::app_command::AppCommand; use crate::app_event::AppEvent; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; @@ -1166,7 +1165,7 @@ mod tests { let mut decision = None; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { decision: d, .. }, + op: AppCommand::ExecApproval { decision: d, .. }, .. } = ev { @@ -1197,7 +1196,7 @@ mod tests { let mut decision = None; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ResolveElicitation { decision: d, .. }, + op: AppCommand::ResolveElicitation { decision: d, .. }, .. } = ev { @@ -1250,7 +1249,7 @@ mod tests { let mut saw_denied = false; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { decision, .. }, + op: AppCommand::ExecApproval { decision, .. }, .. } = ev { @@ -1298,7 +1297,7 @@ mod tests { let mut saw_deny = false; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { decision, .. }, + op: AppCommand::ExecApproval { decision, .. }, .. } = ev { @@ -1461,7 +1460,7 @@ mod tests { let mut saw_op = false; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { decision, .. }, + op: AppCommand::ExecApproval { decision, .. }, .. } = ev { @@ -1706,7 +1705,7 @@ mod tests { let mut saw_op = false; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::RequestPermissionsResponse { response, .. }, + op: AppCommand::RequestPermissionsResponse { response, .. }, .. } = ev { @@ -1741,7 +1740,7 @@ mod tests { let mut saw_op = false; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::RequestPermissionsResponse { response, .. }, + op: AppCommand::RequestPermissionsResponse { response, .. }, .. } = ev { @@ -1769,7 +1768,7 @@ mod tests { let mut saw_op = false; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::RequestPermissionsResponse { response, .. }, + op: AppCommand::RequestPermissionsResponse { response, .. }, .. } = ev { @@ -2032,7 +2031,7 @@ mod tests { let mut decision = None; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ResolveElicitation { decision: d, .. }, + op: AppCommand::ResolveElicitation { decision: d, .. }, .. } = ev { @@ -2066,7 +2065,7 @@ mod tests { let mut esc_decision = None; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ResolveElicitation { decision, .. }, + op: AppCommand::ResolveElicitation { decision, .. }, .. } = ev { @@ -2096,7 +2095,7 @@ mod tests { let mut n_decision = None; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ResolveElicitation { decision, .. }, + op: AppCommand::ResolveElicitation { decision, .. }, .. } = ev { @@ -2122,7 +2121,7 @@ mod tests { let mut decision = None; while let Ok(ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { decision: d, .. }, + op: AppCommand::ExecApproval { decision: d, .. }, .. } = ev { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs index 22f4a16a6464..1db113749c85 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs @@ -15,6 +15,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; +use crate::app_command::AppCommand; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::MentionBinding; @@ -598,10 +599,9 @@ impl ChatComposerHistory { boundary_if_exhausted, }); } - app_event_tx.send(AppEvent::CodexOp(Op::GetHistoryEntryRequest { - offset, - log_id, - })); + app_event_tx.send(AppEvent::CodexOp(AppCommand::from( + Op::GetHistoryEntryRequest { offset, log_id }, + ))); return HistorySearchResult::Pending; } @@ -719,10 +719,12 @@ impl ChatComposerHistory { self.last_history_text = Some(entry.text.clone()); return Some(entry); } else if let Some(log_id) = self.history_log_id { - app_event_tx.send(AppEvent::CodexOp(Op::GetHistoryEntryRequest { - offset: global_idx, - log_id, - })); + app_event_tx.send(AppEvent::CodexOp(AppCommand::from( + Op::GetHistoryEntryRequest { + offset: global_idx, + log_id, + }, + ))); } None } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index d3274e96dd90..02275755b78b 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -1567,12 +1567,12 @@ impl Renderable for BottomPane { mod tests { use super::*; use crate::app::app_server_requests::ResolvedAppServerRequest; + use crate::app_command::AppCommand; use crate::app_event::AppEvent; use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES; use crate::status_indicator_widget::StatusDetailsCapitalization; use crate::test_support::PathBufExt; use crate::test_support::test_path_buf; - use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SkillScope; use crossterm::event::KeyCode; @@ -1890,7 +1890,7 @@ mod tests { let mut approval_decision = None; while let Ok(event) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { decision, .. }, + op: AppCommand::ExecApproval { decision, .. }, .. } = event { @@ -2371,7 +2371,7 @@ mod tests { while let Ok(ev) = rx.try_recv() { assert!( - !matches!(ev, AppEvent::CodexOp(Op::Interrupt)), + !matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)), "expected Esc to not send Op::Interrupt when dismissing skill popup" ); } @@ -2409,7 +2409,7 @@ mod tests { while let Ok(ev) = rx.try_recv() { assert!( - !matches!(ev, AppEvent::CodexOp(Op::Interrupt)), + !matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)), "expected Esc to not send Op::Interrupt while command popup is active" ); } @@ -2445,7 +2445,7 @@ mod tests { while let Ok(ev) = rx.try_recv() { assert!( - !matches!(ev, AppEvent::CodexOp(Op::Interrupt)), + !matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)), "expected Esc to not send Op::Interrupt while typing `/agent`" ); } @@ -2490,7 +2490,7 @@ mod tests { while let Ok(ev) = rx.try_recv() { assert!( - !matches!(ev, AppEvent::CodexOp(Op::Interrupt)), + !matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)), "expected Esc release after dismissing agent picker to not interrupt" ); } @@ -2520,7 +2520,7 @@ mod tests { pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert!( - matches!(rx.try_recv(), Ok(AppEvent::CodexOp(Op::Interrupt))), + matches!(rx.try_recv(), Ok(AppEvent::CodexOp(AppCommand::Interrupt))), "expected Esc to send Op::Interrupt while a task is running" ); } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index c11b5c1ed5f3..bbbfe1f0bc0e 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -1308,6 +1308,7 @@ impl BottomPaneView for RequestUserInputOverlay { #[cfg(test)] mod tests { use super::*; + use crate::app_command::AppCommand; use crate::app_event::AppEvent; use crate::bottom_pane::selection_popup_common::menu_surface_inset; use crate::render::renderable::Renderable; @@ -1688,7 +1689,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { id, response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { id, response, .. }) = event else { panic!("expected UserInputAnswer"); }; assert_eq!(id, "turn-1"); @@ -1710,7 +1711,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -1746,7 +1747,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let mut expected = HashMap::new(); @@ -1779,7 +1780,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Char('2'))); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2029,7 +2030,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2329,7 +2330,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2354,7 +2355,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2397,7 +2398,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2433,7 +2434,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2518,7 +2519,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fda2dd1f62f5..0cb372769d9c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -8313,22 +8313,19 @@ impl ChatWidget { let default_effort: ReasoningEffortConfig = preset.default_reasoning_effort; let switch_actions: Vec = vec![Box::new(move |tx| { - tx.send(AppEvent::CodexOp( - AppCommand::override_turn_context( - /*cwd*/ None, - /*approval_policy*/ None, - /*approvals_reviewer*/ None, - /*permission_profile*/ None, - /*windows_sandbox_level*/ None, - Some(switch_model_for_events.clone()), - Some(Some(default_effort)), - /*summary*/ None, - /*service_tier*/ None, - /*collaboration_mode*/ None, - /*personality*/ None, - ) - .into_core(), - )); + tx.send(AppEvent::CodexOp(AppCommand::override_turn_context( + /*cwd*/ None, + /*approval_policy*/ None, + /*approvals_reviewer*/ None, + /*permission_profile*/ None, + /*windows_sandbox_level*/ None, + Some(switch_model_for_events.clone()), + Some(Some(default_effort)), + /*summary*/ None, + /*service_tier*/ None, + /*collaboration_mode*/ None, + /*personality*/ None, + ))); tx.send(AppEvent::UpdateModel(switch_model_for_events.clone())); tx.send(AppEvent::UpdateReasoningEffort(Some(default_effort))); })]; @@ -8537,22 +8534,19 @@ impl ChatWidget { let name = Self::personality_label(personality).to_string(); let description = Some(Self::personality_description(personality).to_string()); let actions: Vec = vec![Box::new(move |tx| { - tx.send(AppEvent::CodexOp( - AppCommand::override_turn_context( - /*cwd*/ None, - /*approval_policy*/ None, - /*approvals_reviewer*/ None, - /*permission_profile*/ None, - /*windows_sandbox_level*/ None, - /*model*/ None, - /*effort*/ None, - /*summary*/ None, - /*service_tier*/ None, - /*collaboration_mode*/ None, - Some(personality), - ) - .into_core(), - )); + tx.send(AppEvent::CodexOp(AppCommand::override_turn_context( + /*cwd*/ None, + /*approval_policy*/ None, + /*approvals_reviewer*/ None, + /*permission_profile*/ None, + /*windows_sandbox_level*/ None, + /*model*/ None, + /*effort*/ None, + /*summary*/ None, + /*service_tier*/ None, + /*collaboration_mode*/ None, + Some(personality), + ))); tx.send(AppEvent::UpdatePersonality(personality)); tx.send(AppEvent::PersistPersonalitySelection { personality }); })]; @@ -9567,7 +9561,7 @@ impl ChatWidget { self.app_event_tx.send(AppEvent::SubmitThreadOp { thread_id, - op: Op::ApproveGuardianDeniedAction { event }, + op: AppCommand::from(Op::ApproveGuardianDeniedAction { event }), }); self.add_info_message( "Approval recorded for one retry of the selected auto-review denial.".to_string(), @@ -9605,22 +9599,19 @@ impl ChatWidget { ) -> Vec { vec![Box::new(move |tx| { let permission_profile_clone = permission_profile.clone(); - tx.send(AppEvent::CodexOp( - AppCommand::override_turn_context( - /*cwd*/ None, - Some(approval), - Some(approvals_reviewer), - Some(permission_profile_clone.clone()), - /*windows_sandbox_level*/ None, - /*model*/ None, - /*effort*/ None, - /*summary*/ None, - /*service_tier*/ None, - /*collaboration_mode*/ None, - /*personality*/ None, - ) - .into_core(), - )); + tx.send(AppEvent::CodexOp(AppCommand::override_turn_context( + /*cwd*/ None, + Some(approval), + Some(approvals_reviewer), + Some(permission_profile_clone.clone()), + /*windows_sandbox_level*/ None, + /*model*/ None, + /*effort*/ None, + /*summary*/ None, + /*service_tier*/ None, + /*collaboration_mode*/ None, + /*personality*/ None, + ))); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); tx.send(AppEvent::UpdatePermissionProfile(permission_profile_clone)); tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); @@ -10457,8 +10448,8 @@ impl ChatWidget { self.config.notices.fast_default_opt_out = Some(true); } self.set_service_tier(service_tier); - self.app_event_tx.send(AppEvent::CodexOp( - AppCommand::override_turn_context( + self.app_event_tx + .send(AppEvent::CodexOp(AppCommand::override_turn_context( /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, @@ -10470,9 +10461,7 @@ impl ChatWidget { Some(service_tier), /*collaboration_mode*/ None, /*personality*/ None, - ) - .into_core(), - )); + ))); self.app_event_tx .send(AppEvent::PersistServiceTierSelection { service_tier }); } @@ -11539,7 +11528,7 @@ impl ChatWidget { } } CodexOpTarget::AppEvent => { - self.app_event_tx.send(AppEvent::CodexOp(op.into())); + self.app_event_tx.send(AppEvent::CodexOp(op)); } } true diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index eb7a9f1248f2..c1cbeae61325 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -5,6 +5,7 @@ //! changes show up as stable, reviewable diffs. pub(super) use super::*; +pub(super) use crate::app_command::AppCommand; pub(super) use crate::app_event::AppEvent; pub(super) use crate::app_event::ExitMode; #[cfg(not(target_os = "linux"))] diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index 2e5c51307c2a..3a57207845fd 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -217,7 +217,7 @@ async fn exec_approval_uses_approval_id_when_present() { let mut found = false; while let Ok(app_ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { id, decision, .. }, + op: AppCommand::ExecApproval { id, decision, .. }, .. } = app_ev { diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index d3d5de1f1229..b63912a591c7 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -114,7 +114,7 @@ async fn exec_approval_uses_approval_id_when_present() { let mut found = false; while let Ok(app_ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::ExecApproval { id, decision, .. }, + op: AppCommand::ExecApproval { id, decision, .. }, .. } = app_ev { @@ -1712,7 +1712,7 @@ async fn apply_patch_approval_sends_op_with_call_id() { let mut found = false; while let Ok(app_ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { - op: Op::PatchApproval { id, decision }, + op: AppCommand::PatchApproval { id, decision }, .. } = app_ev { @@ -1751,7 +1751,7 @@ async fn apply_patch_full_flow_integration_like() { let mut maybe_op: Option = None; while let Ok(app_ev) = rx.try_recv() { if let AppEvent::SubmitThreadOp { op, .. } = app_ev { - maybe_op = Some(op); + maybe_op = Some(op.into_core()); break; } } diff --git a/codex-rs/tui/src/chatwidget/tests/guardian.rs b/codex-rs/tui/src/chatwidget/tests/guardian.rs index ea5a193f003e..e8957ff0ecca 100644 --- a/codex-rs/tui/src/chatwidget/tests/guardian.rs +++ b/codex-rs/tui/src/chatwidget/tests/guardian.rs @@ -52,7 +52,7 @@ async fn approving_recent_denial_emits_structured_core_op_once() { rx.try_recv(), Ok(AppEvent::SubmitThreadOp { thread_id: submitted_thread_id, - op: Op::ApproveGuardianDeniedAction { event } + op: AppCommand::ApproveGuardianDeniedAction { event } }) if submitted_thread_id == thread_id && event.id == "auto-review-recent-1" && event.status == GuardianAssessmentStatus::Denied diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 829fa77f5de9..30c40fe01c5e 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -315,7 +315,7 @@ async fn approvals_popup_navigation_skips_disabled() { assert!( app_events.iter().any(|ev| matches!( ev, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::CodexOp(AppCommand::OverrideTurnContext { approval_policy: Some(AskForApproval::OnRequest), personality: None, .. @@ -326,7 +326,7 @@ async fn approvals_popup_navigation_skips_disabled() { assert!( !app_events.iter().any(|ev| matches!( ev, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::CodexOp(AppCommand::OverrideTurnContext { approval_policy: Some(AskForApproval::Never), personality: None, .. @@ -697,7 +697,7 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context let op = std::iter::from_fn(|| rx.try_recv().ok()) .find_map(|event| match event { - AppEvent::CodexOp(op @ Op::OverrideTurnContext { .. }) => Some(op), + AppEvent::CodexOp(op @ AppCommand::OverrideTurnContext { .. }) => Some(op), _ => None, }) .expect("expected OverrideTurnContext op"); diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index 8061b89fc1b6..720b17c1859b 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -1221,10 +1221,10 @@ async fn custom_prompt_submit_sends_review_op() { chat.handle_paste(" please audit dependencies ".to_string()); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - // Expect AppEvent::CodexOp(Op::Review { .. }) with trimmed prompt + // Expect AppEvent::CodexOp(AppCommand::Review { .. }) with trimmed prompt let evt = rx.try_recv().expect("expected one app event"); match evt { - AppEvent::CodexOp(Op::Review { review_request }) => { + AppEvent::CodexOp(AppCommand::Review { review_request }) => { assert_eq!( review_request, ReviewRequest { diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 339021e2d1c2..54d45c848128 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -51,7 +51,7 @@ async fn slash_compact_eagerly_queues_follow_up_before_turn_start() { assert!(chat.bottom_pane.is_task_running()); match rx.try_recv() { - Ok(AppEvent::CodexOp(Op::Compact)) => {} + Ok(AppEvent::CodexOp(AppCommand::Compact)) => {} other => panic!("expected compact op to be submitted, got {other:?}"), } @@ -103,7 +103,7 @@ async fn queued_slash_compact_dispatches_after_active_turn() { assert!( events .iter() - .any(|event| matches!(event, AppEvent::CodexOp(Op::Compact))), + .any(|event| matches!(event, AppEvent::CodexOp(AppCommand::Compact))), "expected queued /compact to submit compact op; events: {events:?}" ); } @@ -446,7 +446,7 @@ async fn queued_bare_rename_drains_next_input_after_name_update() { assert!( events.iter().any(|event| matches!( event, - AppEvent::CodexOp(Op::SetThreadName { name }) if name == "Queued rename" + AppEvent::CodexOp(AppCommand::SetThreadName { name }) if name == "Queued rename" )), "expected rename prompt to submit thread name; events: {events:?}" ); @@ -500,7 +500,7 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() { assert!( events.iter().any(|event| matches!( event, - AppEvent::CodexOp(Op::SetThreadName { name }) if name == "Queued rename" + AppEvent::CodexOp(AppCommand::SetThreadName { name }) if name == "Queued rename" )), "expected queued /rename to submit thread name; events: {events:?}" ); @@ -1137,7 +1137,7 @@ async fn slash_rename_prefills_existing_thread_name() { assert_matches!( rx.try_recv(), - Ok(AppEvent::CodexOp(Op::SetThreadName { name })) if name == "Current project title" + Ok(AppEvent::CodexOp(AppCommand::SetThreadName { name })) if name == "Current project title" ); } @@ -2056,7 +2056,7 @@ async fn fast_slash_command_updates_and_persists_local_service_tier() { assert!( events.iter().any(|event| matches!( event, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::CodexOp(AppCommand::OverrideTurnContext { service_tier: Some(Some(ServiceTier::Fast)), .. }) @@ -2128,7 +2128,7 @@ async fn queued_fast_slash_applies_before_next_queued_message() { assert!( events.iter().any(|event| matches!( event, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::CodexOp(AppCommand::OverrideTurnContext { service_tier: Some(Some(ServiceTier::Fast)), .. }) @@ -2167,7 +2167,7 @@ async fn user_turn_sends_standard_override_after_fast_is_turned_off() { assert!( events.iter().any(|event| matches!( event, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::CodexOp(AppCommand::OverrideTurnContext { service_tier: Some(None), .. }) From 4e1aa0c969d024621a5f227e52db78da02a084c1 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 29 Apr 2026 02:00:28 -0700 Subject: [PATCH 2/3] codex: address PR review feedback (#20174) --- codex-rs/tui/src/app/thread_routing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 7538209cd4a8..5957f552e277 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -706,7 +706,8 @@ impl App { Ok(true) } AppCommandView::OverrideTurnContext { .. } => Ok(true), - AppCommandView::Other(Op::ApproveGuardianDeniedAction { event }) => { + AppCommandView::ApproveGuardianDeniedAction { event } + | AppCommandView::Other(Op::ApproveGuardianDeniedAction { event }) => { app_server .thread_approve_guardian_denied_action(thread_id, event) .await?; From f27d4aa864518045d54b385504715218737e6b94 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 29 Apr 2026 02:15:58 -0700 Subject: [PATCH 3/3] codex: fix CI failure on PR #20174 --- codex-rs/tui/src/app/event_dispatch.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 787d85e62efa..62cb7271596e 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -313,15 +313,14 @@ impl App { return Ok(AppRunControl::Exit(ExitReason::Fatal(message))); } AppEvent::CodexOp(op) => { - self.submit_active_thread_op(app_server, op.into()).await?; + self.submit_active_thread_op(app_server, op).await?; } AppEvent::ApproveRecentAutoReviewDenial { thread_id, id } => { self.chat_widget .approve_recent_auto_review_denial(thread_id, id); } AppEvent::SubmitThreadOp { thread_id, op } => { - self.submit_thread_op(app_server, thread_id, op.into()) - .await?; + self.submit_thread_op(app_server, thread_id, op).await?; } AppEvent::ThreadHistoryEntryResponse { thread_id, event } => { self.enqueue_thread_history_entry_response(thread_id, event)