From a5d018ead5dd6a446093075706a02c4e201be570 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 29 Apr 2026 00:25:53 -0700 Subject: [PATCH] Remove legacy ChatWidget test event shims --- codex-rs/tui/src/app/config_persistence.rs | 16 +- .../tui/src/app/pending_interactive_replay.rs | 6 +- codex-rs/tui/src/app/tests.rs | 57 +- codex-rs/tui/src/app_backtrack.rs | 4 +- codex-rs/tui/src/chatwidget.rs | 490 +---------- codex-rs/tui/src/chatwidget/realtime.rs | 12 - ...e_final_message_are_rendered_snapshot.snap | 4 +- .../tui/src/chatwidget/status_surfaces.rs | 6 +- codex-rs/tui/src/chatwidget/test_events.rs | 309 ------- codex-rs/tui/src/chatwidget/tests.rs | 29 - .../src/chatwidget/tests/approval_requests.rs | 26 +- .../src/chatwidget/tests/background_events.rs | 18 - .../chatwidget/tests/composer_submission.rs | 140 ++- .../tui/src/chatwidget/tests/exec_flow.rs | 393 +++------ codex-rs/tui/src/chatwidget/tests/guardian.rs | 323 +++---- codex-rs/tui/src/chatwidget/tests/helpers.rs | 780 +++++++++++++++-- .../src/chatwidget/tests/history_replay.rs | 522 ++++------- .../tui/src/chatwidget/tests/mcp_startup.rs | 10 +- .../tui/src/chatwidget/tests/permissions.rs | 76 +- .../tui/src/chatwidget/tests/plan_mode.rs | 125 +-- .../chatwidget/tests/popups_and_settings.rs | 12 +- .../tui/src/chatwidget/tests/review_mode.rs | 376 ++------ .../src/chatwidget/tests/slash_commands.rs | 571 +++--------- .../src/chatwidget/tests/status_and_layout.rs | 814 +++++++----------- .../src/chatwidget/tests/terminal_title.rs | 20 +- codex-rs/tui/src/chatwidget/user_messages.rs | 2 +- codex-rs/tui/src/status_indicator_widget.rs | 5 - codex-rs/tui/src/token_usage.rs | 7 - codex-rs/tui/src/turn_state.rs | 2 - 29 files changed, 1794 insertions(+), 3361 deletions(-) delete mode 100644 codex-rs/tui/src/chatwidget/test_events.rs delete mode 100644 codex-rs/tui/src/chatwidget/tests/background_events.rs diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 18da9a4b05..a106d37d5d 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -547,9 +547,6 @@ mod tests { use super::*; use crate::app::test_support::app_enabled_in_effective_config; use crate::app::test_support::make_test_app; - use crate::chatwidget::test_events::Event; - use crate::chatwidget::test_events::EventMsg; - use crate::chatwidget::test_events::SessionConfiguredEvent; use crate::test_support::PathBufExt; use codex_protocol::models::PermissionProfile; use pretty_assertions::assert_eq; @@ -635,11 +632,11 @@ mod tests { let next_cwd_tmp = tempdir()?; let next_cwd = next_cwd_tmp.path().to_path_buf(); - app.chat_widget.handle_codex_event(Event { - id: String::new(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: ThreadId::new(), + app.chat_widget + .handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), @@ -648,14 +645,13 @@ mod tests { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: next_cwd.clone().abs(), + instruction_source_paths: Vec::new(), reasoning_effort: None, history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(PathBuf::new()), - }), - }); + }); assert_eq!(app.chat_widget.config_ref().cwd.to_path_buf(), next_cwd); assert_eq!(app.config.cwd, original_cwd); diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index 3c7c6be9fd..cc1a049a05 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -32,9 +32,9 @@ impl ElicitationRequestKey { // - buffer eviction (`note_evicted_event`) // // We keep both fast lookup sets (for snapshot filtering by call_id/request key) and -// turn-indexed queues/vectors so `TurnComplete`/`TurnAborted` can clear stale prompts tied -// to a turn. `request_user_input` removal is FIFO because the overlay answers queued prompts -// in FIFO order for a shared `turn_id`. +// turn-indexed queues/vectors so turn completion or interruption can clear +// stale prompts tied to a turn. `request_user_input` removal is FIFO because +// the overlay answers queued prompts in FIFO order for a shared `turn_id`. pub(super) struct PendingInteractiveReplayState { exec_approval_call_ids: HashSet, exec_approval_call_ids_by_turn_id: HashMap>, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 1b549f9576..962b8b7f79 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -22,9 +22,6 @@ use crate::multi_agents::AgentPickerThreadEntry; use assert_matches::assert_matches; use crate::app_command::AppCommand as Op; -use crate::chatwidget::test_events::Event; -use crate::chatwidget::test_events::EventMsg; -use crate::chatwidget::test_events::SessionConfiguredEvent; use crate::diff_model::FileChange; use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; @@ -3176,6 +3173,7 @@ async fn side_thread_snapshot_hides_forked_parent_transcript() { let mut store = ThreadEventStore::new(/*capacity*/ 4); let session = ThreadSessionState { forked_from_id: Some(parent_thread_id), + fork_parent_title: None, ..test_thread_session(side_thread_id, test_path_buf("/tmp/side")) }; let parent_turn = test_turn( @@ -3238,6 +3236,7 @@ async fn side_thread_snapshot_skips_session_header_preamble() { let snapshot = ThreadEventSnapshot { session: Some(ThreadSessionState { forked_from_id: Some(parent_thread_id), + fork_parent_title: None, ..test_thread_session(side_thread_id, test_path_buf("/tmp/side")) }), turns: Vec::new(), @@ -4350,11 +4349,11 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { assert_eq!(user_count(&app.transcript_cells), 2); let base_id = ThreadId::new(); - app.chat_widget.handle_codex_event(Event { - id: String::new(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: base_id, + app.chat_widget + .handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: base_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), @@ -4363,14 +4362,13 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: None, history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(PathBuf::new()), - }), - }); + }); app.backtrack.base_id = Some(base_id); app.backtrack.primed = true; @@ -4443,11 +4441,11 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() { let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await; let thread_id = ThreadId::new(); - app.chat_widget.handle_codex_event(Event { - id: String::new(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: thread_id, + app.chat_widget + .handle_thread_session(crate::session_state::ThreadSessionState { + thread_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), @@ -4456,14 +4454,13 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: None, history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(PathBuf::new()), - }), - }); + }); let data_image_url = "data:image/png;base64,abc123".to_string(); app.transcript_cells = vec![Arc::new(UserHistoryCell { @@ -4684,6 +4681,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() { )]; let resumed_session = ThreadSessionState { cwd: test_path_buf("/tmp/refreshed").abs(), + instruction_source_paths: Vec::new(), ..initial_session.clone() }; let mut snapshot = ThreadEventSnapshot { @@ -4831,9 +4829,10 @@ async fn new_session_requests_shutdown_for_previous_conversation() { let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await; let thread_id = ThreadId::new(); - let event = SessionConfiguredEvent { - session_id: thread_id, + let event = crate::session_state::ThreadSessionState { + thread_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), @@ -4842,18 +4841,15 @@ async fn new_session_requests_shutdown_for_previous_conversation() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: None, history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(PathBuf::new()), }; - app.chat_widget.handle_codex_event(Event { - id: String::new(), - msg: EventMsg::SessionConfigured(event), - }); + app.chat_widget.handle_thread_session(event); while app_event_rx.try_recv().is_ok() {} while op_rx.try_recv().is_ok() {} @@ -4941,11 +4937,11 @@ async fn interrupt_without_active_turn_is_treated_as_handled() { async fn clear_only_ui_reset_preserves_chat_session_state() { let mut app = make_test_app().await; let thread_id = ThreadId::new(); - app.chat_widget.handle_codex_event(Event { - id: String::new(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: thread_id, + app.chat_widget + .handle_thread_session(crate::session_state::ThreadSessionState { + thread_id, forked_from_id: None, + fork_parent_title: None, thread_name: Some("keep me".to_string()), model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), @@ -4954,14 +4950,13 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/tmp/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: None, history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(PathBuf::new()), - }), - }); + }); app.chat_widget .apply_external_edit("draft prompt".to_string()); app.transcript_cells = vec![Arc::new(UserHistoryCell { diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index a12918c848..231e5e9cb0 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -497,8 +497,8 @@ impl App { self.backtrack.pending_rollback = None; } - /// Apply rollback semantics for `ThreadRolledBack` events where this TUI does not have an - /// in-flight backtrack request (`pending_rollback` is `None`). + /// Apply rollback semantics for a confirmed rollback where this TUI does + /// not have an in-flight backtrack request (`pending_rollback` is `None`). /// /// Returns `true` when local transcript state changed. pub(crate) fn apply_non_pending_thread_rollback(&mut self, num_turns: u32) -> bool { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d42d7f6cee..fb40d641e0 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -42,44 +42,6 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; -#[cfg(test)] -use self::test_events::AgentMessageDeltaEvent; -#[cfg(test)] -use self::test_events::AgentMessageEvent; -#[cfg(test)] -use self::test_events::AgentReasoningDeltaEvent; -#[cfg(test)] -use self::test_events::AgentReasoningEvent; -#[cfg(test)] -use self::test_events::AgentReasoningRawContentDeltaEvent; -#[cfg(test)] -use self::test_events::AgentReasoningRawContentEvent; -#[cfg(test)] -use self::test_events::BackgroundEventEvent; -#[cfg(test)] -use self::test_events::ErrorEvent; -#[cfg(test)] -use self::test_events::Event; -#[cfg(test)] -use self::test_events::EventMsg; -#[cfg(test)] -use self::test_events::ExitedReviewModeEvent; -#[cfg(test)] -use self::test_events::McpListToolsResponseEvent; -#[cfg(test)] -use self::test_events::SessionConfiguredEvent; -#[cfg(test)] -use self::test_events::StreamErrorEvent; -#[cfg(test)] -use self::test_events::TurnCompleteEvent; -#[cfg(test)] -use self::test_events::TurnDiffEvent; -#[cfg(test)] -use self::test_events::UndoCompletedEvent; -#[cfg(test)] -use self::test_events::UndoStartedEvent; -#[cfg(test)] -use self::test_events::WarningEvent; use crate::app::app_server_requests::ResolvedAppServerRequest; use crate::app_command::AppCommand; use crate::app_event::HistoryLookupResponse; @@ -143,8 +105,6 @@ use codex_app_server_protocol::AddCreditsNudgeCreditType; use codex_app_server_protocol::AddCreditsNudgeEmailStatus; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppSummary; -#[cfg(test)] -use codex_app_server_protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo; use codex_app_server_protocol::CollabAgentTool; use codex_app_server_protocol::CollabAgentToolCallStatus; @@ -158,8 +118,6 @@ use codex_app_server_protocol::GuardianApprovalReviewAction; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::McpServerStatusDetail; -#[cfg(test)] -use codex_app_server_protocol::ModelVerification as CoreModelVerification; use codex_app_server_protocol::ModelVerification as AppServerModelVerification; use codex_app_server_protocol::RateLimitReachedType; use codex_app_server_protocol::RateLimitSnapshot; @@ -649,18 +607,6 @@ enum RateLimitErrorKind { Generic, } -#[cfg(test)] -fn core_rate_limit_error_kind(info: &CoreCodexErrorInfo) -> Option { - match info { - CoreCodexErrorInfo::ServerOverloaded => Some(RateLimitErrorKind::ServerOverloaded), - CoreCodexErrorInfo::UsageLimitExceeded => Some(RateLimitErrorKind::UsageLimit), - CoreCodexErrorInfo::ResponseTooManyFailedAttempts { - http_status_code: Some(429), - } => Some(RateLimitErrorKind::Generic), - _ => None, - } -} - fn app_server_rate_limit_error_kind(info: &AppServerCodexErrorInfo) -> Option { match info { AppServerCodexErrorInfo::ServerOverloaded => Some(RateLimitErrorKind::ServerOverloaded), @@ -672,11 +618,6 @@ fn app_server_rate_limit_error_kind(info: &AppServerCodexErrorInfo) -> Option bool { - matches!(info, CoreCodexErrorInfo::CyberPolicy) -} - fn is_app_server_cyber_policy_error(info: &AppServerCodexErrorInfo) -> bool { matches!(info, AppServerCodexErrorInfo::CyberPolicy) } @@ -2013,15 +1954,7 @@ impl ChatWidget { .iter() .any(|item| item == "run-state" || item == "status") }); - let title_uses_activity = self.config.tui_terminal_title.as_ref().is_none_or(|items| { - items - .iter() - .any(|item| item == "activity" || item == "spinner") - }); - if title_uses_status - || (title_uses_activity - && self.terminal_title_status_kind == TerminalTitleStatusKind::Undoing) - { + if title_uses_status { self.refresh_status_surfaces(); } } @@ -2199,23 +2132,11 @@ impl ChatWidget { } // --- Small event handlers --- - #[cfg(test)] - fn on_session_configured(&mut self, event: SessionConfiguredEvent) { - let (session, initial_messages) = event.into_session(); - self.on_session_configured_with_display_and_fork_parent_title( - session, - SessionConfiguredDisplay::Normal, - /*fork_parent_title*/ None, - initial_messages, - ); - } - fn on_session_configured_with_display_and_fork_parent_title( &mut self, session: ThreadSessionState, display: SessionConfiguredDisplay, fork_parent_title: Option, - #[cfg(test)] initial_messages: Option>, ) { self.last_agent_markdown = None; self.agent_turn_markdowns.clear(); @@ -2300,11 +2221,6 @@ impl ChatWidget { show_fast_status, ); self.apply_session_info_cell(session_info_cell); - - #[cfg(test)] - if let Some(messages) = initial_messages { - self.replay_initial_messages(messages); - } } else if self .active_cell .as_ref() @@ -2352,9 +2268,6 @@ impl ChatWidget { session, SessionConfiguredDisplay::Normal, fork_parent_title, - #[cfg(test)] - /*initial_messages*/ - None, ); } @@ -2364,9 +2277,6 @@ impl ChatWidget { session, SessionConfiguredDisplay::Quiet, /*fork_parent_title*/ None, - #[cfg(test)] - /*initial_messages*/ - None, ); } @@ -2377,9 +2287,6 @@ impl ChatWidget { session, SessionConfiguredDisplay::SideConversation, fork_parent_title, - #[cfg(test)] - /*initial_messages*/ - None, ); } @@ -2496,11 +2403,6 @@ impl ChatWidget { self.request_redraw(); } - #[cfg(test)] - fn on_agent_message(&mut self, message: String) { - self.finalize_completed_assistant_message(Some(&message)); - } - fn on_agent_message_delta(&mut self, delta: String) { self.handle_streaming_delta(delta); } @@ -2917,14 +2819,6 @@ impl ChatWidget { true } - #[cfg(test)] - fn handle_steer_rejected_error(&mut self, codex_error_info: &CoreCodexErrorInfo) -> bool { - matches!( - codex_error_info, - CoreCodexErrorInfo::ActiveTurnNotSteerable { .. } - ) && self.enqueue_rejected_steer() - } - fn handle_app_server_steer_rejected_error( &mut self, codex_error_info: &AppServerCodexErrorInfo, @@ -3036,28 +2930,6 @@ impl ChatWidget { } } - #[cfg(test)] - fn apply_turn_started_context_window(&mut self, model_context_window: Option) { - let info = match self.token_info.take() { - Some(mut info) => { - info.model_context_window = model_context_window; - info - } - None => { - let Some(model_context_window) = model_context_window else { - return; - }; - TokenUsageInfo { - total_token_usage: TokenUsage::default(), - last_token_usage: TokenUsage::default(), - model_context_window: Some(model_context_window), - } - } - }; - - self.apply_token_info(info); - } - fn apply_token_info(&mut self, info: TokenUsageInfo) { let percent = self.context_remaining_percent(&info); let used_tokens = self.context_used_tokens(&info, percent.is_some()); @@ -3344,13 +3216,6 @@ impl ChatWidget { self.request_redraw(); } - #[cfg(test)] - fn on_core_model_verification(&mut self, verifications: &[CoreModelVerification]) { - if verifications.contains(&CoreModelVerification::TrustedAccessForCyber) { - self.on_warning(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING); - } - } - fn on_app_server_model_verification(&mut self, verifications: &[AppServerModelVerification]) { if verifications.contains(&AppServerModelVerification::TrustedAccessForCyber) { self.on_warning(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING); @@ -3366,9 +3231,7 @@ impl ChatWidget { self.finalize_turn(); let send_pending_steers_immediately = self.submit_pending_steers_after_interrupt; self.submit_pending_steers_after_interrupt = false; - if reason != TurnAbortReason::ReviewEnded - && self.interrupted_turn_notice_mode != InterruptedTurnNoticeMode::Suppress - { + if self.interrupted_turn_notice_mode != InterruptedTurnNoticeMode::Suppress { if send_pending_steers_immediately { self.add_to_history(history_cell::new_info_event( "Model interrupted to submit steer instructions.".to_owned(), @@ -3381,8 +3244,9 @@ impl ChatWidget { } } - // Core clears pending_input before emitting TurnAborted, so any unacknowledged steers - // still tracked here must be restored locally instead of waiting for a later commit. + // The server has already discarded pending input by the time the + // interrupted turn reaches the UI, so any unacknowledged steers still + // tracked here must be restored locally instead of waiting for a later commit. if send_pending_steers_immediately { let pending_steers = self .pending_steers @@ -4226,16 +4090,6 @@ impl ChatWidget { self.request_redraw(); } - #[cfg(test)] - fn on_background_event(&mut self, message: String) { - debug!("BackgroundEvent: {message}"); - self.bottom_pane.ensure_status_indicator(); - self.bottom_pane - .set_interrupt_hint_visible(/*visible*/ true); - self.terminal_title_status_kind = TerminalTitleStatusKind::Thinking; - self.set_status_header(message); - } - fn on_hook_started(&mut self, event: HookStartedEvent) { self.flush_answer_stream_with_separator(); self.flush_completed_hook_output(); @@ -4358,38 +4212,6 @@ impl ChatWidget { self.frame_requester.schedule_frame_in(delay); } - #[cfg(test)] - fn on_undo_started(&mut self, event: UndoStartedEvent) { - self.bottom_pane.ensure_status_indicator(); - self.bottom_pane - .set_interrupt_hint_visible(/*visible*/ false); - let message = event - .message - .unwrap_or_else(|| "Undo in progress...".to_string()); - self.terminal_title_status_kind = TerminalTitleStatusKind::Undoing; - self.set_status_header(message); - } - - #[cfg(test)] - fn on_undo_completed(&mut self, event: UndoCompletedEvent) { - let UndoCompletedEvent { success, message } = event; - self.bottom_pane.hide_status_indicator(); - self.terminal_title_status_kind = TerminalTitleStatusKind::Working; - self.refresh_terminal_title(); - let message = message.unwrap_or_else(|| { - if success { - "Undo completed successfully.".to_string() - } else { - "Undo failed.".to_string() - } - }); - if success { - self.add_info_message(message, /*hint*/ None); - } else { - self.add_error_message(message); - } - } - fn on_stream_error(&mut self, message: String, additional_details: Option) { if self.retry_status_header.is_none() { self.retry_status_header = Some(self.current_status.header.clone()); @@ -6965,289 +6787,6 @@ impl ChatWidget { }); } - #[cfg(test)] - fn replay_initial_messages(&mut self, events: Vec) { - for msg in events { - if matches!( - msg, - EventMsg::SessionConfigured(_) | EventMsg::ThreadNameUpdated(_) - ) { - continue; - } - // `id: None` indicates a synthetic/fake id coming from replay. - self.dispatch_event_msg( - /*id*/ None, - msg, - Some(ReplayKind::ResumeInitialMessages), - ); - } - } - - #[cfg(test)] - pub(crate) fn handle_codex_event(&mut self, event: Event) { - let Event { id, msg } = event; - self.dispatch_event_msg(Some(id), msg, /*replay_kind*/ None); - } - - #[cfg(test)] - pub(crate) fn handle_codex_event_replay(&mut self, event: Event) { - let Event { msg, .. } = event; - if matches!(msg, EventMsg::ShutdownComplete) { - return; - } - self.dispatch_event_msg(/*id*/ None, msg, Some(ReplayKind::ThreadSnapshot)); - } - - /// Dispatch a test-only event message to the appropriate handler. - /// - /// `id` is `Some` for live events and `None` for replayed events from - /// `replay_initial_messages()`. Callers should treat `None` as a "fake" id - /// that must not be used to correlate follow-up actions. - #[cfg(test)] - fn dispatch_event_msg( - &mut self, - id: Option, - msg: EventMsg, - replay_kind: Option, - ) { - let from_replay = replay_kind.is_some(); - let is_resume_initial_replay = - matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)); - let is_stream_error = matches!(&msg, EventMsg::StreamError(_)); - if !is_resume_initial_replay && !is_stream_error { - self.restore_retry_status_header_if_present(); - } - - match msg { - EventMsg::AgentMessageDelta(_) - | EventMsg::PlanDelta(_) - | EventMsg::AgentReasoningDelta(_) - | EventMsg::TerminalInteraction(_) - | EventMsg::ExecCommandOutputDelta(_) => {} - _ => { - tracing::trace!("handle_codex_event: {:?}", msg); - } - } - - match msg { - EventMsg::SessionConfigured(e) => self.on_session_configured(e), - EventMsg::ThreadNameUpdated(e) => { - self.on_thread_name_updated(e.thread_id, e.thread_name) - } - // NOTE: All three AgentMessage arms feed `record_agent_markdown` even - // when the message is otherwise not rendered (thread-snapshot replay, - // non-review live messages). This ensures the copy source stays - // populated across replay, resume, and live paths. - EventMsg::AgentMessage(AgentMessageEvent { message, .. }) - if matches!(replay_kind, Some(ReplayKind::ThreadSnapshot)) - && !self.is_review_mode => - { - if !message.is_empty() { - self.record_agent_markdown(&message); - } - } - EventMsg::AgentMessage(AgentMessageEvent { message, .. }) - if from_replay || self.is_review_mode => - { - if !message.is_empty() { - self.record_agent_markdown(&message); - } - // TODO(ccunningham): stop relying on legacy AgentMessage in review mode, - // including thread-snapshot replay, and forward - // ItemCompleted(TurnItem::AgentMessage(_)) instead. - self.on_agent_message(message) - } - EventMsg::AgentMessage(AgentMessageEvent { message, .. }) => { - if !message.is_empty() { - self.record_agent_markdown(&message); - } - } - EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => { - self.on_agent_message_delta(delta) - } - EventMsg::PlanDelta(event) => self.on_plan_delta(event.delta), - EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) - | EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent { - delta, - }) => self.on_agent_reasoning_delta(delta), - EventMsg::AgentReasoning(AgentReasoningEvent { .. }) => self.on_agent_reasoning_final(), - EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => { - self.on_agent_reasoning_delta(text); - self.on_agent_reasoning_final(); - } - EventMsg::TurnStarted(event) => { - let turn_id = event.turn_id; - let model_context_window = event.model_context_window; - self.last_turn_id = Some(turn_id); - if !is_resume_initial_replay { - self.apply_turn_started_context_window(model_context_window); - self.on_task_started(); - } - } - EventMsg::TurnComplete(TurnCompleteEvent { - last_agent_message, - duration_ms, - .. - }) => { - self.on_task_complete(last_agent_message, duration_ms, from_replay); - } - EventMsg::TokenCount(ev) => { - self.set_token_info(ev.info); - self.on_rate_limit_snapshot(ev.rate_limits); - } - EventMsg::Warning(WarningEvent { message }) - | EventMsg::GuardianWarning(WarningEvent { message }) => self.on_warning(message), - EventMsg::GuardianAssessment(ev) => self.on_guardian_assessment(ev), - EventMsg::ModelVerification(event) => { - self.on_core_model_verification(&event.verifications) - } - EventMsg::Error(ErrorEvent { - message, - codex_error_info, - }) => { - if codex_error_info - .as_ref() - .is_some_and(|info| self.handle_steer_rejected_error(info)) - { - } else if codex_error_info - .as_ref() - .is_some_and(is_core_cyber_policy_error) - { - self.on_cyber_policy_error(); - } else if let Some(kind) = codex_error_info - .as_ref() - .and_then(core_rate_limit_error_kind) - { - match kind { - RateLimitErrorKind::ServerOverloaded => { - self.on_server_overloaded_error(message) - } - RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => { - self.on_rate_limit_error(kind, message) - } - } - } else { - self.on_error(message); - } - } - EventMsg::TurnAborted(ev) => match ev.reason { - TurnAbortReason::Interrupted => { - let reason = if ev - .turn_id - .as_deref() - .is_some_and(|turn_id| self.budget_limited_turn_ids.remove(turn_id)) - { - TurnAbortReason::BudgetLimited - } else { - ev.reason - }; - self.on_interrupted_turn(reason); - } - TurnAbortReason::Replaced => { - self.submit_pending_steers_after_interrupt = false; - self.pending_steers.clear(); - self.refresh_pending_input_preview(); - self.on_error("Turn aborted: replaced by a new task".to_owned()) - } - TurnAbortReason::ReviewEnded => { - self.on_interrupted_turn(ev.reason); - } - TurnAbortReason::BudgetLimited => { - if let Some(turn_id) = ev.turn_id.as_deref() { - self.budget_limited_turn_ids.remove(turn_id); - } - self.on_interrupted_turn(ev.reason); - } - }, - EventMsg::PlanUpdate(update) => self.on_plan_update(update), - EventMsg::ExecApprovalRequest(ev) => { - // For replayed events, synthesize an empty id (these should not occur). - self.on_exec_approval_request(id.unwrap_or_default(), ev) - } - EventMsg::ApplyPatchApprovalRequest(ev) => { - self.on_apply_patch_approval_request(id.unwrap_or_default(), ev) - } - EventMsg::ElicitationRequest(ev) => { - self.on_elicitation_request(ev); - } - EventMsg::RequestUserInput(ev) => { - self.on_request_user_input(ev); - } - EventMsg::RequestPermissions(ev) => { - self.on_request_permissions(ev); - } - EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev), - EventMsg::TerminalInteraction(delta) => self.on_terminal_interaction(delta), - EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta), - EventMsg::PatchApplyBegin(ev) => self.on_patch_apply_begin(ev), - EventMsg::PatchApplyEnd(ev) => self.on_patch_apply_end(ev), - EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev), - EventMsg::ViewImageToolCall(ev) => self.on_view_image_tool_call(ev), - EventMsg::ImageGenerationBegin(ev) => self.on_image_generation_begin(ev), - EventMsg::ImageGenerationEnd(ev) => self.on_image_generation_end(ev), - EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev), - EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev), - EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev), - EventMsg::WebSearchEnd(ev) => self.on_web_search_end(ev), - EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev), - EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev), - EventMsg::SkillsUpdateAvailable => { - self.refresh_skills_for_current_cwd(/*force_reload*/ true); - } - EventMsg::ShutdownComplete => self.on_shutdown_complete(), - EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff), - EventMsg::DeprecationNotice(ev) => self.on_deprecation_notice(ev.summary, ev.details), - EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => { - self.on_background_event(message) - } - EventMsg::UndoStarted(ev) => self.on_undo_started(ev), - EventMsg::UndoCompleted(ev) => self.on_undo_completed(ev), - EventMsg::StreamError(StreamErrorEvent { - message, - additional_details, - .. - }) => { - if !is_resume_initial_replay { - self.on_stream_error(message, additional_details); - } - } - EventMsg::UserMessage(ev) => { - if from_replay || self.should_render_realtime_user_message_event(&ev) { - self.on_user_message_event(ev); - } - } - EventMsg::EnteredReviewMode(hint) => { - self.enter_review_mode_with_hint(hint, from_replay) - } - EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review), - EventMsg::ThreadRolledBack(rollback) => { - if from_replay { - self.app_event_tx.send(AppEvent::ApplyThreadRollback { - num_turns: rollback.num_turns, - }); - } - } - EventMsg::HookStarted(event) => self.on_hook_started(event), - EventMsg::HookCompleted(event) => self.on_hook_completed(event), - EventMsg::ItemCompleted(event) => { - let item = event.item; - if !from_replay && let codex_protocol::items::TurnItem::UserMessage(item) = &item { - self.on_committed_user_message(item, from_replay); - } - if let codex_protocol::items::TurnItem::Plan(plan_item) = &item { - self.on_plan_item_completed(plan_item.text.clone()); - } - if let codex_protocol::items::TurnItem::AgentMessage(item) = item { - self.on_agent_message_item_completed(item); - } - } - } - - if !from_replay && self.agent_turn_running { - self.refresh_runtime_metrics(); - } - } - fn enter_review_mode_with_hint(&mut self, hint: String, from_replay: bool) { if self.pre_review_token_info.is_none() { self.pre_review_token_info = Some(self.token_info.clone()); @@ -7273,11 +6812,6 @@ impl ChatWidget { self.request_redraw(); } - #[cfg(test)] - fn on_exited_review_mode(&mut self, _review: ExitedReviewModeEvent) { - self.exit_review_mode_after_item(); - } - fn on_committed_user_message(&mut self, item: &UserMessageItem, from_replay: bool) { let rendered_event = Self::rendered_user_message_event_from_inputs(&item.content); let event = UserMessageEvent { @@ -11115,17 +10649,6 @@ impl ChatWidget { } } - #[cfg(test)] - fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) { - self.add_to_history(history_cell::new_mcp_tools_output( - &self.config, - ev.tools, - ev.resources, - ev.resource_templates, - &ev.auth_statuses, - )); - } - fn on_list_skills(&mut self, ev: SkillsListResponse) { self.set_skills_from_response(&ev); self.refresh_plugin_mentions(); @@ -11744,8 +11267,5 @@ pub(crate) fn show_review_commit_picker_with_entries( }); } -#[cfg(test)] -pub(crate) mod test_events; - #[cfg(test)] pub(crate) mod tests; diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index 00097a476c..22ce6f870a 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -65,18 +65,6 @@ impl RealtimeConversationUiState { } impl ChatWidget { - #[cfg(test)] - pub(super) fn should_render_realtime_user_message_event( - &self, - event: &UserMessageEvent, - ) -> bool { - if !self.realtime_conversation.is_live() { - return false; - } - let key = Self::rendered_user_message_event_from_event(event); - self.last_rendered_user_message_event.as_ref() != Some(&key) - } - fn realtime_footer_hint_items() -> Vec<(String, String)> { vec![("/realtime".to_string(), "stop live voice".to_string())] } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__deltas_then_same_final_message_are_rendered_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__deltas_then_same_final_message_are_rendered_snapshot.snap index e353cbef2c..4d916a33cc 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__deltas_then_same_final_message_are_rendered_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__deltas_then_same_final_message_are_rendered_snapshot.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/status_and_layout.rs expression: combined --- - +• Here is the result. diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 6b590d7ea6..462b878e56 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -33,7 +33,6 @@ const TERMINAL_TITLE_ACTION_REQUIRED_PREFIX_HIDDEN: &str = "[ . ] Action Require pub(super) enum TerminalTitleStatusKind { Working, WaitingForBackgroundTerminal, - Undoing, #[default] Thinking, } @@ -711,7 +710,6 @@ impl ChatWidget { } TerminalTitleStatusKind::Working => "Working".to_string(), TerminalTitleStatusKind::WaitingForBackgroundTerminal => "Waiting".to_string(), - TerminalTitleStatusKind::Undoing => "Undoing".to_string(), TerminalTitleStatusKind::Thinking => "Thinking".to_string(), } } @@ -748,9 +746,7 @@ impl ChatWidget { return false; } - self.mcp_startup_status.is_some() - || self.bottom_pane.is_task_running() - || self.terminal_title_status_kind == TerminalTitleStatusKind::Undoing + self.mcp_startup_status.is_some() || self.bottom_pane.is_task_running() } pub(super) fn should_animate_terminal_title_spinner(&self) -> bool { diff --git a/codex-rs/tui/src/chatwidget/test_events.rs b/codex-rs/tui/src/chatwidget/test_events.rs deleted file mode 100644 index 1e3e06b30d..0000000000 --- a/codex-rs/tui/src/chatwidget/test_events.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Test-only event fixtures for legacy-style ChatWidget coverage. - -#![allow(dead_code)] - -use std::collections::HashMap; -use std::path::PathBuf; - -use crate::approval_events::ApplyPatchApprovalRequestEvent; -use crate::approval_events::ExecApprovalRequestEvent; -use crate::session_state::SessionNetworkProxyRuntime; -use crate::session_state::ThreadSessionState; -use crate::token_usage::TokenCountEvent; -use crate::tool_activity::ExecCommandBeginEvent; -use crate::tool_activity::ExecCommandEndEvent; -use crate::tool_activity::ExecCommandOutputDeltaEvent; -use crate::tool_activity::HookCompletedEvent; -use crate::tool_activity::HookStartedEvent; -use crate::tool_activity::ImageGenerationBeginEvent; -use crate::tool_activity::ImageGenerationEndEvent; -use crate::tool_activity::McpToolCallBeginEvent; -use crate::tool_activity::McpToolCallEndEvent; -use crate::tool_activity::PatchApplyBeginEvent; -use crate::tool_activity::PatchApplyEndEvent; -use crate::tool_activity::TerminalInteractionEvent; -use crate::tool_activity::ViewImageToolCallEvent; -use crate::tool_activity::WebSearchBeginEvent; -use crate::tool_activity::WebSearchEndEvent; -use crate::turn_state::TurnAbortReason; -use codex_app_server_protocol::AskForApproval; -use codex_app_server_protocol::CodexErrorInfo; -use codex_app_server_protocol::McpAuthStatus; -use codex_app_server_protocol::ModelVerification; -use codex_protocol::ThreadId; -use codex_protocol::approvals::ElicitationRequestEvent; -use codex_protocol::approvals::GuardianAssessmentEvent; -use codex_protocol::config_types::ApprovalsReviewer; -use codex_protocol::config_types::ServiceTier; -use codex_protocol::items::TurnItem; -use codex_protocol::mcp::Resource as McpResource; -use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; -use codex_protocol::mcp::Tool as McpTool; -use codex_protocol::memory_citation::MemoryCitation; -use codex_protocol::models::MessagePhase; -use codex_protocol::models::PermissionProfile; -use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; -use codex_protocol::plan_tool::UpdatePlanArgs; -use codex_protocol::request_permissions::RequestPermissionsEvent; -use codex_protocol::request_user_input::RequestUserInputEvent; -use codex_utils_absolute_path::AbsolutePathBuf; -use serde::Deserialize; - -use super::user_messages::UserMessageEvent; - -#[derive(Debug, Clone)] -pub(crate) struct Event { - pub(crate) id: String, - pub(crate) msg: EventMsg, -} - -#[derive(Debug, Clone)] -pub(crate) struct SessionConfiguredEvent { - pub(crate) session_id: ThreadId, - pub(crate) forked_from_id: Option, - pub(crate) thread_name: Option, - pub(crate) model: String, - #[allow(dead_code)] - pub(crate) model_provider_id: String, - pub(crate) service_tier: Option, - pub(crate) approval_policy: AskForApproval, - pub(crate) approvals_reviewer: ApprovalsReviewer, - pub(crate) permission_profile: PermissionProfile, - pub(crate) cwd: AbsolutePathBuf, - pub(crate) reasoning_effort: Option, - pub(crate) history_log_id: u64, - pub(crate) history_entry_count: usize, - pub(crate) initial_messages: Option>, - pub(crate) network_proxy: Option, - pub(crate) rollout_path: Option, -} - -impl SessionConfiguredEvent { - pub(crate) fn into_session(self) -> (ThreadSessionState, Option>) { - ( - ThreadSessionState { - thread_id: self.session_id, - forked_from_id: self.forked_from_id, - fork_parent_title: None, - thread_name: self.thread_name, - model: self.model, - model_provider_id: self.model_provider_id, - service_tier: self.service_tier, - approval_policy: self.approval_policy, - approvals_reviewer: self.approvals_reviewer, - permission_profile: self.permission_profile, - cwd: self.cwd, - instruction_source_paths: Vec::new(), - reasoning_effort: self.reasoning_effort, - history_log_id: self.history_log_id, - history_entry_count: u64::try_from(self.history_entry_count).unwrap_or(u64::MAX), - network_proxy: self.network_proxy, - rollout_path: self.rollout_path, - }, - self.initial_messages, - ) - } -} - -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -pub(crate) enum EventMsg { - Error(ErrorEvent), - Warning(WarningEvent), - GuardianWarning(WarningEvent), - ModelVerification(ModelVerificationEvent), - ThreadRolledBack(ThreadRolledBackEvent), - TurnStarted(TurnStartedEvent), - TurnComplete(TurnCompleteEvent), - TokenCount(TokenCountEvent), - AgentMessage(AgentMessageEvent), - UserMessage(UserMessageEvent), - AgentMessageDelta(AgentMessageDeltaEvent), - AgentReasoning(AgentReasoningEvent), - AgentReasoningDelta(AgentReasoningDeltaEvent), - AgentReasoningRawContent(AgentReasoningRawContentEvent), - AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent), - SessionConfigured(SessionConfiguredEvent), - ThreadNameUpdated(ThreadNameUpdatedEvent), - McpToolCallBegin(McpToolCallBeginEvent), - McpToolCallEnd(McpToolCallEndEvent), - WebSearchBegin(WebSearchBeginEvent), - WebSearchEnd(WebSearchEndEvent), - ImageGenerationBegin(ImageGenerationBeginEvent), - ImageGenerationEnd(ImageGenerationEndEvent), - ExecCommandBegin(ExecCommandBeginEvent), - ExecCommandOutputDelta(ExecCommandOutputDeltaEvent), - TerminalInteraction(TerminalInteractionEvent), - ExecCommandEnd(ExecCommandEndEvent), - ViewImageToolCall(ViewImageToolCallEvent), - ExecApprovalRequest(ExecApprovalRequestEvent), - RequestPermissions(RequestPermissionsEvent), - RequestUserInput(RequestUserInputEvent), - ElicitationRequest(ElicitationRequestEvent), - ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent), - GuardianAssessment(GuardianAssessmentEvent), - DeprecationNotice(DeprecationNoticeEvent), - BackgroundEvent(BackgroundEventEvent), - UndoStarted(UndoStartedEvent), - UndoCompleted(UndoCompletedEvent), - StreamError(StreamErrorEvent), - PatchApplyBegin(PatchApplyBeginEvent), - PatchApplyEnd(PatchApplyEndEvent), - TurnDiff(TurnDiffEvent), - McpListToolsResponse(McpListToolsResponseEvent), - ListSkillsResponse(codex_app_server_protocol::SkillsListResponse), - SkillsUpdateAvailable, - PlanUpdate(UpdatePlanArgs), - TurnAborted(TurnAbortedEvent), - ShutdownComplete, - EnteredReviewMode(String), - ExitedReviewMode(ExitedReviewModeEvent), - ItemCompleted(ItemCompletedEvent), - HookStarted(HookStartedEvent), - HookCompleted(HookCompletedEvent), - PlanDelta(PlanDeltaEvent), -} - -#[derive(Debug, Clone)] -pub(crate) struct ErrorEvent { - pub(crate) message: String, - pub(crate) codex_error_info: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct WarningEvent { - pub(crate) message: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ModelVerificationEvent { - pub(crate) verifications: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct TurnCompleteEvent { - pub(crate) turn_id: String, - pub(crate) last_agent_message: Option, - pub(crate) completed_at: Option, - pub(crate) duration_ms: Option, - pub(crate) time_to_first_token_ms: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct TurnStartedEvent { - pub(crate) turn_id: String, - pub(crate) started_at: Option, - pub(crate) model_context_window: Option, - pub(crate) collaboration_mode_kind: codex_protocol::config_types::ModeKind, -} - -#[derive(Debug, Clone)] -pub(crate) struct AgentMessageEvent { - pub(crate) message: String, - pub(crate) phase: Option, - pub(crate) memory_citation: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct AgentMessageDeltaEvent { - pub(crate) delta: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct AgentReasoningEvent { - pub(crate) text: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct AgentReasoningRawContentEvent { - pub(crate) text: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct AgentReasoningRawContentDeltaEvent { - pub(crate) delta: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct AgentReasoningDeltaEvent { - pub(crate) delta: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct BackgroundEventEvent { - pub(crate) message: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct DeprecationNoticeEvent { - pub(crate) summary: String, - pub(crate) details: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct UndoStartedEvent { - pub(crate) message: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct UndoCompletedEvent { - pub(crate) success: bool, - pub(crate) message: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct ThreadRolledBackEvent { - pub(crate) num_turns: u32, -} - -#[derive(Debug, Clone)] -pub(crate) struct StreamErrorEvent { - pub(crate) message: String, - pub(crate) codex_error_info: Option, - pub(crate) additional_details: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct TurnDiffEvent { - pub(crate) unified_diff: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct McpListToolsResponseEvent { - pub(crate) tools: HashMap, - pub(crate) resources: HashMap>, - pub(crate) resource_templates: HashMap>, - pub(crate) auth_statuses: HashMap, -} - -#[derive(Debug, Clone)] -pub(crate) struct ThreadNameUpdatedEvent { - pub(crate) thread_id: ThreadId, - pub(crate) thread_name: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct TurnAbortedEvent { - pub(crate) turn_id: Option, - pub(crate) reason: TurnAbortReason, - pub(crate) completed_at: Option, - pub(crate) duration_ms: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct ItemCompletedEvent { - pub(crate) thread_id: ThreadId, - pub(crate) turn_id: String, - pub(crate) item: TurnItem, -} - -#[derive(Debug, Clone)] -pub(crate) struct PlanDeltaEvent { - pub(crate) thread_id: String, - pub(crate) turn_id: String, - pub(crate) item_id: String, - pub(crate) delta: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct ExitedReviewModeEvent; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index a1f91ea2d2..1d318e0fab 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -17,25 +17,6 @@ pub(super) use crate::bottom_pane::LocalImageAttachment; pub(super) use crate::bottom_pane::MentionBinding; pub(super) use crate::bottom_pane::QueuedInputAction; pub(super) use crate::chatwidget::realtime::RealtimeConversationPhase; -pub(super) use crate::chatwidget::test_events::AgentMessageDeltaEvent; -pub(super) use crate::chatwidget::test_events::AgentMessageEvent; -pub(super) use crate::chatwidget::test_events::AgentReasoningDeltaEvent; -pub(super) use crate::chatwidget::test_events::AgentReasoningEvent; -pub(super) use crate::chatwidget::test_events::BackgroundEventEvent; -pub(super) use crate::chatwidget::test_events::ErrorEvent; -pub(super) use crate::chatwidget::test_events::Event; -pub(super) use crate::chatwidget::test_events::EventMsg; -pub(super) use crate::chatwidget::test_events::ExitedReviewModeEvent; -pub(super) use crate::chatwidget::test_events::ItemCompletedEvent; -pub(super) use crate::chatwidget::test_events::ModelVerificationEvent; -pub(super) use crate::chatwidget::test_events::SessionConfiguredEvent; -pub(super) use crate::chatwidget::test_events::StreamErrorEvent; -pub(super) use crate::chatwidget::test_events::ThreadRolledBackEvent; -pub(super) use crate::chatwidget::test_events::TurnCompleteEvent; -pub(super) use crate::chatwidget::test_events::TurnStartedEvent; -pub(super) use crate::chatwidget::test_events::UndoCompletedEvent; -pub(super) use crate::chatwidget::test_events::UndoStartedEvent; -pub(super) use crate::chatwidget::test_events::WarningEvent; pub(super) use crate::diff_model::FileChange; pub(super) use crate::history_cell::UserHistoryCell; pub(super) use crate::legacy_core::config::Config; @@ -47,7 +28,6 @@ pub(super) use crate::test_backend::VT100Backend; pub(super) use crate::test_support::PathBufExt; pub(super) use crate::test_support::test_path_buf; pub(super) use crate::test_support::test_path_display; -pub(super) use crate::token_usage::TokenCountEvent; pub(super) use crate::token_usage::TokenUsage; pub(super) use crate::token_usage::TokenUsageInfo; pub(super) use crate::tool_activity::ExecCommandBeginEvent; @@ -75,7 +55,6 @@ pub(super) use codex_app_server_protocol::CommandAction as AppServerCommandActio pub(super) use codex_app_server_protocol::CommandExecutionRequestApprovalParams as AppServerCommandExecutionRequestApprovalParams; pub(super) use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource; pub(super) use codex_app_server_protocol::CommandExecutionSource as AppServerCommandExecutionSource; -pub(super) use codex_app_server_protocol::CommandExecutionStatus as CoreExecCommandStatus; pub(super) use codex_app_server_protocol::CommandExecutionStatus as AppServerCommandExecutionStatus; pub(super) use codex_app_server_protocol::ConfigWarningNotification; pub(super) use codex_app_server_protocol::CreditsSnapshot; @@ -107,11 +86,9 @@ pub(super) use codex_app_server_protocol::MarketplaceInterface; pub(super) use codex_app_server_protocol::McpServerStartupState; pub(super) use codex_app_server_protocol::McpServerStatusDetail; pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification; -pub(super) use codex_app_server_protocol::ModelVerification as CoreModelVerification; pub(super) use codex_app_server_protocol::ModelVerification as AppServerModelVerification; pub(super) use codex_app_server_protocol::ModelVerificationNotification; pub(super) use codex_app_server_protocol::NonSteerableTurnKind; -pub(super) use codex_app_server_protocol::PatchApplyStatus as CorePatchApplyStatus; pub(super) use codex_app_server_protocol::PatchApplyStatus as AppServerPatchApplyStatus; pub(super) use codex_app_server_protocol::PatchChangeKind; pub(super) use codex_app_server_protocol::PermissionsRequestApprovalParams as AppServerPermissionsRequestApprovalParams; @@ -175,11 +152,6 @@ pub(super) use codex_protocol::config_types::ModeKind; pub(super) use codex_protocol::config_types::Personality; pub(super) use codex_protocol::config_types::ServiceTier; pub(super) use codex_protocol::config_types::Settings; -pub(super) use codex_protocol::items::AgentMessageContent; -pub(super) use codex_protocol::items::AgentMessageItem; -pub(super) use codex_protocol::items::PlanItem; -pub(super) use codex_protocol::items::TurnItem; -pub(super) use codex_protocol::items::UserMessageItem; pub(super) use codex_protocol::models::FileSystemPermissions; pub(super) use codex_protocol::models::MessagePhase; pub(super) use codex_protocol::models::NetworkPermissions; @@ -257,7 +229,6 @@ macro_rules! assert_chatwidget_snapshot { mod app_server; mod approval_requests; -mod background_events; mod composer_submission; mod exec_flow; mod goal_menu; diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index ad90cf14d1..9988bb9f22 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -25,10 +25,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-short".into(), - msg: EventMsg::ExecApprovalRequest(ev), - }); + handle_exec_approval_request(&mut chat, "sub-short", ev); let proposed_cells = drain_insert_history(&mut rx); assert!( @@ -192,9 +189,10 @@ fn app_server_request_permissions_preserves_file_system_permissions() { async fn exec_approval_uses_approval_id_when_present() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "sub-short".into(), - msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + handle_exec_approval_request( + &mut chat, + "sub-short", + ExecApprovalRequestEvent { call_id: "call-parent".into(), approval_id: Some("approval-subcommand".into()), turn_id: "turn-short".into(), @@ -209,8 +207,8 @@ async fn exec_approval_uses_approval_id_when_present() { additional_permissions: None, available_decisions: None, parsed_cmd: vec![], - }), - }); + }, + ); chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); @@ -253,10 +251,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-multi".into(), - msg: EventMsg::ExecApprovalRequest(ev_multi), - }); + handle_exec_approval_request(&mut chat, "sub-multi", ev_multi); let proposed_multi = drain_insert_history(&mut rx); assert!( proposed_multi.is_empty(), @@ -306,10 +301,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-long".into(), - msg: EventMsg::ExecApprovalRequest(ev_long), - }); + handle_exec_approval_request(&mut chat, "sub-long", ev_long); let proposed_long = drain_insert_history(&mut rx); assert!( proposed_long.is_empty(), diff --git a/codex-rs/tui/src/chatwidget/tests/background_events.rs b/codex-rs/tui/src/chatwidget/tests/background_events.rs deleted file mode 100644 index 1aa09557d6..0000000000 --- a/codex-rs/tui/src/chatwidget/tests/background_events.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::*; -use pretty_assertions::assert_eq; - -#[tokio::test] -async fn background_event_updates_status_header() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.handle_codex_event(Event { - id: "bg-1".into(), - msg: EventMsg::BackgroundEvent(BackgroundEventEvent { - message: "Waiting for `vim`".to_string(), - }), - }); - - assert!(chat.bottom_pane.status_indicator_visible()); - assert_eq!(chat.current_status.header, "Waiting for `vim`"); - assert!(drain_insert_history(&mut rx).is_empty()); -} diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index ef179789ee..87bb84dabc 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -14,9 +14,10 @@ async fn submission_preserves_text_elements_and_local_images() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -25,17 +26,14 @@ async fn submission_preserves_text_elements_and_local_images() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let placeholder = "[Image #1]"; @@ -119,9 +117,10 @@ async fn submission_includes_configured_permission_profile() { }, } .into(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -130,17 +129,14 @@ async fn submission_includes_configured_permission_profile() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: expected_permission_profile.clone(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); chat.bottom_pane.set_composer_text( @@ -170,9 +166,10 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() { file_system: PermissionProfileFileSystemPermissions::Unrestricted, } .into(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -181,17 +178,14 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: expected_permission_profile.clone(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); chat.bottom_pane @@ -213,9 +207,10 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -224,17 +219,14 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let remote_url = "https://example.com/remote.png".to_string(); @@ -308,9 +300,10 @@ async fn enter_with_only_remote_images_submits_user_turn() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -319,17 +312,14 @@ async fn enter_with_only_remote_images_submits_user_turn() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let remote_url = "https://example.com/remote-only.png".to_string(); @@ -373,9 +363,10 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -384,17 +375,14 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let remote_url = "https://example.com/remote-only.png".to_string(); @@ -413,9 +401,10 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -424,17 +413,14 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let remote_url = "https://example.com/remote-only.png".to_string(); @@ -453,9 +439,10 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -464,17 +451,14 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let remote_url = "https://example.com/remote-only.png".to_string(); @@ -496,9 +480,10 @@ async fn submission_prefers_selected_duplicate_skill_path() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -507,17 +492,14 @@ async fn submission_prefers_selected_duplicate_skill_path() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); let repo_skill_path = test_path_buf("/tmp/repo/figma/SKILL.md").abs(); @@ -738,15 +720,7 @@ async fn interrupted_turn_restore_keeps_active_mode_for_resubmission() { ); chat.refresh_pending_input_preview(); - chat.handle_codex_event(Event { - id: "interrupt".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); assert_eq!(chat.bottom_pane.composer_text(), "Implement the plan."); assert!(chat.queued_user_messages.is_empty()); @@ -1184,16 +1158,8 @@ async fn interrupt_restores_queued_messages_into_composer() { .push_back(UserMessage::from("second queued".to_string()).into()); chat.refresh_pending_input_preview(); - // Deliver a TurnAborted event with Interrupted reason (as if Esc was pressed). - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + // Deliver an interrupted turn notification as if Esc was pressed. + handle_turn_interrupted(&mut chat, "turn-1"); // Composer should now contain the queued messages joined by newlines, in order. assert_eq!( @@ -1226,15 +1192,7 @@ async fn interrupt_prepends_queued_messages_before_existing_composer_text() { .push_back(UserMessage::from("second queued".to_string()).into()); chat.refresh_pending_input_preview(); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); assert_eq!( chat.bottom_pane.composer_text(), diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 47d3c623b8..e1f924218d 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -22,10 +22,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-short".into(), - msg: EventMsg::ExecApprovalRequest(ev), - }); + handle_exec_approval_request(&mut chat, "sub-short", ev); let proposed_cells = drain_insert_history(&mut rx); assert!( @@ -89,9 +86,10 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() { async fn exec_approval_uses_approval_id_when_present() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "sub-short".into(), - msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + handle_exec_approval_request( + &mut chat, + "sub-short", + ExecApprovalRequestEvent { call_id: "call-parent".into(), approval_id: Some("approval-subcommand".into()), turn_id: "turn-short".into(), @@ -106,8 +104,8 @@ async fn exec_approval_uses_approval_id_when_present() { additional_permissions: None, available_decisions: None, parsed_cmd: vec![], - }), - }); + }, + ); chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); @@ -151,10 +149,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-multi".into(), - msg: EventMsg::ExecApprovalRequest(ev_multi), - }); + handle_exec_approval_request(&mut chat, "sub-multi", ev_multi); let proposed_multi = drain_insert_history(&mut rx); assert!( proposed_multi.is_empty(), @@ -206,10 +201,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-long".into(), - msg: EventMsg::ExecApprovalRequest(ev_long), - }); + handle_exec_approval_request(&mut chat, "sub-long", ev_long); let proposed_long = drain_insert_history(&mut rx); assert!( proposed_long.is_empty(), @@ -361,9 +353,9 @@ async fn exec_end_without_begin_uses_event_command() { ]; let parsed_cmd = codex_shell_command::parse_command::parse_command(&command); let cwd = AbsolutePathBuf::current_dir().expect("current dir"); - chat.handle_codex_event(Event { - id: "call-orphan".to_string(), - msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { + handle_exec_end( + &mut chat, + ExecCommandEndEvent { call_id: "call-orphan".to_string(), process_id: None, turn_id: "turn-1".to_string(), @@ -378,9 +370,9 @@ async fn exec_end_without_begin_uses_event_command() { exit_code: 0, duration: std::time::Duration::from_millis(5), formatted_output: "done".to_string(), - status: CoreExecCommandStatus::Completed, - }), - }); + status: AppServerCommandExecutionStatus::Completed, + }, + ); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1, "expected finalized exec cell to flush"); @@ -621,14 +613,10 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() { /*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false, ); - chat.handle_codex_event(Event { - id: "call-1".to_string(), - msg: EventMsg::TerminalInteraction(TerminalInteractionEvent { - call_id: "call-1".to_string(), - process_id: "proc-1".to_string(), - stdin: "ls\n".to_string(), - }), - }); + handle_terminal_interaction( + &mut chat, + terminal_interaction_event("call-1", "proc-1", "ls\n"), + ); let cells = drain_insert_history(&mut rx); assert!( @@ -640,30 +628,13 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() { #[tokio::test] async fn unified_exec_wait_after_final_agent_message_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); begin_unified_exec_startup(&mut chat, "call-wait", "proc-1", "cargo test -p codex-core"); terminal_interaction(&mut chat, "call-wait-stdin", "proc-1", ""); complete_assistant_message(&mut chat, "msg-1", "Final response.", /*phase*/ None); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Final response.".into()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); let cells = drain_insert_history(&mut rx); let combined = cells @@ -676,15 +647,7 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() { #[tokio::test] async fn unified_exec_wait_before_streamed_agent_message_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); begin_unified_exec_startup( &mut chat, @@ -694,22 +657,8 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() { ); terminal_interaction(&mut chat, "call-wait-stream-stdin", "proc-1", ""); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { - delta: "Streaming response.".into(), - }), - }); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_agent_message_delta(&mut chat, "Streaming response."); + handle_turn_completed(&mut chat, "turn-wait-1", None); let cells = drain_insert_history(&mut rx); let combined = cells @@ -722,15 +671,7 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() { #[tokio::test] async fn final_worked_for_uses_cumulative_turn_duration_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); let exec = begin_exec_with_source( &mut chat, @@ -746,16 +687,7 @@ async fn final_worked_for_uses_cumulative_turn_duration_snapshot() { "Final response.", Some(MessagePhase::FinalAnswer), ); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Final response.".to_string()), - completed_at: None, - duration_ms: Some(125_000), - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", Some(125_000)); let cells = drain_insert_history(&mut rx); let combined = cells @@ -818,16 +750,7 @@ async fn unified_exec_waiting_multiple_empty_snapshots() { assert_eq!(status.header(), "Waiting for background terminal"); assert_eq!(status.details(), Some("just fix")); - chat.handle_codex_event(Event { - id: "turn-wait-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-wait-3", None); let cells = drain_insert_history(&mut rx); let combined = cells @@ -899,16 +822,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() { .collect::(); assert_chatwidget_snapshot!("unified_exec_non_empty_then_empty_active", active_combined); - chat.handle_codex_event(Event { - id: "turn-wait-3".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); let post_cells = drain_insert_history(&mut rx); let mut combined = pre_cells @@ -931,13 +845,13 @@ async fn view_image_tool_call_adds_history_cell() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let image_path = chat.config.cwd.join("example.png"); - chat.handle_codex_event(Event { - id: "sub-image".into(), - msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent { + handle_view_image_tool_call( + &mut chat, + ViewImageToolCallEvent { call_id: "call-image".into(), path: image_path, - }), - }); + }, + ); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1, "expected a single history cell"); @@ -949,16 +863,16 @@ async fn view_image_tool_call_adds_history_cell() { async fn image_generation_call_adds_history_cell() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "sub-image-generation".into(), - msg: EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { + handle_image_generation_end( + &mut chat, + ImageGenerationEndEvent { call_id: "call-image-generation".into(), status: "completed".into(), revised_prompt: Some("A tiny blue square".into()), result: "Zm9v".into(), saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()), - }), - }); + }, + ); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1, "expected a single history cell"); @@ -1052,9 +966,10 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -1063,28 +978,17 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); drain_insert_history(&mut rx); while op_rx.try_recv().is_ok() {} - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); chat.bottom_pane .set_composer_text("!echo hi".to_string(), Vec::new(), Vec::new()); @@ -1105,15 +1009,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { async fn user_message_during_user_shell_command_is_queued_not_steered() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); let begin = begin_exec_with_source( &mut chat, "user-shell-sleep", @@ -1130,16 +1026,13 @@ async fn user_message_during_user_shell_command_is_queued_not_steered() { assert_eq!(chat.queued_user_message_texts(), vec!["hi".to_string()]); end_exec(&mut chat, begin, "", "", /*exit_code*/ 0); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("done".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + complete_assistant_message( + &mut chat, + "msg-done", + "done", + Some(MessagePhase::FinalAnswer), + ); + handle_turn_completed(&mut chat, "turn-1", None); match next_submit_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( @@ -1176,7 +1069,7 @@ async fn disabled_slash_command_while_task_running_snapshot() { // // Snapshot test: command approval modal // -// Synthesizes a Codex ExecApprovalRequest event to trigger the approval modal +// Synthesizes an exec approval request to trigger the approval modal // and snapshots the visual output using the ratatui TestBackend. #[tokio::test] async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { @@ -1208,10 +1101,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-approve".into(), - msg: EventMsg::ExecApprovalRequest(ev), - }); + handle_exec_approval_request(&mut chat, "sub-approve", ev); // Render to a fixed-size test terminal and snapshot. // Call desired_height first and use that exact height for rendering. let width = 100; @@ -1269,10 +1159,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-approve-noreason".into(), - msg: EventMsg::ExecApprovalRequest(ev), - }); + handle_exec_approval_request(&mut chat, "sub-approve-noreason", ev); let width = 100; let height = chat.desired_height(width); @@ -1317,10 +1204,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot() available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-approve-multiline-trunc".into(), - msg: EventMsg::ExecApprovalRequest(ev), - }); + handle_exec_approval_request(&mut chat, "sub-approve-multiline-trunc", ev); let width = 100; let height = chat.desired_height(width); @@ -1364,10 +1248,7 @@ async fn approval_modal_patch_snapshot() -> anyhow::Result<()> { reason: Some("The model wants to apply changes".into()), grant_root: Some(PathBuf::from("/tmp")), }; - chat.handle_codex_event(Event { - id: "sub-approve-patch".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ev), - }); + handle_apply_patch_approval_request(&mut chat, "sub-approve-patch", ev); // Render at the widget's desired height and snapshot. let height = chat.desired_height(/*width*/ 80); @@ -1392,15 +1273,7 @@ async fn interrupt_preserves_unified_exec_processes() { begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6"); assert_eq!(chat.unified_exec_processes.len(), 2); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); assert_eq!(chat.unified_exec_processes.len(), 2); @@ -1427,28 +1300,12 @@ async fn interrupt_preserves_unified_exec_processes() { async fn interrupt_preserves_unified_exec_wait_streak_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); let begin = begin_unified_exec_startup(&mut chat, "call-1", "process-1", "just fix"); terminal_interaction(&mut chat, "call-1a", "process-1", ""); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); end_exec(&mut chat, begin, "", "", /*exit_code*/ 0); let cells = drain_insert_history(&mut rx); @@ -1469,16 +1326,7 @@ async fn turn_complete_keeps_unified_exec_processes() { begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6"); assert_eq!(chat.unified_exec_processes.len(), 2); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); assert_eq!(chat.unified_exec_processes.len(), 2); @@ -1520,10 +1368,7 @@ async fn apply_patch_events_emit_history_cells() { reason: None, grant_root: None, }; - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ev), - }); + handle_apply_patch_approval_request(&mut chat, "s1", ev); assert!( drain_insert_history(&mut rx).is_empty(), "expected approval request to surface via modal without emitting history cells" @@ -1543,10 +1388,7 @@ async fn apply_patch_events_emit_history_cells() { auto_approved: true, changes: changes2, }; - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::PatchApplyBegin(begin), - }); + handle_patch_apply_begin(&mut chat, begin); let cells = drain_insert_history(&mut rx); assert!(!cells.is_empty(), "expected apply block cell to be sent"); let blob = lines_to_single_string(cells.last().unwrap()); @@ -1570,12 +1412,9 @@ async fn apply_patch_events_emit_history_cells() { stderr: String::new(), success: true, changes: end_changes, - status: CorePatchApplyStatus::Completed, + status: AppServerPatchApplyStatus::Completed, }; - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::PatchApplyEnd(end), - }); + handle_patch_apply_end(&mut chat, end); let cells = drain_insert_history(&mut rx); assert!( cells.is_empty(), @@ -1594,16 +1433,17 @@ async fn apply_patch_manual_approval_adjusts_header() { content: "hello\n".to_string(), }, ); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + handle_apply_patch_approval_request( + &mut chat, + "s1", + ApplyPatchApprovalRequestEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), changes: proposed_changes, reason: None, grant_root: None, - }), - }); + }, + ); drain_insert_history(&mut rx); let mut apply_changes = HashMap::new(); @@ -1613,15 +1453,15 @@ async fn apply_patch_manual_approval_adjusts_header() { content: "hello\n".to_string(), }, ); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { + handle_patch_apply_begin( + &mut chat, + PatchApplyBeginEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), auto_approved: false, changes: apply_changes, - }), - }); + }, + ); let cells = drain_insert_history(&mut rx); assert!(!cells.is_empty(), "expected apply block cell to be sent"); @@ -1643,16 +1483,17 @@ async fn apply_patch_manual_flow_snapshot() { content: "hello\n".to_string(), }, ); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + handle_apply_patch_approval_request( + &mut chat, + "s1", + ApplyPatchApprovalRequestEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), changes: proposed_changes, reason: Some("Manual review required".into()), grant_root: None, - }), - }); + }, + ); let history_before_apply = drain_insert_history(&mut rx); assert!( history_before_apply.is_empty(), @@ -1666,15 +1507,15 @@ async fn apply_patch_manual_flow_snapshot() { content: "hello\n".to_string(), }, ); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { + handle_patch_apply_begin( + &mut chat, + PatchApplyBeginEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), auto_approved: false, changes: apply_changes, - }), - }); + }, + ); let approved_lines = drain_insert_history(&mut rx) .pop() .expect("approved patch cell"); @@ -1703,10 +1544,7 @@ async fn apply_patch_approval_sends_op_with_call_id() { reason: None, grant_root: None, }; - chat.handle_codex_event(Event { - id: "sub-123".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ev), - }); + handle_apply_patch_approval_request(&mut chat, "sub-123", ev); // Approve via key press 'y' chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); @@ -1741,16 +1579,17 @@ async fn apply_patch_full_flow_integration_like() { PathBuf::from("pkg.rs"), FileChange::Add { content: "".into() }, ); - chat.handle_codex_event(Event { - id: "sub-xyz".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + handle_apply_patch_approval_request( + &mut chat, + "sub-xyz", + ApplyPatchApprovalRequestEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), changes, reason: None, grant_root: None, - }), - }); + }, + ); // 2) User approves via 'y' and App receives a thread-scoped op chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); @@ -1785,32 +1624,32 @@ async fn apply_patch_full_flow_integration_like() { PathBuf::from("pkg.rs"), FileChange::Add { content: "".into() }, ); - chat.handle_codex_event(Event { - id: "sub-xyz".into(), - msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { + handle_patch_apply_begin( + &mut chat, + PatchApplyBeginEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), auto_approved: false, changes: changes2, - }), - }); + }, + ); let mut end_changes = HashMap::new(); end_changes.insert( PathBuf::from("pkg.rs"), FileChange::Add { content: "".into() }, ); - chat.handle_codex_event(Event { - id: "sub-xyz".into(), - msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent { + handle_patch_apply_end( + &mut chat, + PatchApplyEndEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), stdout: String::from("ok"), stderr: String::new(), success: true, changes: end_changes, - status: CorePatchApplyStatus::Completed, - }), - }); + status: AppServerPatchApplyStatus::Completed, + }, + ); } #[tokio::test] @@ -1828,16 +1667,17 @@ async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> { PathBuf::from("a.rs"), FileChange::Add { content: "".into() }, ); - chat.handle_codex_event(Event { - id: "sub-1".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + handle_apply_patch_approval_request( + &mut chat, + "sub-1", + ApplyPatchApprovalRequestEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), changes, reason: None, grant_root: None, - }), - }); + }, + ); // Render and ensure the approval modal title is present let area = Rect::new(0, 0, 80, 12); @@ -1882,16 +1722,17 @@ async fn apply_patch_request_omits_diff_summary_from_modal() -> anyhow::Result<( content: "line one\nline two\n".into(), }, ); - chat.handle_codex_event(Event { - id: "sub-apply".into(), - msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + handle_apply_patch_approval_request( + &mut chat, + "sub-apply", + ApplyPatchApprovalRequestEvent { call_id: "call-apply".into(), turn_id: "turn-apply".into(), changes, reason: None, grant_root: None, - }), - }); + }, + ); assert!( drain_insert_history(&mut rx).is_empty(), diff --git a/codex-rs/tui/src/chatwidget/tests/guardian.rs b/codex-rs/tui/src/chatwidget/tests/guardian.rs index ea5a193f00..c51b3f6687 100644 --- a/codex-rs/tui/src/chatwidget/tests/guardian.rs +++ b/codex-rs/tui/src/chatwidget/tests/guardian.rs @@ -23,10 +23,7 @@ fn auto_review_denial_event() -> GuardianAssessmentEvent { async fn auto_review_denials_popup_lists_stored_auto_review_denials() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "guardian-assessment".into(), - msg: EventMsg::GuardianAssessment(auto_review_denial_event()), - }); + chat.on_guardian_assessment(auto_review_denial_event()); drain_insert_history(&mut rx); chat.open_auto_review_denials_popup(); @@ -40,10 +37,7 @@ async fn approving_recent_denial_emits_structured_core_op_once() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let thread_id = ThreadId::new(); chat.thread_id = Some(thread_id); - chat.handle_codex_event(Event { - id: "guardian-assessment".into(), - msg: EventMsg::GuardianAssessment(auto_review_denial_event()), - }); + chat.on_guardian_assessment(auto_review_denial_event()); drain_insert_history(&mut rx); chat.approve_recent_auto_review_denial(thread_id, "auto-review-recent-1".to_string()); @@ -75,39 +69,28 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() { cwd: test_path_buf("/tmp").abs(), }; - chat.handle_codex_event(Event { - id: "guardian-in-progress".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-1".into(), - target_item_id: Some("guardian-target-1".into()), - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::InProgress, - risk_level: None, - user_authorization: None, - rationale: None, - decision_source: None, - action: action.clone(), - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-1".into(), + target_item_id: Some("guardian-target-1".into()), + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::InProgress, + risk_level: None, + user_authorization: None, + rationale: None, + decision_source: None, + action: action.clone(), }); - chat.handle_codex_event(Event { - id: "guardian-warning".into(), - msg: EventMsg::GuardianWarning(WarningEvent { - message: "Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to `https://example.com`, which is an external and untrusted endpoint.".into(), - }), - }); - chat.handle_codex_event(Event { - id: "guardian-assessment".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-1".into(), - target_item_id: Some("guardian-target-1".into()), - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::Denied, - risk_level: Some(GuardianRiskLevel::High), - user_authorization: Some(GuardianUserAuthorization::Low), - rationale: Some("Would exfiltrate local source code.".into()), - decision_source: Some(GuardianAssessmentDecisionSource::Agent), - action, - }), + chat.on_warning("Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to `https://example.com`, which is an external and untrusted endpoint."); + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-1".into(), + target_item_id: Some("guardian-target-1".into()), + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::Denied, + risk_level: Some(GuardianRiskLevel::High), + user_authorization: Some(GuardianUserAuthorization::Low), + rationale: Some("Would exfiltrate local source code.".into()), + decision_source: Some(GuardianAssessmentDecisionSource::Agent), + action, }); let width: u16 = 140; @@ -140,23 +123,20 @@ async fn guardian_approved_exec_renders_approved_request() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.show_welcome_banner = false; - chat.handle_codex_event(Event { - id: "guardian-assessment".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "thread:child-thread:guardian-1".into(), - target_item_id: Some("guardian-approved-target".into()), - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::Approved, - risk_level: Some(GuardianRiskLevel::Low), - user_authorization: Some(GuardianUserAuthorization::High), - rationale: Some("Narrowly scoped to the requested file.".into()), - decision_source: Some(GuardianAssessmentDecisionSource::Agent), - action: GuardianAssessmentAction::Command { - source: GuardianCommandSource::Shell, - command: "rm -f /tmp/guardian-approved.sqlite".to_string(), - cwd: test_path_buf("/tmp").abs(), - }, - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "thread:child-thread:guardian-1".into(), + target_item_id: Some("guardian-approved-target".into()), + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::Approved, + risk_level: Some(GuardianRiskLevel::Low), + user_authorization: Some(GuardianUserAuthorization::High), + rationale: Some("Narrowly scoped to the requested file.".into()), + decision_source: Some(GuardianAssessmentDecisionSource::Agent), + action: GuardianAssessmentAction::Command { + source: GuardianCommandSource::Shell, + command: "rm -f /tmp/guardian-approved.sqlite".to_string(), + cwd: test_path_buf("/tmp").abs(), + }, }); let width: u16 = 120; @@ -199,19 +179,16 @@ async fn guardian_approved_request_permissions_renders_request_summary() { }, }; - chat.handle_codex_event(Event { - id: "guardian-in-progress".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-request-permissions".into(), - target_item_id: None, - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::InProgress, - risk_level: None, - user_authorization: None, - rationale: None, - decision_source: None, - action: action.clone(), - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-request-permissions".into(), + target_item_id: None, + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::InProgress, + risk_level: None, + user_authorization: None, + rationale: None, + decision_source: None, + action: action.clone(), }); let status = chat @@ -224,19 +201,16 @@ async fn guardian_approved_request_permissions_renders_request_summary() { Some("permission request: Need write access for generated report assets.") ); - chat.handle_codex_event(Event { - id: "guardian-assessment".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-request-permissions".into(), - target_item_id: None, - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::Approved, - risk_level: Some(GuardianRiskLevel::Low), - user_authorization: Some(GuardianUserAuthorization::High), - rationale: Some("Request is scoped to report output.".into()), - decision_source: Some(GuardianAssessmentDecisionSource::Agent), - action, - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-request-permissions".into(), + target_item_id: None, + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::Approved, + risk_level: Some(GuardianRiskLevel::Low), + user_authorization: Some(GuardianUserAuthorization::High), + rationale: Some("Request is scoped to report output.".into()), + decision_source: Some(GuardianAssessmentDecisionSource::Agent), + action, }); let width: u16 = 110; @@ -275,43 +249,30 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() { cwd: test_path_buf("/tmp").abs(), }; - chat.handle_codex_event(Event { - id: "guardian-in-progress".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-1".into(), - target_item_id: Some("guardian-target-1".into()), - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::InProgress, - risk_level: None, - user_authorization: None, - rationale: None, - decision_source: None, - action: action.clone(), - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-1".into(), + target_item_id: Some("guardian-target-1".into()), + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::InProgress, + risk_level: None, + user_authorization: None, + rationale: None, + decision_source: None, + action: action.clone(), }); - chat.handle_codex_event(Event { - id: "guardian-warning".into(), - msg: EventMsg::GuardianWarning(WarningEvent { - message: "Automatic approval review timed out while evaluating the requested approval." - .into(), - }), - }); - chat.handle_codex_event(Event { - id: "guardian-assessment".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-1".into(), - target_item_id: Some("guardian-target-1".into()), - turn_id: "turn-1".into(), - status: GuardianAssessmentStatus::TimedOut, - risk_level: None, - user_authorization: None, - rationale: Some( - "Automatic approval review timed out while evaluating the requested approval." - .into(), - ), - decision_source: Some(GuardianAssessmentDecisionSource::Agent), - action, - }), + chat.on_warning("Automatic approval review timed out while evaluating the requested approval."); + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-1".into(), + target_item_id: Some("guardian-target-1".into()), + turn_id: "turn-1".into(), + status: GuardianAssessmentStatus::TimedOut, + risk_level: None, + user_authorization: None, + rationale: Some( + "Automatic approval review timed out while evaluating the requested approval.".into(), + ), + decision_source: Some(GuardianAssessmentDecisionSource::Agent), + action, }); let width: u16 = 140; @@ -541,23 +502,20 @@ async fn guardian_parallel_reviews_render_aggregate_status_snapshot() { ("guardian-1", "rm -rf '/tmp/guardian target 1'"), ("guardian-2", "rm -rf '/tmp/guardian target 2'"), ] { - chat.handle_codex_event(Event { - id: format!("event-{id}"), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: id.to_string(), - target_item_id: Some(format!("{id}-target")), - turn_id: "turn-1".to_string(), - status: GuardianAssessmentStatus::InProgress, - risk_level: None, - user_authorization: None, - rationale: None, - decision_source: None, - action: GuardianAssessmentAction::Command { - source: GuardianCommandSource::Shell, - command: command.to_string(), - cwd: test_path_buf("/tmp").abs(), - }, - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: id.to_string(), + target_item_id: Some(format!("{id}-target")), + turn_id: "turn-1".to_string(), + status: GuardianAssessmentStatus::InProgress, + risk_level: None, + user_authorization: None, + rationale: None, + decision_source: None, + action: GuardianAssessmentAction::Command { + source: GuardianCommandSource::Shell, + command: command.to_string(), + cwd: test_path_buf("/tmp").abs(), + }, }); } @@ -573,59 +531,50 @@ async fn guardian_parallel_reviews_keep_remaining_review_visible_after_denial() let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.on_task_started(); - chat.handle_codex_event(Event { - id: "event-guardian-1".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-1".to_string(), - target_item_id: Some("guardian-1-target".to_string()), - turn_id: "turn-1".to_string(), - status: GuardianAssessmentStatus::InProgress, - risk_level: None, - user_authorization: None, - rationale: None, - decision_source: None, - action: GuardianAssessmentAction::Command { - source: GuardianCommandSource::Shell, - command: "rm -rf '/tmp/guardian target 1'".to_string(), - cwd: test_path_buf("/tmp").abs(), - }, - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-1".to_string(), + target_item_id: Some("guardian-1-target".to_string()), + turn_id: "turn-1".to_string(), + status: GuardianAssessmentStatus::InProgress, + risk_level: None, + user_authorization: None, + rationale: None, + decision_source: None, + action: GuardianAssessmentAction::Command { + source: GuardianCommandSource::Shell, + command: "rm -rf '/tmp/guardian target 1'".to_string(), + cwd: test_path_buf("/tmp").abs(), + }, }); - chat.handle_codex_event(Event { - id: "event-guardian-2".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-2".to_string(), - target_item_id: Some("guardian-2-target".to_string()), - turn_id: "turn-1".to_string(), - status: GuardianAssessmentStatus::InProgress, - risk_level: None, - user_authorization: None, - rationale: None, - decision_source: None, - action: GuardianAssessmentAction::Command { - source: GuardianCommandSource::Shell, - command: "rm -rf '/tmp/guardian target 2'".to_string(), - cwd: test_path_buf("/tmp").abs(), - }, - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-2".to_string(), + target_item_id: Some("guardian-2-target".to_string()), + turn_id: "turn-1".to_string(), + status: GuardianAssessmentStatus::InProgress, + risk_level: None, + user_authorization: None, + rationale: None, + decision_source: None, + action: GuardianAssessmentAction::Command { + source: GuardianCommandSource::Shell, + command: "rm -rf '/tmp/guardian target 2'".to_string(), + cwd: test_path_buf("/tmp").abs(), + }, }); - chat.handle_codex_event(Event { - id: "event-guardian-1-denied".into(), - msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent { - id: "guardian-1".to_string(), - target_item_id: Some("guardian-1-target".to_string()), - turn_id: "turn-1".to_string(), - status: GuardianAssessmentStatus::Denied, - risk_level: Some(GuardianRiskLevel::High), - user_authorization: Some(GuardianUserAuthorization::Low), - rationale: Some("Would delete important data.".to_string()), - decision_source: Some(GuardianAssessmentDecisionSource::Agent), - action: GuardianAssessmentAction::Command { - source: GuardianCommandSource::Shell, - command: "rm -rf '/tmp/guardian target 1'".to_string(), - cwd: test_path_buf("/tmp").abs(), - }, - }), + chat.on_guardian_assessment(GuardianAssessmentEvent { + id: "guardian-1".to_string(), + target_item_id: Some("guardian-1-target".to_string()), + turn_id: "turn-1".to_string(), + status: GuardianAssessmentStatus::Denied, + risk_level: Some(GuardianRiskLevel::High), + user_authorization: Some(GuardianUserAuthorization::Low), + rationale: Some("Would delete important data.".to_string()), + decision_source: Some(GuardianAssessmentDecisionSource::Agent), + action: GuardianAssessmentAction::Command { + source: GuardianCommandSource::Shell, + command: "rm -rf '/tmp/guardian target 1'".to_string(), + cwd: test_path_buf("/tmp").abs(), + }, }); assert_eq!(chat.current_status.header, "Reviewing approval request"); diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 570805f272..a4dd8baaf3 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -497,6 +497,405 @@ pub(super) fn make_token_info(total_tokens: i64, context_window: i64) -> TokenUs } } +fn thread_id(chat: &ChatWidget) -> String { + chat.thread_id.map(|id| id.to_string()).unwrap_or_default() +} + +fn token_usage_breakdown(usage: TokenUsage) -> codex_app_server_protocol::TokenUsageBreakdown { + codex_app_server_protocol::TokenUsageBreakdown { + total_tokens: usage.total_tokens, + input_tokens: usage.input_tokens, + cached_input_tokens: usage.cached_input_tokens, + output_tokens: usage.output_tokens, + reasoning_output_tokens: usage.reasoning_output_tokens, + } +} + +pub(super) fn handle_token_count(chat: &mut ChatWidget, info: Option) { + match info { + Some(info) => { + chat.handle_server_notification( + ServerNotification::ThreadTokenUsageUpdated( + codex_app_server_protocol::ThreadTokenUsageUpdatedNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + token_usage: codex_app_server_protocol::ThreadTokenUsage { + total: token_usage_breakdown(info.total_token_usage), + last: token_usage_breakdown(info.last_token_usage), + model_context_window: info.model_context_window, + }, + }, + ), + /*replay_kind*/ None, + ); + } + None => chat.set_token_info(None), + } +} + +pub(super) fn handle_error( + chat: &mut ChatWidget, + message: impl Into, + codex_error_info: Option, +) { + chat.handle_server_notification( + ServerNotification::Error(ErrorNotification { + error: AppServerTurnError { + message: message.into(), + codex_error_info, + additional_details: None, + }, + will_retry: false, + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_stream_error( + chat: &mut ChatWidget, + message: impl Into, + additional_details: Option, +) { + handle_stream_error_with_replay(chat, message, additional_details, /*replay_kind*/ None); +} + +pub(super) fn handle_stream_error_with_replay( + chat: &mut ChatWidget, + message: impl Into, + additional_details: Option, + replay_kind: Option, +) { + chat.handle_server_notification( + ServerNotification::Error(ErrorNotification { + error: AppServerTurnError { + message: message.into(), + codex_error_info: None, + additional_details, + }, + will_retry: true, + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + }), + replay_kind, + ); +} + +pub(super) fn handle_warning(chat: &mut ChatWidget, message: impl Into) { + chat.handle_server_notification( + ServerNotification::Warning(WarningNotification { + thread_id: Some(thread_id(chat)), + message: message.into(), + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_model_verification( + chat: &mut ChatWidget, + verifications: Vec, +) { + chat.handle_server_notification( + ServerNotification::ModelVerification(ModelVerificationNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + verifications, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_agent_message_delta(chat: &mut ChatWidget, delta: impl Into) { + chat.handle_server_notification( + ServerNotification::AgentMessageDelta( + codex_app_server_protocol::AgentMessageDeltaNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item_id: "msg-1".to_string(), + delta: delta.into(), + }, + ), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_agent_reasoning_delta(chat: &mut ChatWidget, delta: impl Into) { + chat.handle_server_notification( + ServerNotification::ReasoningSummaryTextDelta(ReasoningSummaryTextDeltaNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item_id: "reasoning-1".to_string(), + delta: delta.into(), + summary_index: 0, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_agent_reasoning_final(chat: &mut ChatWidget) { + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item: AppServerThreadItem::Reasoning { + id: "reasoning-1".to_string(), + summary: Vec::new(), + content: Vec::new(), + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_entered_review_mode(chat: &mut ChatWidget, review: impl Into) { + chat.handle_server_notification( + ServerNotification::ItemStarted(ItemStartedNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item: AppServerThreadItem::EnteredReviewMode { + id: "review-start".to_string(), + review: review.into(), + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn replay_entered_review_mode(chat: &mut ChatWidget, review: impl Into) { + chat.replay_thread_item( + AppServerThreadItem::EnteredReviewMode { + id: "review-start".to_string(), + review: review.into(), + }, + "turn-1".to_string(), + ReplayKind::ThreadSnapshot, + ); +} + +pub(super) fn handle_exited_review_mode(chat: &mut ChatWidget) { + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item: AppServerThreadItem::ExitedReviewMode { + id: "review-end".to_string(), + review: String::new(), + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_exec_approval_request( + chat: &mut ChatWidget, + id: impl Into, + event: ExecApprovalRequestEvent, +) { + chat.on_exec_approval_request(id.into(), event); +} + +pub(super) fn handle_apply_patch_approval_request( + chat: &mut ChatWidget, + id: impl Into, + event: ApplyPatchApprovalRequestEvent, +) { + chat.on_apply_patch_approval_request(id.into(), event); +} + +fn file_update_changes_from_tui(changes: HashMap) -> Vec { + changes + .into_iter() + .map(|(path, change)| { + let (kind, diff) = match change { + FileChange::Add { content } => (PatchChangeKind::Add, content), + FileChange::Delete { content } => (PatchChangeKind::Delete, content), + FileChange::Update { + unified_diff, + move_path, + } => (PatchChangeKind::Update { move_path }, unified_diff), + }; + FileUpdateChange { + path: path.display().to_string(), + kind, + diff, + } + }) + .collect() +} + +pub(super) fn handle_patch_apply_begin(chat: &mut ChatWidget, event: PatchApplyBeginEvent) { + chat.handle_server_notification( + ServerNotification::ItemStarted(ItemStartedNotification { + thread_id: thread_id(chat), + turn_id: event.turn_id.clone(), + item: AppServerThreadItem::FileChange { + id: event.call_id, + changes: file_update_changes_from_tui(event.changes), + status: AppServerPatchApplyStatus::InProgress, + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_patch_apply_end(chat: &mut ChatWidget, event: PatchApplyEndEvent) { + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: thread_id(chat), + turn_id: event.turn_id.clone(), + item: AppServerThreadItem::FileChange { + id: event.call_id, + changes: file_update_changes_from_tui(event.changes), + status: event.status, + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_view_image_tool_call(chat: &mut ChatWidget, event: ViewImageToolCallEvent) { + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: thread_id(chat), + turn_id: "turn-1".to_string(), + item: AppServerThreadItem::ImageView { + id: event.call_id, + path: event.path, + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_image_generation_end(chat: &mut ChatWidget, event: ImageGenerationEndEvent) { + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: thread_id(chat), + turn_id: "turn-1".to_string(), + item: AppServerThreadItem::ImageGeneration { + id: event.call_id, + status: event.status, + revised_prompt: event.revised_prompt, + result: event.result, + saved_path: event.saved_path, + }, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn replay_user_message_inputs( + chat: &mut ChatWidget, + item_id: &str, + content: Vec, + replay_kind: ReplayKind, +) { + chat.replay_thread_item( + AppServerThreadItem::UserMessage { + id: item_id.to_string(), + content, + }, + "turn-1".to_string(), + replay_kind, + ); +} + +pub(super) fn replay_user_message_text( + chat: &mut ChatWidget, + item_id: &str, + text: impl Into, + replay_kind: ReplayKind, +) { + replay_user_message_inputs( + chat, + item_id, + vec![AppServerUserInput::Text { + text: text.into(), + text_elements: Vec::new(), + }], + replay_kind, + ); +} + +pub(super) fn replay_agent_message( + chat: &mut ChatWidget, + item_id: &str, + text: impl Into, + replay_kind: ReplayKind, +) { + chat.replay_thread_item( + AppServerThreadItem::AgentMessage { + id: item_id.to_string(), + text: text.into(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + }, + "turn-1".to_string(), + replay_kind, + ); +} + +pub(super) fn replay_turn_started(chat: &mut ChatWidget, replay_kind: ReplayKind) { + chat.handle_server_notification( + ServerNotification::TurnStarted(TurnStartedNotification { + thread_id: thread_id(chat), + turn: app_server_turn( + "turn-1", + AppServerTurnStatus::InProgress, + /*duration_ms*/ None, + /*error*/ None, + ), + }), + Some(replay_kind), + ); +} + +pub(super) fn replay_agent_message_delta( + chat: &mut ChatWidget, + delta: impl Into, + replay_kind: ReplayKind, +) { + chat.handle_server_notification( + ServerNotification::AgentMessageDelta( + codex_app_server_protocol::AgentMessageDeltaNotification { + thread_id: thread_id(chat), + turn_id: "turn-1".to_string(), + item_id: "msg-1".to_string(), + delta: delta.into(), + }, + ), + Some(replay_kind), + ); +} + // --- Small helpers to tersely drive exec begin/end and snapshot active cell --- pub(super) fn begin_exec_with_source( chat: &mut ChatWidget, @@ -521,10 +920,7 @@ pub(super) fn begin_exec_with_source( source, interaction_input, }; - chat.handle_codex_event(Event { - id: call_id.to_string(), - msg: EventMsg::ExecCommandBegin(event.clone()), - }); + handle_exec_begin(chat, event.clone()); event } @@ -546,27 +942,89 @@ pub(super) fn begin_unified_exec_startup( source: ExecCommandSource::UnifiedExecStartup, interaction_input: None, }; - chat.handle_codex_event(Event { - id: call_id.to_string(), - msg: EventMsg::ExecCommandBegin(event.clone()), - }); + handle_exec_begin(chat, event.clone()); event } +pub(super) fn handle_exec_begin(chat: &mut ChatWidget, event: ExecCommandBeginEvent) { + let command_actions = event + .parsed_cmd + .iter() + .cloned() + .map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &event.cwd)) + .collect(); + chat.handle_server_notification( + ServerNotification::ItemStarted(ItemStartedNotification { + thread_id: thread_id(chat), + turn_id: event.turn_id.clone(), + item: AppServerThreadItem::CommandExecution { + id: event.call_id, + command: codex_shell_command::parse_command::shlex_join(&event.command), + cwd: event.cwd, + process_id: event.process_id, + source: event.source, + status: AppServerCommandExecutionStatus::InProgress, + command_actions, + aggregated_output: None, + exit_code: None, + duration_ms: None, + }, + }), + /*replay_kind*/ None, + ); +} + pub(super) fn terminal_interaction( chat: &mut ChatWidget, call_id: &str, process_id: &str, stdin: &str, ) { - chat.handle_codex_event(Event { - id: call_id.to_string(), - msg: EventMsg::TerminalInteraction(TerminalInteractionEvent { - call_id: call_id.to_string(), - process_id: process_id.to_string(), - stdin: stdin.to_string(), - }), - }); + chat.handle_server_notification( + ServerNotification::TerminalInteraction( + codex_app_server_protocol::TerminalInteractionNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item_id: call_id.to_string(), + process_id: process_id.to_string(), + stdin: stdin.to_string(), + }, + ), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_terminal_interaction(chat: &mut ChatWidget, event: TerminalInteractionEvent) { + chat.handle_server_notification( + ServerNotification::TerminalInteraction( + codex_app_server_protocol::TerminalInteractionNotification { + thread_id: thread_id(chat), + turn_id: chat + .last_turn_id + .clone() + .unwrap_or_else(|| "turn-1".to_string()), + item_id: event.call_id, + process_id: event.process_id, + stdin: event.stdin, + }, + ), + /*replay_kind*/ None, + ); +} + +pub(super) fn terminal_interaction_event( + call_id: &str, + process_id: &str, + stdin: &str, +) -> TerminalInteractionEvent { + TerminalInteractionEvent { + call_id: call_id.to_string(), + process_id: process_id.to_string(), + stdin: stdin.to_string(), + } } pub(super) fn complete_assistant_message( @@ -575,21 +1033,19 @@ pub(super) fn complete_assistant_message( text: &str, phase: Option, ) { - chat.handle_codex_event(Event { - id: format!("raw-{item_id}"), - msg: EventMsg::ItemCompleted(ItemCompletedEvent { - thread_id: ThreadId::new(), + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), turn_id: "turn-1".to_string(), - item: TurnItem::AgentMessage(AgentMessageItem { + item: AppServerThreadItem::AgentMessage { id: item_id.to_string(), - content: vec![AgentMessageContent::Text { - text: text.to_string(), - }], + text: text.to_string(), phase, memory_citation: None, - }), + }, }), - }); + /*replay_kind*/ None, + ); } pub(super) fn pending_steer(text: &str) -> PendingSteer { @@ -619,17 +1075,88 @@ pub(super) fn complete_user_message_for_inputs( item_id: &str, content: Vec, ) { - chat.handle_codex_event(Event { - id: format!("raw-{item_id}"), - msg: EventMsg::ItemCompleted(ItemCompletedEvent { - thread_id: ThreadId::new(), + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), turn_id: "turn-1".to_string(), - item: TurnItem::UserMessage(UserMessageItem { + item: AppServerThreadItem::UserMessage { id: item_id.to_string(), - content, - }), + content: content.into_iter().map(AppServerUserInput::from).collect(), + }, }), - }); + /*replay_kind*/ None, + ); +} + +pub(super) fn app_server_turn( + turn_id: &str, + status: AppServerTurnStatus, + duration_ms: Option, + error: Option, +) -> AppServerTurn { + AppServerTurn { + id: turn_id.to_string(), + items: Vec::new(), + status, + error, + started_at: None, + completed_at: None, + duration_ms, + } +} + +pub(super) fn handle_turn_started(chat: &mut ChatWidget, turn_id: &str) { + chat.handle_server_notification( + ServerNotification::TurnStarted(TurnStartedNotification { + thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), + turn: app_server_turn( + turn_id, + AppServerTurnStatus::InProgress, + /*duration_ms*/ None, + /*error*/ None, + ), + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_turn_completed( + chat: &mut ChatWidget, + turn_id: &str, + duration_ms: Option, +) { + chat.handle_server_notification( + ServerNotification::TurnCompleted(TurnCompletedNotification { + thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), + turn: app_server_turn( + turn_id, + AppServerTurnStatus::Completed, + duration_ms, + /*error*/ None, + ), + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_turn_interrupted(chat: &mut ChatWidget, turn_id: &str) { + chat.handle_server_notification( + ServerNotification::TurnCompleted(TurnCompletedNotification { + thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(), + turn: app_server_turn( + turn_id, + AppServerTurnStatus::Interrupted, + /*duration_ms*/ None, + /*error*/ None, + ), + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_budget_limited_turn(chat: &mut ChatWidget, turn_id: &str) { + chat.budget_limited_turn_ids.insert(turn_id.to_string()); + handle_turn_interrupted(chat, turn_id); } pub(super) fn begin_exec( @@ -662,9 +1189,9 @@ pub(super) fn end_exec( interaction_input, process_id, } = begin_event; - chat.handle_codex_event(Event { - id: call_id.clone(), - msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { + handle_exec_end( + chat, + ExecCommandEndEvent { call_id, process_id, turn_id, @@ -680,12 +1207,42 @@ pub(super) fn end_exec( duration: std::time::Duration::from_millis(5), formatted_output: aggregated, status: if exit_code == 0 { - CoreExecCommandStatus::Completed + AppServerCommandExecutionStatus::Completed } else { - CoreExecCommandStatus::Failed + AppServerCommandExecutionStatus::Failed + }, + }, + ); +} + +pub(super) fn handle_exec_end(chat: &mut ChatWidget, event: ExecCommandEndEvent) { + let duration_ms = i64::try_from(event.duration.as_millis()).unwrap_or(i64::MAX); + let command_actions = event + .parsed_cmd + .iter() + .cloned() + .map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &event.cwd)) + .collect(); + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: thread_id(chat), + turn_id: event.turn_id.clone(), + item: AppServerThreadItem::CommandExecution { + id: event.call_id, + command: codex_shell_command::parse_command::shlex_join(&event.command), + cwd: event.cwd, + process_id: event.process_id, + source: event.source, + status: event.status, + command_actions, + aggregated_output: (!event.aggregated_output.is_empty()) + .then_some(event.aggregated_output), + exit_code: Some(event.exit_code), + duration_ms: Some(duration_ms), }, }), - }); + /*replay_kind*/ None, + ); } pub(super) fn active_blob(chat: &ChatWidget) -> String { @@ -1015,6 +1572,67 @@ pub(super) fn type_plugins_search_query(chat: &mut ChatWidget, query: &str) { } } +pub(super) fn handle_hook_started(chat: &mut ChatWidget, run: AppServerHookRunSummary) { + chat.handle_server_notification( + ServerNotification::HookStarted(AppServerHookStartedNotification { + thread_id: thread_id(chat), + turn_id: None, + run, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn handle_hook_completed(chat: &mut ChatWidget, run: AppServerHookRunSummary) { + chat.handle_server_notification( + ServerNotification::HookCompleted(AppServerHookCompletedNotification { + thread_id: thread_id(chat), + turn_id: None, + run, + }), + /*replay_kind*/ None, + ); +} + +pub(super) fn hook_run( + run_id: &str, + event_name: codex_app_server_protocol::HookEventName, + status: codex_app_server_protocol::HookRunStatus, + status_message: &str, + entries: Vec, +) -> codex_app_server_protocol::HookRunSummary { + codex_app_server_protocol::HookRunSummary { + id: run_id.to_string(), + event_name, + handler_type: codex_app_server_protocol::HookHandlerType::Command, + execution_mode: codex_app_server_protocol::HookExecutionMode::Sync, + scope: codex_app_server_protocol::HookScope::Turn, + source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(), + source: codex_app_server_protocol::HookSource::User, + display_order: 0, + status, + status_message: Some(status_message.to_string()), + started_at: 1, + completed_at: matches!( + status, + codex_app_server_protocol::HookRunStatus::Completed + | codex_app_server_protocol::HookRunStatus::Failed + | codex_app_server_protocol::HookRunStatus::Blocked + | codex_app_server_protocol::HookRunStatus::Stopped + ) + .then_some(11), + duration_ms: matches!( + status, + codex_app_server_protocol::HookRunStatus::Completed + | codex_app_server_protocol::HookRunStatus::Failed + | codex_app_server_protocol::HookRunStatus::Blocked + | codex_app_server_protocol::HookRunStatus::Stopped + ) + .then_some(10), + entries, + } +} + pub(super) async fn assert_hook_events_snapshot( event_name: codex_app_server_protocol::HookEventName, run_id: &str, @@ -1023,28 +1641,16 @@ pub(super) async fn assert_hook_events_snapshot( ) { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "hook-1".into(), - msg: EventMsg::HookStarted(crate::tool_activity::HookStartedEvent { - turn_id: None, - run: codex_app_server_protocol::HookRunSummary { - id: run_id.to_string(), - event_name, - handler_type: codex_app_server_protocol::HookHandlerType::Command, - execution_mode: codex_app_server_protocol::HookExecutionMode::Sync, - scope: codex_app_server_protocol::HookScope::Turn, - source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(), - source: codex_app_server_protocol::HookSource::User, - display_order: 0, - status: codex_app_server_protocol::HookRunStatus::Running, - status_message: Some(status_message.to_string()), - started_at: 1, - completed_at: None, - duration_ms: None, - entries: vec![], - }, - }), - }); + handle_hook_started( + &mut chat, + hook_run( + run_id, + event_name, + codex_app_server_protocol::HookRunStatus::Running, + status_message, + Vec::new(), + ), + ); assert!( drain_insert_history(&mut rx).is_empty(), "hook start should update the live hook cell instead of writing history" @@ -1058,37 +1664,25 @@ pub(super) async fn assert_hook_events_snapshot( "hook start should render in the live hook cell" ); - chat.handle_codex_event(Event { - id: "hook-1".into(), - msg: EventMsg::HookCompleted(crate::tool_activity::HookCompletedEvent { - turn_id: None, - run: codex_app_server_protocol::HookRunSummary { - id: run_id.to_string(), - event_name, - handler_type: codex_app_server_protocol::HookHandlerType::Command, - execution_mode: codex_app_server_protocol::HookExecutionMode::Sync, - scope: codex_app_server_protocol::HookScope::Turn, - source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(), - source: codex_app_server_protocol::HookSource::User, - display_order: 0, - status: codex_app_server_protocol::HookRunStatus::Completed, - status_message: Some(status_message.to_string()), - started_at: 1, - completed_at: Some(11), - duration_ms: Some(10), - entries: vec![ - codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Warning, - text: "Heads up from the hook".to_string(), - }, - codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Context, - text: "Remember the startup checklist.".to_string(), - }, - ], - }, - }), - }); + handle_hook_completed( + &mut chat, + hook_run( + run_id, + event_name, + codex_app_server_protocol::HookRunStatus::Completed, + status_message, + vec![ + codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Warning, + text: "Heads up from the hook".to_string(), + }, + codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Context, + text: "Remember the startup checklist.".to_string(), + }, + ], + ), + ); let cells = drain_insert_history(&mut rx); let combined = cells diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index ea420e8638..8e5bc2029d 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -16,41 +16,39 @@ async fn resumed_initial_messages_render_history() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: Some(vec![ - EventMsg::UserMessage(UserMessageEvent { - message: "hello from user".to_string(), - images: None, - text_elements: Vec::new(), - local_images: Vec::new(), - }), - EventMsg::AgentMessage(AgentMessageEvent { - message: "assistant reply".to_string(), - phase: None, - memory_citation: None, - }), - ]), network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); + replay_user_message_text( + &mut chat, + "user-1", + "hello from user", + ReplayKind::ResumeInitialMessages, + ); + replay_agent_message( + &mut chat, + "assistant-1", + "assistant reply", + ReplayKind::ResumeInitialMessages, + ); let cells = drain_insert_history(&mut rx); let mut merged_lines = Vec::new(); @@ -74,47 +72,6 @@ async fn resumed_initial_messages_render_history() { ); } -#[tokio::test] -async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() { - let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.handle_codex_event_replay(Event { - id: "turn-1".into(), - msg: EventMsg::ItemCompleted(ItemCompletedEvent { - thread_id: ThreadId::new(), - turn_id: "turn-1".to_string(), - item: TurnItem::AgentMessage(AgentMessageItem { - id: "msg-1".to_string(), - content: vec![AgentMessageContent::Text { - text: "assistant reply".to_string(), - }], - phase: None, - memory_citation: None, - }), - }), - }); - chat.handle_codex_event_replay(Event { - id: "turn-1".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "assistant reply".to_string(), - phase: None, - memory_citation: None, - }), - }); - - let cells = drain_insert_history(&mut rx); - assert_eq!( - cells.len(), - 1, - "expected replayed assistant message to render once" - ); - let rendered = lines_to_single_string(&cells[0]); - assert!( - rendered.contains("assistant reply"), - "expected replayed assistant message, got {rendered:?}" - ); -} - #[tokio::test] async fn replayed_user_message_preserves_text_elements_and_local_images() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; @@ -129,34 +86,41 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent { - message: message.clone(), - images: None, - text_elements: text_elements.clone(), - local_images: local_images.clone(), - })]), network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); + replay_user_message_inputs( + &mut chat, + "user-1", + vec![ + AppServerUserInput::Text { + text: message.clone(), + text_elements: text_elements.clone().into_iter().map(Into::into).collect(), + }, + AppServerUserInput::LocalImage { + path: local_images[0].clone(), + }, + ], + ReplayKind::ResumeInitialMessages, + ); let mut user_cell = None; while let Ok(ev) = rx.try_recv() { @@ -190,34 +154,41 @@ async fn replayed_user_message_preserves_remote_image_urls() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent { - message: message.clone(), - images: Some(remote_image_urls.clone()), - text_elements: Vec::new(), - local_images: Vec::new(), - })]), network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); + replay_user_message_inputs( + &mut chat, + "user-1", + vec![ + AppServerUserInput::Text { + text: message.clone(), + text_elements: Vec::new(), + }, + AppServerUserInput::Image { + url: remote_image_urls[0].clone(), + }, + ], + ReplayKind::ResumeInitialMessages, + ); let mut user_cell = None; while let Ok(ev) = rx.try_recv() { @@ -282,9 +253,10 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { .to_legacy_sandbox_policy(expected_cwd.as_path()) .expect("permission profile should project to legacy sandbox policy"); let expected_sandbox = SandboxPolicy::from(expected_core_sandbox); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: ThreadId::new(), + let configured = crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -293,18 +265,15 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: expected_permission_profile, cwd: expected_cwd.clone(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: None, }; - chat.handle_codex_event(Event { - id: "session-configured".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); assert_eq!( AskForApproval::from(chat.config_ref().permissions.approval_policy.value()), @@ -340,9 +309,10 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { let expected_sandbox = SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, }; - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: ThreadId::new(), + let configured = crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -351,18 +321,15 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: expected_permission_profile, cwd: test_path_buf("/home/user/external").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: None, }; - chat.handle_codex_event(Event { - id: "session-configured".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); let actual_sandbox = SandboxPolicy::from(chat.config_ref().legacy_sandbox_policy()); assert_eq!(&actual_sandbox, &expected_sandbox); @@ -380,34 +347,35 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent { - message: String::new(), - images: Some(remote_image_urls.clone()), - text_elements: Vec::new(), - local_images: Vec::new(), - })]), network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); + replay_user_message_inputs( + &mut chat, + "user-1", + vec![AppServerUserInput::Image { + url: remote_image_urls[0].clone(), + }], + ReplayKind::ResumeInitialMessages, + ); let mut user_cell = None; while let Ok(ev) = rx.try_recv() { @@ -429,38 +397,39 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { async fn replayed_user_message_with_only_local_images_does_not_render_history_cell() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - let local_images = vec![PathBuf::from("/tmp/replay-local-only.png")]; + let local_images = [PathBuf::from("/tmp/replay-local-only.png")]; let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), + permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent { - message: String::new(), - images: None, - text_elements: Vec::new(), - local_images, - })]), network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); + replay_user_message_inputs( + &mut chat, + "user-1", + vec![AppServerUserInput::LocalImage { + path: local_images[0].clone(), + }], + ReplayKind::ResumeInitialMessages, + ); let mut found_user_history_cell = false; while let Ok(ev) = rx.try_recv() { @@ -620,72 +589,21 @@ async fn app_server_forked_thread_history_line_without_app_server_name_ignores_l async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event_replay(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("current changes".to_string()), - }); + replay_entered_review_mode(&mut chat, "current changes"); let _ = drain_insert_history(&mut rx); - chat.handle_codex_event_replay(Event { - id: "review-message".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "Review progress update".to_string(), - phase: None, - memory_citation: None, - }), - }); + replay_agent_message( + &mut chat, + "review-message", + "Review progress update", + ReplayKind::ThreadSnapshot, + ); let inserted = drain_insert_history(&mut rx); assert_eq!(inserted.len(), 1); assert!(lines_to_single_string(&inserted[0]).contains("Review progress update")); } -#[tokio::test] -async fn replayed_thread_rollback_emits_ordered_app_event() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; - - chat.replay_initial_messages(vec![EventMsg::ThreadRolledBack(ThreadRolledBackEvent { - num_turns: 2, - })]); - - let mut saw = false; - while let Ok(event) = rx.try_recv() { - if let AppEvent::ApplyThreadRollback { num_turns } = event { - saw = true; - assert_eq!(num_turns, 2); - break; - } - } - - assert!(saw, "expected replay rollback app event"); -} - -#[tokio::test] -async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assistant_message() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - complete_assistant_message( - &mut chat, - "msg-live", - "hello", - Some(MessagePhase::FinalAnswer), - ); - let inserted = drain_insert_history(&mut rx); - assert_eq!(inserted.len(), 1); - assert!(lines_to_single_string(&inserted[0]).contains("hello")); - - chat.handle_codex_event(Event { - id: "legacy-live".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "hello".into(), - phase: Some(MessagePhase::FinalAnswer), - memory_citation: None, - }), - }); - - assert!(drain_insert_history(&mut rx).is_empty()); -} - #[tokio::test] async fn replayed_retryable_app_server_error_keeps_turn_running() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -749,26 +667,24 @@ async fn replayed_thread_closed_notification_does_not_exit_tui() { async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.config.show_raw_agent_reasoning = false; - chat.handle_codex_event(Event { - id: "configured".into(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: ThreadId::new(), - forked_from_id: None, - thread_name: None, - model: "test-model".to_string(), - model_provider_id: "test-provider".to_string(), - service_tier: None, - approval_policy: AskForApproval::Never, - approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), - cwd: test_project_path().abs(), - reasoning_effort: None, - history_log_id: 0, - history_entry_count: 0, - initial_messages: None, - network_proxy: None, - rollout_path: None, - }), + chat.handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), + forked_from_id: None, + fork_parent_title: None, + thread_name: None, + model: "test-model".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::Never, + approvals_reviewer: ApprovalsReviewer::User, + permission_profile: PermissionProfile::read_only(), + cwd: test_project_path().abs(), + instruction_source_paths: Vec::new(), + reasoning_effort: None, + history_log_id: 0, + history_entry_count: 0, + network_proxy: None, + rollout_path: None, }); let _ = drain_insert_history(&mut rx); @@ -796,26 +712,24 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() { async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.config.show_raw_agent_reasoning = true; - chat.handle_codex_event(Event { - id: "configured".into(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: ThreadId::new(), - forked_from_id: None, - thread_name: None, - model: "test-model".to_string(), - model_provider_id: "test-provider".to_string(), - service_tier: None, - approval_policy: AskForApproval::Never, - approvals_reviewer: ApprovalsReviewer::User, - permission_profile: codex_protocol::models::PermissionProfile::read_only(), - cwd: test_project_path().abs(), - reasoning_effort: None, - history_log_id: 0, - history_entry_count: 0, - initial_messages: None, - network_proxy: None, - rollout_path: None, - }), + chat.handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), + forked_from_id: None, + fork_parent_title: None, + thread_name: None, + model: "test-model".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::Never, + approvals_reviewer: ApprovalsReviewer::User, + permission_profile: PermissionProfile::read_only(), + cwd: test_project_path().abs(), + instruction_source_paths: Vec::new(), + reasoning_effort: None, + history_log_id: 0, + history_entry_count: 0, + network_proxy: None, + rollout_path: None, }); let _ = drain_insert_history(&mut rx); @@ -893,34 +807,11 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() { assert_eq!(rendered.matches("Summary only").count(), 1); } -#[tokio::test] -async fn replayed_turn_started_does_not_mark_task_running() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.replay_initial_messages(vec![EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - })]); - - assert!(!chat.bottom_pane.is_task_running()); - assert!(chat.bottom_pane.status_widget().is_none()); -} - #[tokio::test] async fn thread_snapshot_replayed_turn_started_marks_task_running() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event_replay(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + replay_turn_started(&mut chat, ReplayKind::ThreadSnapshot); drain_insert_history(&mut rx); assert!(chat.bottom_pane.is_task_running()); @@ -962,11 +853,12 @@ async fn replayed_stream_error_does_not_set_retry_status_or_status_indicator() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.set_status_header("Idle".to_string()); - chat.replay_initial_messages(vec![EventMsg::StreamError(StreamErrorEvent { - message: "Reconnecting... 2/5".to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: Some("Idle timeout waiting for SSE".to_string()), - })]); + handle_stream_error_with_replay( + &mut chat, + "Reconnecting... 2/5", + Some("Idle timeout waiting for SSE".to_string()), + Some(ReplayKind::ResumeInitialMessages), + ); let cells = drain_insert_history(&mut rx); assert!( @@ -982,33 +874,18 @@ async fn replayed_stream_error_does_not_set_retry_status_or_status_indicator() { async fn thread_snapshot_replayed_stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event_replay(Event { - id: "task".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + replay_turn_started(&mut chat, ReplayKind::ThreadSnapshot); drain_insert_history(&mut rx); - chat.handle_codex_event_replay(Event { - id: "retry".into(), - msg: EventMsg::StreamError(StreamErrorEvent { - message: "Reconnecting... 1/5".to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: None, - }), - }); + handle_stream_error_with_replay( + &mut chat, + "Reconnecting... 1/5", + /*additional_details*/ None, + Some(ReplayKind::ThreadSnapshot), + ); drain_insert_history(&mut rx); - chat.handle_codex_event_replay(Event { - id: "delta".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { - delta: "hello".to_string(), - }), - }); + replay_agent_message_delta(&mut chat, "hello", ReplayKind::ThreadSnapshot); let status = chat .bottom_pane @@ -1019,93 +896,18 @@ async fn thread_snapshot_replayed_stream_recovery_restores_previous_status_heade assert!(chat.retry_status_header.is_none()); } -#[tokio::test] -async fn resume_replay_interrupted_reconnect_does_not_leave_stale_working_state() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.set_status_header("Idle".to_string()); - - chat.replay_initial_messages(vec![ - EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - EventMsg::StreamError(StreamErrorEvent { - message: "Reconnecting... 1/5".to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: None, - }), - EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { - delta: "hello".to_string(), - }), - ]); - - let cells = drain_insert_history(&mut rx); - assert!( - cells.is_empty(), - "expected no history cells for replayed interrupted reconnect sequence" - ); - assert!(!chat.bottom_pane.is_task_running()); - assert!(chat.bottom_pane.status_widget().is_none()); - assert_eq!(chat.current_status.header, "Idle"); - assert!(chat.retry_status_header.is_none()); -} - -#[tokio::test] -async fn replayed_interrupted_reconnect_footer_row_snapshot() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.replay_initial_messages(vec![ - EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - EventMsg::StreamError(StreamErrorEvent { - message: "Reconnecting... 2/5".to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: Some("Idle timeout waiting for SSE".to_string()), - }), - ]); - - let header = render_bottom_first_row(&chat, /*width*/ 80); - assert!( - !header.contains("Reconnecting") && !header.contains("Working"), - "expected replayed interrupted reconnect to avoid active status row, got {header:?}" - ); - assert_chatwidget_snapshot!("replayed_interrupted_reconnect_footer_row", header); -} - #[tokio::test] async fn stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "task".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); drain_insert_history(&mut rx); - chat.handle_codex_event(Event { - id: "retry".into(), - msg: EventMsg::StreamError(StreamErrorEvent { - message: "Reconnecting... 1/5".to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: None, - }), - }); + handle_stream_error( + &mut chat, + "Reconnecting... 1/5", + /*additional_details*/ None, + ); drain_insert_history(&mut rx); - chat.handle_codex_event(Event { - id: "delta".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { - delta: "hello".to_string(), - }), - }); + handle_agent_message_delta(&mut chat, "hello"); let status = chat .bottom_pane diff --git a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs index 5e85fa7a8d..977bea2887 100644 --- a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs +++ b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs @@ -46,15 +46,7 @@ async fn mcp_startup_header_booting_snapshot() { async fn mcp_startup_complete_does_not_clear_running_task() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); assert!(chat.bottom_pane.is_task_running()); assert!(chat.bottom_pane.status_indicator_visible()); diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 67722fa261..f0461fc082 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -571,26 +571,24 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure .features .set_enabled(Feature::GuardianApproval, /*enabled*/ true); - chat.handle_codex_event(Event { - id: "session-configured".to_string(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: ThreadId::new(), - forked_from_id: None, - thread_name: None, - model: "gpt-test".to_string(), - model_provider_id: "test-provider".to_string(), - service_tier: None, - approval_policy: AskForApproval::OnRequest, - approvals_reviewer: ApprovalsReviewer::AutoReview, - permission_profile: PermissionProfile::workspace_write(), - cwd: test_project_path().abs(), - reasoning_effort: None, - history_log_id: 0, - history_entry_count: 0, - initial_messages: None, - network_proxy: None, - rollout_path: Some(PathBuf::new()), - }), + chat.handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), + forked_from_id: None, + fork_parent_title: None, + thread_name: None, + model: "gpt-test".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::AutoReview, + permission_profile: PermissionProfile::workspace_write(), + cwd: test_project_path().abs(), + instruction_source_paths: Vec::new(), + reasoning_effort: None, + history_log_id: 0, + history_entry_count: 0, + network_proxy: None, + rollout_path: Some(PathBuf::new()), }); chat.open_permissions_popup(); @@ -620,26 +618,24 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w let cwd = test_project_path().abs(); let permission_profile = app_server_workspace_write_profile(extra_root); - chat.handle_codex_event(Event { - id: "session-configured-custom-workspace".to_string(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: ThreadId::new(), - forked_from_id: None, - thread_name: None, - model: "gpt-test".to_string(), - model_provider_id: "test-provider".to_string(), - service_tier: None, - approval_policy: AskForApproval::OnRequest, - approvals_reviewer: ApprovalsReviewer::AutoReview, - permission_profile, - cwd, - reasoning_effort: None, - history_log_id: 0, - history_entry_count: 0, - initial_messages: None, - network_proxy: None, - rollout_path: Some(PathBuf::new()), - }), + chat.handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), + forked_from_id: None, + fork_parent_title: None, + thread_name: None, + model: "gpt-test".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::AutoReview, + permission_profile, + cwd, + instruction_source_paths: Vec::new(), + reasoning_effort: None, + history_log_id: 0, + history_entry_count: 0, + network_proxy: None, + rollout_path: Some(PathBuf::new()), }); chat.open_permissions_popup(); diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 829c199aab..0755758e90 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -802,13 +802,23 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() { .expect("expected plan collaboration mask"); chat.set_collaboration_mask(plan_mask); - chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Plan details".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })]); + chat.replay_thread_turns( + vec![AppServerTurn { + id: "turn-1".to_string(), + items: vec![AppServerThreadItem::AgentMessage { + id: "msg-plan".to_string(), + text: "Plan details".to_string(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + }], + status: AppServerTurnStatus::Completed, + error: None, + started_at: None, + completed_at: None, + duration_ms: None, + }], + ReplayKind::ResumeInitialMessages, + ); let popup = render_bottom_popup(&chat, /*width*/ 80); assert!( @@ -829,29 +839,36 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com chat.on_plan_delta("- Step 1\n- Step 2\n".to_string()); chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string()); - chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Plan details".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })]); + chat.replay_thread_turns( + vec![AppServerTurn { + id: "turn-1".to_string(), + items: vec![AppServerThreadItem::AgentMessage { + id: "msg-plan-replay".to_string(), + text: "Plan details".to_string(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + }], + status: AppServerTurnStatus::Completed, + error: None, + started_at: None, + completed_at: None, + duration_ms: None, + }], + ReplayKind::ResumeInitialMessages, + ); let replay_popup = render_bottom_popup(&chat, /*width*/ 80); assert!( !replay_popup.contains(PLAN_IMPLEMENTATION_TITLE), "expected no prompt for replayed turn completion, got {replay_popup:?}" ); - chat.handle_codex_event(Event { - id: "live-turn-complete-1".to_string(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Plan details".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + complete_assistant_message( + &mut chat, + "msg-plan-live-1", + "Plan details", + Some(MessagePhase::FinalAnswer), + ); + handle_turn_completed(&mut chat, "live-turn-complete-1", None); let popup = render_bottom_popup(&chat, /*width*/ 80); assert!( @@ -866,16 +883,13 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com "expected prompt to dismiss on Esc, got {dismissed_popup:?}" ); - chat.handle_codex_event(Event { - id: "live-turn-complete-2".to_string(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Plan details".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + complete_assistant_message( + &mut chat, + "msg-plan-live-2", + "Plan details", + Some(MessagePhase::FinalAnswer), + ); + handle_turn_completed(&mut chat, "live-turn-complete-2", None); let duplicate_popup = render_bottom_popup(&chat, /*width*/ 80); assert!( !duplicate_popup.contains(PLAN_IMPLEMENTATION_TITLE), @@ -1137,15 +1151,13 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() { other => panic!("expected running-turn compact steer submit, got {other:?}"), } - chat.handle_codex_event(Event { - id: "steer-rejected".into(), - msg: EventMsg::Error(ErrorEvent { - message: "cannot steer a compact turn".to_string(), - codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Compact, - }), + handle_error( + &mut chat, + "cannot steer a compact turn", + Some(CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Compact, }), - }); + ); assert!(chat.pending_steers.is_empty()); assert_eq!( @@ -1186,9 +1198,10 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: conversation_id, + let configured = crate::session_state::ThreadSessionState { + thread_id: conversation_id, forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -1197,17 +1210,14 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: Some(rollout_file.path().to_path_buf()), }; - chat.handle_codex_event(Event { - id: "initial".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); chat.bottom_pane .set_plugin_mentions(Some(vec![codex_plugin::PluginCapabilitySummary { @@ -1431,9 +1441,10 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true); - let configured = crate::chatwidget::test_events::SessionConfiguredEvent { - session_id: ThreadId::new(), + let configured = crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), forked_from_id: None, + fork_parent_title: None, thread_name: None, model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), @@ -1442,17 +1453,14 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { approvals_reviewer: ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, history_entry_count: 0, - initial_messages: None, network_proxy: None, rollout_path: None, }; - chat.handle_codex_event(Event { - id: "configured".into(), - msg: EventMsg::SessionConfigured(configured), - }); + chat.handle_thread_session(configured); chat.bottom_pane .set_composer_text("/plan build the plan".to_string(), Vec::new(), Vec::new()); @@ -1652,10 +1660,7 @@ async fn plan_update_renders_history_cell() { }, ], }; - chat.handle_codex_event(Event { - id: "sub-1".into(), - msg: EventMsg::PlanUpdate(update), - }); + chat.on_plan_update(update); let cells = drain_insert_history(&mut rx); assert!(!cells.is_empty(), "expected plan update cell to be sent"); let blob = lines_to_single_string(cells.last().unwrap()); diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 6169870139..1590575159 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -2204,13 +2204,11 @@ async fn server_overloaded_error_does_not_switch_models() { while rx.try_recv().is_ok() {} while op_rx.try_recv().is_ok() {} - chat.handle_codex_event(Event { - id: "err-1".to_string(), - msg: EventMsg::Error(ErrorEvent { - message: "server overloaded".to_string(), - codex_error_info: Some(CodexErrorInfo::ServerOverloaded), - }), - }); + handle_error( + &mut chat, + "server overloaded", + Some(CodexErrorInfo::ServerOverloaded), + ); while let Ok(event) = rx.try_recv() { if let AppEvent::UpdateModel(model) = event { diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index ef2621736e..787e4301b3 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -62,15 +62,7 @@ async fn interrupted_turn_restores_queued_messages_with_images_and_elements() { // When interrupted, queued messages are merged into the composer; image placeholders // must be renumbered to match the combined local image list. - chat.handle_codex_event(Event { - id: "interrupt".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); let first = "[Image #1] first".to_string(); let second = "[Image #2] second".to_string(); @@ -111,10 +103,7 @@ async fn interrupted_turn_restores_queued_messages_with_images_and_elements() { async fn entered_review_mode_uses_request_hint() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("feature branch".to_string()), - }); + handle_entered_review_mode(&mut chat, "feature branch"); let cells = drain_insert_history(&mut rx); let banner = lines_to_single_string(cells.last().expect("review banner")); @@ -127,10 +116,7 @@ async fn entered_review_mode_uses_request_hint() { async fn entered_review_mode_defaults_to_current_changes_banner() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("current changes".to_string()), - }); + handle_entered_review_mode(&mut chat, "current changes"); let cells = drain_insert_history(&mut rx); let banner = lines_to_single_string(cells.last().expect("review banner")); @@ -139,13 +125,10 @@ async fn entered_review_mode_defaults_to_current_changes_banner() { } #[tokio::test] -async fn live_core_review_prompt_item_is_not_rendered() { +async fn live_review_prompt_item_is_not_rendered() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("changes against 'main'".to_string()), - }); + handle_entered_review_mode(&mut chat, "changes against 'main'"); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1); assert!(lines_to_single_string(&cells[0]).contains("Code review started")); @@ -211,19 +194,8 @@ async fn live_app_server_review_prompt_item_is_not_rendered() { async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); - chat.handle_codex_event(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("feature branch".to_string()), - }); + handle_turn_started(&mut chat, "turn-1"); + handle_entered_review_mode(&mut chat, "feature branch"); let _ = drain_insert_history(&mut rx); chat.queued_user_messages .push_back(UserMessage::from("queued later").into()); @@ -253,24 +225,20 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages other => panic!("expected second running-turn steer submit, got {other:?}"), } - chat.handle_codex_event(Event { - id: "steer-rejected-1".into(), - msg: EventMsg::Error(ErrorEvent { - message: "cannot steer a review turn".to_string(), - codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Review, - }), + handle_error( + &mut chat, + "cannot steer a review turn", + Some(CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Review, }), - }); - chat.handle_codex_event(Event { - id: "steer-rejected-2".into(), - msg: EventMsg::Error(ErrorEvent { - message: "cannot steer a review turn".to_string(), - codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Review, - }), + ); + handle_error( + &mut chat, + "cannot steer a review turn", + Some(CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Review, }), - }); + ); assert!(chat.pending_steers.is_empty()); assert_eq!( @@ -283,20 +251,8 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages ); assert!(drain_insert_history(&mut rx).is_empty()); - chat.handle_codex_event(Event { - id: "review-exit".into(), - msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent), - }); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_exited_review_mode(&mut chat); + handle_turn_completed(&mut chat, "turn-1", None); match next_submit_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( @@ -309,16 +265,7 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages other => panic!("expected merged rejected-steer follow-up submit, got {other:?}"), } - chat.handle_codex_event(Event { - id: "turn-complete-2".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-2".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); match next_submit_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( @@ -336,20 +283,15 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages async fn live_agent_message_renders_during_review_mode() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("current changes".to_string()), - }); + handle_entered_review_mode(&mut chat, "current changes"); let _ = drain_insert_history(&mut rx); - chat.handle_codex_event(Event { - id: "review-message".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "Review progress update".to_string(), - phase: None, - memory_citation: None, - }), - }); + complete_assistant_message( + &mut chat, + "review-message", + "Review progress update", + /*phase*/ None, + ); let inserted = drain_insert_history(&mut rx); assert_eq!(inserted.len(), 1); @@ -365,33 +307,21 @@ async fn review_restores_context_window_indicator() { let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline. let review_tokens = 12_030; // ~97% remaining after subtracting baseline. - chat.handle_codex_event(Event { - id: "token-before".into(), - msg: EventMsg::TokenCount(TokenCountEvent { - info: Some(make_token_info(pre_review_tokens, context_window)), - rate_limits: None, - }), - }); + handle_token_count( + &mut chat, + Some(make_token_info(pre_review_tokens, context_window)), + ); assert_eq!(chat.bottom_pane.context_window_percent(), Some(30)); - chat.handle_codex_event(Event { - id: "review-start".into(), - msg: EventMsg::EnteredReviewMode("feature branch".to_string()), - }); + handle_entered_review_mode(&mut chat, "feature branch"); - chat.handle_codex_event(Event { - id: "token-review".into(), - msg: EventMsg::TokenCount(TokenCountEvent { - info: Some(make_token_info(review_tokens, context_window)), - rate_limits: None, - }), - }); + handle_token_count( + &mut chat, + Some(make_token_info(review_tokens, context_window)), + ); assert_eq!(chat.bottom_pane.context_window_percent(), Some(97)); - chat.handle_codex_event(Event { - id: "review-end".into(), - msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent), - }); + handle_exited_review_mode(&mut chat); let _ = drain_insert_history(&mut rx); assert_eq!(chat.bottom_pane.context_window_percent(), Some(30)); @@ -960,61 +890,6 @@ queued draft" assert_no_submit_op(&mut op_rx); } -#[tokio::test] -async fn replaced_turn_clears_pending_steers_but_keeps_queued_drafts() { - let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.thread_id = Some(ThreadId::new()); - chat.on_task_started(); - chat.on_agent_message_delta( - "Final answer line -" - .to_string(), - ); - - chat.bottom_pane - .set_composer_text("pending steer".to_string(), Vec::new(), Vec::new()); - chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - chat.queued_user_messages - .push_back(UserMessage::from("queued draft".to_string()).into()); - chat.refresh_pending_input_preview(); - - match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => assert_eq!( - items, - vec![UserInput::Text { - text: "pending steer".to_string(), - text_elements: Vec::new(), - }] - ), - other => panic!("expected Op::UserTurn, got {other:?}"), - } - assert!(drain_insert_history(&mut rx).is_empty()); - - chat.handle_codex_event(Event { - id: "replaced".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Replaced, - completed_at: None, - duration_ms: None, - }), - }); - - assert!(chat.pending_steers.is_empty()); - assert!(chat.queued_user_messages.is_empty()); - assert_eq!(chat.bottom_pane.composer_text(), ""); - match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => assert_eq!( - items, - vec![UserInput::Text { - text: "queued draft".to_string(), - text_elements: Vec::new(), - }] - ), - other => panic!("expected queued draft Op::UserTurn, got {other:?}"), - } -} - #[tokio::test] async fn ctrl_c_shutdown_works_with_caps_lock() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -1230,15 +1105,7 @@ async fn interrupt_exec_marks_failed_snapshot() { // Simulate the task being aborted (as if ESC was pressed), which should // cause the active exec cell to be finalized as failed and flushed. - chat.handle_codex_event(Event { - id: "call-int".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); let cells = drain_insert_history(&mut rx); assert!( @@ -1258,26 +1125,10 @@ async fn interrupted_turn_error_message_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // Simulate an in-progress task so the widget is in a running state. - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); // Abort the turn (like pressing Esc) and drain inserted history. - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); let cells = drain_insert_history(&mut rx); assert!( @@ -1356,24 +1207,8 @@ async fn interrupted_turn_after_goal_budget_limited_uses_budget_message_snapshot async fn direct_budget_limited_turn_uses_budget_message_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::BudgetLimited, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_started(&mut chat, "turn-1"); + handle_budget_limited_turn(&mut chat, "turn-1"); let cells = drain_insert_history(&mut rx); let last = lines_to_single_string(cells.last().unwrap()); @@ -1387,24 +1222,8 @@ async fn budget_limited_turn_restores_queued_input_without_submitting() { .push_back(UserMessage::from("follow-up after budget stop").into()); chat.refresh_pending_input_preview(); - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::BudgetLimited, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_started(&mut chat, "turn-1"); + handle_budget_limited_turn(&mut chat, "turn-1"); assert!(chat.queued_user_messages.is_empty()); assert_eq!( @@ -1424,25 +1243,9 @@ async fn interrupted_turn_pending_steers_message_snapshot() { chat.pending_steers.push_back(pending_steer("steer 1")); chat.submit_pending_steers_after_interrupt = true; - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); let cells = drain_insert_history(&mut rx); let info = cells @@ -1524,63 +1327,13 @@ async fn review_branch_picker_escape_navigates_back_then_dismisses() { ); } -#[tokio::test] -async fn review_ended_keeps_unified_exec_processes() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - begin_unified_exec_startup(&mut chat, "call-1", "process-1", "sleep 5"); - begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6"); - assert_eq!(chat.unified_exec_processes.len(), 2); - - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::ReviewEnded, - completed_at: None, - duration_ms: None, - }), - }); - - assert_eq!(chat.unified_exec_processes.len(), 2); - - chat.add_ps_output(); - let cells = drain_insert_history(&mut rx); - let combined = cells - .iter() - .map(|lines| lines_to_single_string(lines)) - .collect::>() - .join("\n"); - assert!( - combined.contains("Background terminals"), - "expected /ps to remain available after review-ended abort; got {combined:?}" - ); - assert!( - combined.contains("sleep 5") && combined.contains("sleep 6"), - "expected /ps to list running unified exec processes; got {combined:?}" - ); - - let _ = drain_insert_history(&mut rx); -} - #[tokio::test] async fn enter_submits_steer_while_review_is_running() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); - chat.handle_codex_event(Event { - id: "review-1".into(), - msg: EventMsg::EnteredReviewMode("current changes".to_string()), - }); + handle_entered_review_mode(&mut chat, "current changes"); let _ = drain_insert_history(&mut rx); chat.bottom_pane.set_composer_text( @@ -1613,34 +1366,21 @@ async fn enter_submits_steer_while_review_is_running() { async fn review_queues_user_messages_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); - chat.handle_codex_event(Event { - id: "review-1".into(), - msg: EventMsg::EnteredReviewMode("current changes".to_string()), - }); + handle_entered_review_mode(&mut chat, "current changes"); let _ = drain_insert_history(&mut rx); chat.submit_user_message(UserMessage::from( "Steer submitted while /review was running.".to_string(), )); - chat.handle_codex_event(Event { - id: "steer-rejected".into(), - msg: EventMsg::Error(ErrorEvent { - message: "cannot steer a review turn".to_string(), - codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Review, - }), + handle_error( + &mut chat, + "cannot steer a review turn", + Some(CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Review, }), - }); + ); let width: u16 = 80; let height: u16 = 18; diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index fb92507a6b..288a2fa699 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1,12 +1,16 @@ use super::*; use pretty_assertions::assert_eq; -fn turn_complete_event(turn_id: &str, last_agent_message: Option<&str>) -> TurnCompleteEvent { - serde_json::from_value(serde_json::json!({ - "turn_id": turn_id, - "last_agent_message": last_agent_message, - })) - .expect("turn complete event should deserialize") +fn complete_turn_with_message(chat: &mut ChatWidget, turn_id: &str, message: Option<&str>) { + if let Some(message) = message { + complete_assistant_message( + chat, + &format!("{turn_id}-message"), + message, + Some(MessagePhase::FinalAnswer), + ); + } + handle_turn_completed(chat, turn_id, /*duration_ms*/ None); } fn submit_composer_text(chat: &mut ChatWidget, text: &str) { @@ -75,15 +79,7 @@ async fn slash_compact_eagerly_queues_follow_up_before_turn_start() { async fn queued_slash_compact_dispatches_after_active_turn() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/compact"); @@ -94,10 +90,7 @@ async fn queued_slash_compact_dispatches_after_active_turn() { ); assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>(); assert!( @@ -112,22 +105,11 @@ async fn queued_slash_compact_dispatches_after_active_turn() { async fn queued_slash_review_with_args_dispatches_after_active_turn() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/review check regressions"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); match op_rx.try_recv() { Ok(Op::AddToHistory { .. }) => match op_rx.try_recv() { @@ -153,15 +135,7 @@ async fn queued_slash_review_with_args_dispatches_after_active_turn() { async fn queued_slash_review_with_args_restores_for_edit() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/review check regressions"); chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::ALT)); @@ -176,15 +150,7 @@ async fn queued_slash_review_with_args_restores_for_edit() { async fn queued_bang_shell_dispatches_after_active_turn() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "!echo hi"); @@ -195,10 +161,7 @@ async fn queued_bang_shell_dispatches_after_active_turn() { ); assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); match op_rx.try_recv() { Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"), @@ -215,25 +178,14 @@ async fn queued_bang_shell_dispatches_after_active_turn() { async fn queued_empty_bang_shell_reports_help_when_dequeued_and_drains_next_input() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "!"); queue_composer_text_with_tab(&mut chat, "hello after help"); assert!(drain_insert_history(&mut rx).is_empty()); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); let cells = drain_insert_history(&mut rx); let rendered = cells @@ -263,23 +215,12 @@ async fn queued_empty_bang_shell_reports_help_when_dequeued_and_drains_next_inpu async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "!echo hi"); queue_composer_text_with_tab(&mut chat, "hello after shell"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); match op_rx.try_recv() { Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"), @@ -315,23 +256,12 @@ async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() { async fn assert_cancelled_queued_menu_drains_next_input(command: &str, expected_popup_text: &str) { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, command); queue_composer_text_with_tab(&mut chat, "hello after menu"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); assert_eq!(chat.queued_user_messages.len(), 1); let popup = render_bottom_popup(&chat, /*width*/ 80); @@ -367,23 +297,12 @@ async fn queued_slash_menu_cancel_drains_next_input() { async fn queued_slash_menu_selection_drains_next_input() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/permissions"); queue_composer_text_with_tab(&mut chat, "hello after selection"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); let popup = render_bottom_popup(&chat, /*width*/ 80); assert!( @@ -411,23 +330,12 @@ async fn queued_bare_rename_drains_next_input_after_name_update() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let thread_id = ThreadId::new(); chat.thread_id = Some(thread_id); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/rename"); queue_composer_text_with_tab(&mut chat, "hello after rename"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); assert_eq!(chat.queued_user_messages.len(), 1); assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Name thread")); @@ -445,13 +353,15 @@ async fn queued_bare_rename_drains_next_input_after_name_update() { "expected rename prompt to submit thread name; events: {events:?}" ); - chat.handle_codex_event(Event { - id: "rename".into(), - msg: EventMsg::ThreadNameUpdated(crate::chatwidget::test_events::ThreadNameUpdatedEvent { - thread_id, - thread_name: Some("Queued rename".to_string()), - }), - }); + chat.handle_server_notification( + ServerNotification::ThreadNameUpdated( + codex_app_server_protocol::ThreadNameUpdatedNotification { + thread_id: thread_id.to_string(), + thread_name: Some("Queued rename".to_string()), + }, + ), + /*replay_kind*/ None, + ); match next_submit_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( @@ -471,24 +381,13 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let thread_id = ThreadId::new(); chat.thread_id = Some(thread_id); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/rename Queued rename"); queue_composer_text_with_tab(&mut chat, "first after rename"); queue_composer_text_with_tab(&mut chat, "second after rename"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>(); assert!( @@ -528,13 +427,15 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() { vec!["second after rename"] ); - chat.handle_codex_event(Event { - id: "rename".into(), - msg: EventMsg::ThreadNameUpdated(crate::chatwidget::test_events::ThreadNameUpdatedEvent { - thread_id, - thread_name: Some("Queued rename".to_string()), - }), - }); + chat.handle_server_notification( + ServerNotification::ThreadNameUpdated( + codex_app_server_protocol::ThreadNameUpdatedNotification { + thread_id: thread_id.to_string(), + thread_name: Some("Queued rename".to_string()), + }, + ), + /*replay_kind*/ None, + ); assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); assert_eq!( @@ -542,19 +443,8 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() { vec!["second after rename"] ); - chat.handle_codex_event(Event { - id: "turn-2-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-2".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); - chat.handle_codex_event(Event { - id: "turn-2-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-2", Some("done"))), - }); + handle_turn_started(&mut chat, "turn-2"); + complete_turn_with_message(&mut chat, "turn-2", Some("done")); match next_submit_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( @@ -573,24 +463,13 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() { async fn queued_unknown_slash_reports_error_when_dequeued() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/does-not-exist"); assert!(drain_insert_history(&mut rx).is_empty()); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); let cells = drain_insert_history(&mut rx); let rendered = cells @@ -1253,16 +1132,7 @@ async fn slash_logout_requests_app_server_logout() { async fn slash_copy_state_tracks_turn_complete_final_reply() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Final reply **markdown**".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("Final reply **markdown**")); assert_eq!( chat.last_agent_markdown_text(), @@ -1275,27 +1145,18 @@ async fn slash_copy_state_tracks_plan_item_completion() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let plan_text = "## Plan\n\n1. Build it\n2. Test it".to_string(); - chat.handle_codex_event(Event { - id: "item-plan".into(), - msg: EventMsg::ItemCompleted(ItemCompletedEvent { - thread_id: ThreadId::new(), + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: String::new(), turn_id: "turn-1".to_string(), - item: TurnItem::Plan(PlanItem { + item: AppServerThreadItem::Plan { id: "plan-1".to_string(), text: plan_text.clone(), - }), + }, }), - }); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + /*replay_kind*/ None, + ); + handle_turn_completed(&mut chat, "turn-1", None); assert_eq!(chat.last_agent_markdown_text(), Some(plan_text.as_str())); assert_matches!( @@ -1432,16 +1293,7 @@ async fn slash_copy_stores_clipboard_lease_and_preserves_it_on_failure() { async fn slash_copy_state_is_preserved_during_running_task() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: Some("Previous completed reply".to_string()), - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("Previous completed reply")); chat.on_task_started(); assert_eq!( @@ -1450,50 +1302,11 @@ async fn slash_copy_state_is_preserved_during_running_task() { ); } -#[tokio::test] -async fn slash_copy_tracks_replayed_legacy_agent_message_when_turn_complete_omits_text() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.handle_codex_event_replay(Event { - id: "turn-1".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "Legacy final message".into(), - phase: None, - memory_citation: None, - }), - }); - let _ = drain_insert_history(&mut rx); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); - let _ = drain_insert_history(&mut rx); - - assert_eq!( - chat.last_agent_markdown_text(), - Some("Legacy final message") - ); -} - #[tokio::test] async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); complete_assistant_message( &mut chat, "msg-1", @@ -1501,16 +1314,7 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text( /*phase*/ None, ); let _ = drain_insert_history(&mut rx); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); let _ = drain_insert_history(&mut rx); assert_eq!( @@ -1527,18 +1331,10 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text( async fn agent_turn_complete_notification_does_not_reuse_stale_copy_source() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Previous reply"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("Previous reply")); chat.pending_notification = None; - chat.handle_codex_event(Event { - id: "turn-2".into(), - msg: EventMsg::TurnComplete(turn_complete_event( - "turn-2", /*last_agent_message*/ None, - )), - }); + handle_turn_completed(&mut chat, "turn-2", None); assert_matches!( chat.pending_notification, @@ -1570,10 +1366,7 @@ async fn active_goal_without_follow_up_suppresses_agent_turn_complete_notificati /*replay_kind*/ None, ); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Still working"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("Still working")); assert_matches!(chat.pending_notification, None); } @@ -1582,21 +1375,10 @@ async fn active_goal_without_follow_up_suppresses_agent_turn_complete_notificati async fn queued_follow_up_suppresses_agent_turn_complete_notification() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); chat.queue_user_message("Continue".into()); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Still working"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("Still working")); assert_matches!(chat.pending_notification, None); assert!(chat.queued_user_messages.is_empty()); @@ -1607,21 +1389,10 @@ async fn queued_follow_up_suppresses_agent_turn_complete_notification() { async fn queued_menu_slash_keeps_agent_turn_complete_notification() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/model"); - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("Done")); assert_matches!( chat.pending_notification, @@ -1635,40 +1406,20 @@ async fn queued_menu_slash_keeps_agent_turn_complete_notification() { async fn slash_copy_uses_latest_surviving_response_after_rollback() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event_replay(Event { - id: "user-1".into(), - msg: EventMsg::UserMessage(UserMessageEvent { - message: "foo".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - }), - }); - chat.handle_codex_event_replay(Event { - id: "agent-1".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "foo response".to_string(), - phase: None, - memory_citation: None, - }), - }); - chat.handle_codex_event_replay(Event { - id: "user-2".into(), - msg: EventMsg::UserMessage(UserMessageEvent { - message: "bar".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - }), - }); - chat.handle_codex_event_replay(Event { - id: "agent-2".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "bar response".to_string(), - phase: None, - memory_citation: None, - }), - }); + replay_user_message_text(&mut chat, "user-1", "foo", ReplayKind::ThreadSnapshot); + replay_agent_message( + &mut chat, + "agent-1", + "foo response", + ReplayKind::ThreadSnapshot, + ); + replay_user_message_text(&mut chat, "user-2", "bar", ReplayKind::ThreadSnapshot); + replay_agent_message( + &mut chat, + "agent-2", + "bar response", + ReplayKind::ThreadSnapshot, + ); let _ = drain_insert_history(&mut rx); assert_eq!(chat.last_agent_markdown_text(), Some("bar response")); @@ -1685,23 +1436,13 @@ async fn slash_copy_uses_latest_surviving_response_after_rollback() { async fn slash_copy_reports_when_rewind_exceeds_retained_copy_history() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event_replay(Event { - id: "user-1".into(), - msg: EventMsg::UserMessage(UserMessageEvent { - message: "foo".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - }), - }); - chat.handle_codex_event_replay(Event { - id: "agent-1".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "foo response".to_string(), - phase: None, - memory_citation: None, - }), - }); + replay_user_message_text(&mut chat, "user-1", "foo", ReplayKind::ThreadSnapshot); + replay_agent_message( + &mut chat, + "agent-1", + "foo response", + ReplayKind::ThreadSnapshot, + ); let _ = drain_insert_history(&mut rx); chat.truncate_agent_copy_history_to_user_turn_count(/*user_turn_count*/ 0); @@ -1948,97 +1689,6 @@ async fn slash_rollout_handles_missing_path() { ); } -#[tokio::test] -async fn undo_success_events_render_info_messages() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.handle_codex_event(Event { - id: "turn-1".to_string(), - msg: EventMsg::UndoStarted(UndoStartedEvent { - message: Some("Undo requested for the last turn...".to_string()), - }), - }); - assert!( - chat.bottom_pane.status_indicator_visible(), - "status indicator should be visible during undo" - ); - - chat.handle_codex_event(Event { - id: "turn-1".to_string(), - msg: EventMsg::UndoCompleted(UndoCompletedEvent { - success: true, - message: None, - }), - }); - - let cells = drain_insert_history(&mut rx); - assert_eq!(cells.len(), 1, "expected final status only"); - assert!( - !chat.bottom_pane.status_indicator_visible(), - "status indicator should be hidden after successful undo" - ); - - let completed = lines_to_single_string(&cells[0]); - assert!( - completed.contains("Undo completed successfully."), - "expected default success message, got {completed:?}" - ); -} - -#[tokio::test] -async fn undo_failure_events_render_error_message() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.handle_codex_event(Event { - id: "turn-2".to_string(), - msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), - }); - assert!( - chat.bottom_pane.status_indicator_visible(), - "status indicator should be visible during undo" - ); - - chat.handle_codex_event(Event { - id: "turn-2".to_string(), - msg: EventMsg::UndoCompleted(UndoCompletedEvent { - success: false, - message: Some("Failed to restore workspace state.".to_string()), - }), - }); - - let cells = drain_insert_history(&mut rx); - assert_eq!(cells.len(), 1, "expected final status only"); - assert!( - !chat.bottom_pane.status_indicator_visible(), - "status indicator should be hidden after failed undo" - ); - - let completed = lines_to_single_string(&cells[0]); - assert!( - completed.contains("Failed to restore workspace state."), - "expected failure message, got {completed:?}" - ); -} - -#[tokio::test] -async fn undo_started_hides_interrupt_hint() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - - chat.handle_codex_event(Event { - id: "turn-hint".to_string(), - msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), - }); - - let status = chat - .bottom_pane - .status_widget() - .expect("status indicator should be active"); - assert!( - !status.interrupt_hint_visible(), - "undo should hide the interrupt hint because the operation cannot be cancelled" - ); -} - #[tokio::test] async fn fast_slash_command_updates_and_persists_local_service_tier() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; @@ -2100,23 +1750,12 @@ async fn queued_fast_slash_applies_before_next_queued_message() { chat.thread_id = Some(ThreadId::new()); set_chatgpt_auth(&mut chat); chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); queue_composer_text_with_tab(&mut chat, "/fast on"); queue_composer_text_with_tab(&mut chat, "hello after fast"); - chat.handle_codex_event(Event { - id: "turn-complete".into(), - msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))), - }); + complete_turn_with_message(&mut chat, "turn-1", Some("done")); let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>(); assert!( @@ -2193,28 +1832,18 @@ async fn user_turn_sends_standard_override_after_fast_is_turned_off() { async fn compact_queues_user_messages_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); chat.submit_user_message(UserMessage::from( "Steer submitted while /compact was running.".to_string(), )); - chat.handle_codex_event(Event { - id: "steer-rejected".into(), - msg: EventMsg::Error(ErrorEvent { - message: "cannot steer a compact turn".to_string(), - codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Compact, - }), + handle_error( + &mut chat, + "cannot steer a compact turn", + Some(CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Compact, }), - }); + ); let width: u16 = 80; let height: u16 = 18; diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 18bb08e9b8..941682845b 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -2,7 +2,7 @@ use super::*; use crate::bottom_pane::goal_status_indicator_line; use pretty_assertions::assert_eq; -/// Receiving a TokenCount event without usage clears the context indicator. +/// Receiving a token usage update without usage clears the context indicator. #[tokio::test] async fn token_count_none_resets_context_indicator() { let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; @@ -10,36 +10,25 @@ async fn token_count_none_resets_context_indicator() { let context_window = 13_000; let pre_compact_tokens = 12_700; - chat.handle_codex_event(Event { - id: "token-before".into(), - msg: EventMsg::TokenCount(TokenCountEvent { - info: Some(make_token_info(pre_compact_tokens, context_window)), - rate_limits: None, - }), - }); + handle_token_count( + &mut chat, + Some(make_token_info(pre_compact_tokens, context_window)), + ); assert_eq!(chat.bottom_pane.context_window_percent(), Some(30)); - chat.handle_codex_event(Event { - id: "token-cleared".into(), - msg: EventMsg::TokenCount(TokenCountEvent { - info: None, - rate_limits: None, - }), - }); + handle_token_count(&mut chat, /*info*/ None); assert_eq!(chat.bottom_pane.context_window_percent(), None); } #[tokio::test] -async fn core_cyber_policy_error_renders_dedicated_notice() { +async fn app_server_cyber_policy_error_renders_dedicated_notice() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "cyber-policy".into(), - msg: EventMsg::Error(ErrorEvent { - message: "server fallback message".to_string(), - codex_error_info: Some(CodexErrorInfo::CyberPolicy), - }), - }); + handle_error( + &mut chat, + "server fallback message", + Some(CodexErrorInfo::CyberPolicy), + ); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1); @@ -50,15 +39,13 @@ async fn core_cyber_policy_error_renders_dedicated_notice() { } #[tokio::test] -async fn core_model_verification_renders_warning() { +async fn app_server_model_verification_renders_warning() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "model-verification".into(), - msg: EventMsg::ModelVerification(ModelVerificationEvent { - verifications: vec![CoreModelVerification::TrustedAccessForCyber], - }), - }); + handle_model_verification( + &mut chat, + vec![AppServerModelVerification::TrustedAccessForCyber], + ); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1); @@ -89,13 +76,7 @@ async fn context_indicator_shows_used_tokens_when_window_unknown() { model_context_window: None, }; - chat.handle_codex_event(Event { - id: "token-usage".into(), - msg: EventMsg::TokenCount(TokenCountEvent { - info: Some(token_info), - rate_limits: None, - }), - }); + handle_token_count(&mut chat, Some(token_info)); assert_eq!(chat.bottom_pane.context_window_percent(), None); assert_eq!( @@ -105,20 +86,12 @@ async fn context_indicator_shows_used_tokens_when_window_unknown() { } #[tokio::test] -async fn turn_started_uses_runtime_context_window_before_first_token_count() { +async fn token_usage_update_uses_runtime_context_window() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; chat.config.model_context_window = Some(1_000_000); - chat.handle_codex_event(Event { - id: "turn-start".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: Some(950_000), - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_token_count(&mut chat, Some(make_token_info(0, 950_000))); assert_eq!( chat.status_line_value_for_item(&crate::bottom_pane::StatusLineItem::ContextWindowSize), @@ -146,7 +119,7 @@ async fn turn_started_uses_runtime_context_window_before_first_token_count() { assert!( context_line.contains("950K"), - "expected /status to use TurnStarted context window, got: {context_line}" + "expected /status to use runtime context window, got: {context_line}" ); assert!( !context_line.contains("1M"), @@ -1099,21 +1072,8 @@ async fn ui_snapshots_small_heights_task_running() { use ratatui::backend::TestBackend; let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // Activate status line - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "**Thinking**".into(), - }), - }); + handle_turn_started(&mut chat, "turn-1"); + handle_agent_reasoning_delta(&mut chat, "**Thinking**"); for h in [1u16, 2, 3] { let name = format!("chat_small_running_h{h}"); let mut terminal = Terminal::new(TestBackend::new(40, h)).expect("create terminal"); @@ -1133,22 +1093,9 @@ async fn status_widget_and_approval_modal_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // Begin a running task so the status indicator would be active. - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); // Provide a deterministic header for the status line. - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "**Analyzing**".into(), - }), - }); + handle_agent_reasoning_delta(&mut chat, "**Analyzing**"); // Now show an approval modal (e.g. exec approval). let ev = ExecApprovalRequestEvent { @@ -1170,10 +1117,7 @@ async fn status_widget_and_approval_modal_snapshot() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-approve-exec".into(), - msg: EventMsg::ExecApprovalRequest(ev), - }); + handle_exec_approval_request(&mut chat, "sub-approve-exec", ev); // Render at the widget's desired height and snapshot. let width: u16 = 100; @@ -1196,22 +1140,9 @@ async fn status_widget_and_approval_modal_snapshot() { async fn status_widget_active_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // Activate the status indicator by simulating a task start. - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); // Provide a deterministic header via a bold reasoning chunk. - chat.handle_codex_event(Event { - id: "task-1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "**Analyzing**".into(), - }), - }); + handle_agent_reasoning_delta(&mut chat, "**Analyzing**"); // Render and snapshot. let height = chat.desired_height(/*width*/ 80); let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height)) @@ -1231,14 +1162,7 @@ async fn stream_error_updates_status_indicator() { chat.bottom_pane.set_task_running(/*running*/ true); let msg = "Reconnecting... 2/5"; let details = "Idle timeout waiting for SSE"; - chat.handle_codex_event(Event { - id: "sub-1".into(), - msg: EventMsg::StreamError(StreamErrorEvent { - message: msg.to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: Some(details.to_string()), - }), - }); + handle_stream_error(&mut chat, msg, Some(details.to_string())); let cells = drain_insert_history(&mut rx); assert!( @@ -1264,14 +1188,7 @@ async fn stream_error_restores_hidden_status_indicator() { let msg = "Reconnecting... 2/5"; let details = "Idle timeout waiting for SSE"; - chat.handle_codex_event(Event { - id: "sub-1".into(), - msg: EventMsg::StreamError(StreamErrorEvent { - message: msg.to_string(), - codex_error_info: Some(CodexErrorInfo::Other), - additional_details: Some(details.to_string()), - }), - }); + handle_stream_error(&mut chat, msg, Some(details.to_string())); let status = chat .bottom_pane @@ -1284,12 +1201,7 @@ async fn stream_error_restores_hidden_status_indicator() { #[tokio::test] async fn warning_event_adds_warning_history_cell() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "sub-1".into(), - msg: EventMsg::Warning(WarningEvent { - message: "test warning message".to_string(), - }), - }); + handle_warning(&mut chat, "test warning message"); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1, "expected one warning history cell"); @@ -1398,16 +1310,7 @@ async fn status_line_branch_refreshes_after_turn_complete() { chat.status_line_branch_lookup_complete = true; chat.status_line_branch_pending = false; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); assert!(chat.status_line_branch_pending); } @@ -1419,15 +1322,7 @@ async fn status_line_branch_refreshes_after_interrupt() { chat.status_line_branch_lookup_complete = true; chat.status_line_branch_pending = false; - chat.handle_codex_event(Event { - id: "turn-1".into(), - msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent { - turn_id: Some("turn-1".to_string()), - reason: TurnAbortReason::Interrupted, - completed_at: None, - duration_ms: None, - }), - }); + handle_turn_interrupted(&mut chat, "turn-1"); assert!(chat.status_line_branch_pending); } @@ -1590,13 +1485,15 @@ async fn renamed_thread_footer_title_snapshot() { let thread_id = ThreadId::new(); chat.thread_id = Some(thread_id); - chat.handle_codex_event(Event { - id: "rename".to_string(), - msg: EventMsg::ThreadNameUpdated(crate::chatwidget::test_events::ThreadNameUpdatedEvent { - thread_id, - thread_name: Some("Roadmap cleanup".to_string()), - }), - }); + chat.handle_server_notification( + ServerNotification::ThreadNameUpdated( + codex_app_server_protocol::ThreadNameUpdatedNotification { + thread_id: thread_id.to_string(), + thread_name: Some("Roadmap cleanup".to_string()), + }, + ), + /*replay_kind*/ None, + ); let width = 80; let height = chat.desired_height(width); @@ -1779,26 +1676,24 @@ async fn session_configured_clears_goal_status_footer() { chat.budget_limited_turn_ids.insert("turn-1".to_string()); let rollout_file = NamedTempFile::new().unwrap(); - chat.handle_codex_event(Event { - id: "session-2".into(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: ThreadId::new(), - forked_from_id: None, - thread_name: None, - model: "gpt-5.4".to_string(), - model_provider_id: "test-provider".to_string(), - service_tier: None, - approval_policy: AskForApproval::Never, - approvals_reviewer: ApprovalsReviewer::User, - permission_profile: PermissionProfile::read_only(), - cwd: test_path_buf("/home/user/project").abs(), - reasoning_effort: Some(ReasoningEffortConfig::default()), - history_log_id: 0, - history_entry_count: 0, - initial_messages: None, - network_proxy: None, - rollout_path: Some(rollout_file.path().to_path_buf()), - }), + chat.handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), + forked_from_id: None, + fork_parent_title: None, + thread_name: None, + model: "gpt-5.4".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::Never, + approvals_reviewer: ApprovalsReviewer::User, + permission_profile: PermissionProfile::read_only(), + cwd: test_path_buf("/home/user/project").abs(), + instruction_source_paths: Vec::new(), + reasoning_effort: Some(ReasoningEffortConfig::default()), + history_log_id: 0, + history_entry_count: 0, + network_proxy: None, + rollout_path: Some(rollout_file.path().to_path_buf()), }); assert_eq!(chat.current_goal_status_indicator, None); @@ -2001,15 +1896,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // Begin turn - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); // First finalized assistant message complete_assistant_message(&mut chat, "msg-first", "First message", /*phase*/ None); @@ -2023,16 +1910,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { ); // End turn - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); let cells = drain_insert_history(&mut rx); let combined: String = cells @@ -2057,12 +1935,7 @@ async fn final_reasoning_then_message_without_deltas_are_rendered() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // No deltas; only final reasoning followed by final message. - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { - text: "I will first analyze the request.".into(), - }), - }); + handle_agent_reasoning_final(&mut chat); complete_assistant_message( &mut chat, "msg-result", @@ -2087,53 +1960,21 @@ async fn deltas_then_same_final_message_are_rendered_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; // Stream some reasoning deltas first. - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "I will ".into(), - }), - }); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "first analyze the ".into(), - }), - }); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "request.".into(), - }), - }); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { - text: "request.".into(), - }), - }); + handle_agent_reasoning_delta(&mut chat, "I will "); + handle_agent_reasoning_delta(&mut chat, "first analyze the "); + handle_agent_reasoning_delta(&mut chat, "request."); + handle_agent_reasoning_final(&mut chat); // Then stream answer deltas, followed by the exact same final message. - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { - delta: "Here is the ".into(), - }), - }); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { - delta: "result.".into(), - }), - }); + handle_agent_message_delta(&mut chat, "Here is the "); + handle_agent_message_delta(&mut chat, "result."); - chat.handle_codex_event(Event { - id: "s1".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { - message: "Here is the result.".into(), - phase: None, - memory_citation: None, - }), - }); + complete_assistant_message( + &mut chat, + "msg-result", + "Here is the result.", + /*phase*/ None, + ); // Snapshot the combined visible content to ensure we render as expected // when deltas are followed by the identical final message. @@ -2246,54 +2087,27 @@ async fn post_tool_use_hook_events_render_snapshot() { async fn completed_hook_with_no_entries_stays_out_of_history() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(Event { - id: "hook-1".into(), - msg: EventMsg::HookStarted(crate::tool_activity::HookStartedEvent { - turn_id: None, - run: codex_app_server_protocol::HookRunSummary { - id: "post-tool-use:0:/tmp/hooks.json".to_string(), - event_name: codex_app_server_protocol::HookEventName::PostToolUse, - handler_type: codex_app_server_protocol::HookHandlerType::Command, - execution_mode: codex_app_server_protocol::HookExecutionMode::Sync, - scope: codex_app_server_protocol::HookScope::Turn, - source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(), - source: codex_app_server_protocol::HookSource::User, - display_order: 0, - status: codex_app_server_protocol::HookRunStatus::Running, - status_message: None, - started_at: 1, - completed_at: None, - duration_ms: None, - entries: Vec::new(), - }, - }), - }); + handle_hook_started( + &mut chat, + hook_started_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + /*status_message*/ None, + ), + ); assert!(drain_insert_history(&mut rx).is_empty()); reveal_running_hooks(&mut chat); let running_snapshot = hook_live_and_history_snapshot(&chat, "running", ""); - chat.handle_codex_event(Event { - id: "hook-1".into(), - msg: EventMsg::HookCompleted(crate::tool_activity::HookCompletedEvent { - turn_id: None, - run: codex_app_server_protocol::HookRunSummary { - id: "post-tool-use:0:/tmp/hooks.json".to_string(), - event_name: codex_app_server_protocol::HookEventName::PostToolUse, - handler_type: codex_app_server_protocol::HookHandlerType::Command, - execution_mode: codex_app_server_protocol::HookExecutionMode::Sync, - scope: codex_app_server_protocol::HookScope::Turn, - source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(), - source: codex_app_server_protocol::HookSource::User, - display_order: 0, - status: codex_app_server_protocol::HookRunStatus::Completed, - status_message: None, - started_at: 1, - completed_at: Some(2), - duration_ms: Some(1), - entries: Vec::new(), - }, - }), - }); + handle_hook_completed( + &mut chat, + hook_completed_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + codex_app_server_protocol::HookRunStatus::Completed, + Vec::new(), + ), + ); assert!(drain_insert_history(&mut rx).is_empty()); let completed_lingering_snapshot = @@ -2310,20 +2124,26 @@ async fn completed_hook_with_no_entries_stays_out_of_history() { async fn quiet_hook_linger_starts_when_delayed_redraw_reveals_hook() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(hook_started_event( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - Some("checking output policy"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + Some("checking output policy"), + ), + ); assert!(drain_insert_history(&mut rx).is_empty()); reveal_running_hooks_after_delayed_redraw(&mut chat); - chat.handle_codex_event(hook_completed_event( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - codex_app_server_protocol::HookRunStatus::Completed, - Vec::new(), - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + codex_app_server_protocol::HookRunStatus::Completed, + Vec::new(), + ), + ); assert!(drain_insert_history(&mut rx).is_empty()); assert!( @@ -2338,24 +2158,30 @@ async fn quiet_hook_linger_starts_when_delayed_redraw_reveals_hook() { async fn blocked_and_failed_hooks_render_feedback_and_errors() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(hook_completed_event( - "pre-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PreToolUse, - codex_app_server_protocol::HookRunStatus::Blocked, - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Feedback, - text: "run tests before touching the fixture".to_string(), - }], - )); - chat.handle_codex_event(hook_completed_event( - "post-tool-use:1:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - codex_app_server_protocol::HookRunStatus::Failed, - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Error, - text: "hook exited with code 7".to_string(), - }], - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "pre-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PreToolUse, + codex_app_server_protocol::HookRunStatus::Blocked, + vec![codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Feedback, + text: "run tests before touching the fixture".to_string(), + }], + ), + ); + handle_hook_completed( + &mut chat, + hook_completed_run( + "post-tool-use:1:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + codex_app_server_protocol::HookRunStatus::Failed, + vec![codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Error, + text: "hook exited with code 7".to_string(), + }], + ), + ); let rendered = drain_insert_history(&mut rx) .iter() @@ -2378,23 +2204,29 @@ async fn blocked_and_failed_hooks_render_feedback_and_errors() { async fn completed_hook_with_output_flushes_immediately() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(hook_started_event( - "pre-tool-use:0:/tmp/hooks.json:tool-call-1", - codex_app_server_protocol::HookEventName::PreToolUse, - Some("checking command"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "pre-tool-use:0:/tmp/hooks.json:tool-call-1", + codex_app_server_protocol::HookEventName::PreToolUse, + Some("checking command"), + ), + ); reveal_running_hooks(&mut chat); let running_snapshot = hook_live_and_history_snapshot(&chat, "running", ""); - chat.handle_codex_event(hook_completed_event( - "pre-tool-use:0:/tmp/hooks.json:tool-call-1", - codex_app_server_protocol::HookEventName::PreToolUse, - codex_app_server_protocol::HookRunStatus::Blocked, - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Feedback, - text: "command blocked by policy".to_string(), - }], - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "pre-tool-use:0:/tmp/hooks.json:tool-call-1", + codex_app_server_protocol::HookEventName::PreToolUse, + codex_app_server_protocol::HookRunStatus::Blocked, + vec![codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Feedback, + text: "command blocked by policy".to_string(), + }], + ), + ); let history = drain_insert_history(&mut rx) .iter() .map(|lines| lines_to_single_string(lines)) @@ -2411,22 +2243,28 @@ async fn completed_hook_with_output_flushes_immediately() { async fn completed_hook_output_precedes_following_assistant_message() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(hook_started_event( - "pre-tool-use:0:/tmp/hooks.json:tool-call-1", - codex_app_server_protocol::HookEventName::PreToolUse, - Some("checking command"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "pre-tool-use:0:/tmp/hooks.json:tool-call-1", + codex_app_server_protocol::HookEventName::PreToolUse, + Some("checking command"), + ), + ); reveal_running_hooks(&mut chat); - chat.handle_codex_event(hook_completed_event( - "pre-tool-use:0:/tmp/hooks.json:tool-call-1", - codex_app_server_protocol::HookEventName::PreToolUse, - codex_app_server_protocol::HookRunStatus::Blocked, - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Feedback, - text: "command blocked by policy".to_string(), - }], - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "pre-tool-use:0:/tmp/hooks.json:tool-call-1", + codex_app_server_protocol::HookEventName::PreToolUse, + codex_app_server_protocol::HookRunStatus::Blocked, + vec![codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Feedback, + text: "command blocked by policy".to_string(), + }], + ), + ); complete_assistant_message( &mut chat, @@ -2463,26 +2301,35 @@ async fn completed_same_id_hook_output_survives_restart() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let hook_id = "stop:0:/tmp/hooks.json"; - chat.handle_codex_event(hook_started_event( - hook_id, - codex_app_server_protocol::HookEventName::Stop, - Some("checking stop condition"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + hook_id, + codex_app_server_protocol::HookEventName::Stop, + Some("checking stop condition"), + ), + ); reveal_running_hooks(&mut chat); - chat.handle_codex_event(hook_completed_event( - hook_id, - codex_app_server_protocol::HookEventName::Stop, - codex_app_server_protocol::HookRunStatus::Stopped, - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Stop, - text: "continue with more context".to_string(), - }], - )); - chat.handle_codex_event(hook_started_event( - hook_id, - codex_app_server_protocol::HookEventName::Stop, - Some("checking stop condition"), - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + hook_id, + codex_app_server_protocol::HookEventName::Stop, + codex_app_server_protocol::HookRunStatus::Stopped, + vec![codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Stop, + text: "continue with more context".to_string(), + }], + ), + ); + handle_hook_started( + &mut chat, + hook_started_run( + hook_id, + codex_app_server_protocol::HookEventName::Stop, + Some("checking stop condition"), + ), + ); reveal_running_hooks(&mut chat); let history = drain_insert_history(&mut rx) @@ -2507,11 +2354,14 @@ async fn identical_parallel_running_hooks_collapse_to_count() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; for tool_call_id in ["tool-call-1", "tool-call-2", "tool-call-3"] { - chat.handle_codex_event(hook_started_event( - &format!("pre-tool-use:0:/tmp/hooks.json:{tool_call_id}"), - codex_app_server_protocol::HookEventName::PreToolUse, - Some("checking command policy"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + &format!("pre-tool-use:0:/tmp/hooks.json:{tool_call_id}"), + codex_app_server_protocol::HookEventName::PreToolUse, + Some("checking command policy"), + ), + ); } reveal_running_hooks(&mut chat); @@ -2528,30 +2378,39 @@ async fn overlapping_hook_live_cell_tracks_parallel_quiet_hooks() { chat.set_status_header("Thinking".to_string()); chat.bottom_pane.ensure_status_indicator(); - chat.handle_codex_event(hook_started_event( - "pre-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PreToolUse, - Some("checking command policy"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "pre-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PreToolUse, + Some("checking command policy"), + ), + ); assert_eq!(chat.current_status.header, "Thinking"); reveal_running_hooks(&mut chat); let first_running_snapshot = hook_live_and_history_snapshot(&chat, "pre running", ""); - chat.handle_codex_event(hook_started_event( - "post-tool-use:1:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - Some("checking output policy"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "post-tool-use:1:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + Some("checking output policy"), + ), + ); assert_eq!(chat.current_status.header, "Thinking"); reveal_running_hooks(&mut chat); let second_running_snapshot = hook_live_and_history_snapshot(&chat, "post running", ""); - chat.handle_codex_event(hook_completed_event( - "pre-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PreToolUse, - codex_app_server_protocol::HookRunStatus::Completed, - Vec::new(), - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "pre-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PreToolUse, + codex_app_server_protocol::HookRunStatus::Completed, + Vec::new(), + ), + ); assert_eq!(chat.current_status.header, "Thinking"); let older_completed_snapshot = hook_live_and_history_snapshot(&chat, "pre completed lingering", ""); @@ -2559,12 +2418,15 @@ async fn overlapping_hook_live_cell_tracks_parallel_quiet_hooks() { let older_completed_expired_snapshot = hook_live_and_history_snapshot(&chat, "pre completed after linger", ""); - chat.handle_codex_event(hook_completed_event( - "post-tool-use:1:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - codex_app_server_protocol::HookRunStatus::Completed, - Vec::new(), - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "post-tool-use:1:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + codex_app_server_protocol::HookRunStatus::Completed, + Vec::new(), + ), + ); assert_eq!(chat.current_status.header, "Thinking"); assert!(chat.bottom_pane.status_indicator_visible()); assert!(drain_insert_history(&mut rx).is_empty()); @@ -2587,11 +2449,14 @@ async fn running_hook_does_not_displace_active_exec_cell() { let begin = begin_exec(&mut chat, "call-1", "echo done"); let exec_running = active_blob(&chat); - chat.handle_codex_event(hook_started_event( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - Some("checking output policy"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + Some("checking output policy"), + ), + ); reveal_running_hooks(&mut chat); let exec_and_hook_running = format!( "active exec:\n{}active hooks:\n{}", @@ -2606,12 +2471,15 @@ async fn running_hook_does_not_displace_active_exec_cell() { .collect::(); let hook_running_after_exec = active_hook_blob(&chat); - chat.handle_codex_event(hook_completed_event( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - codex_app_server_protocol::HookRunStatus::Completed, - Vec::new(), - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + codex_app_server_protocol::HookRunStatus::Completed, + Vec::new(), + ), + ); assert!(drain_insert_history(&mut rx).is_empty()); let quiet_hook_completed_lingering = active_hook_blob(&chat); expire_quiet_hook_linger(&mut chat); @@ -2635,11 +2503,14 @@ async fn hidden_active_hook_does_not_add_transcript_separator() { .expect("active exec transcript lines") .len(); - chat.handle_codex_event(hook_started_event( - "post-tool-use:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::PostToolUse, - Some("checking output policy"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "post-tool-use:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::PostToolUse, + Some("checking output policy"), + ), + ); let hidden_hook_transcript = chat .active_cell_transcript_lines(/*width*/ 80) .expect("active exec transcript lines"); @@ -2670,22 +2541,28 @@ async fn hidden_active_hook_does_not_add_transcript_separator() { async fn hook_completed_before_reveal_renders_completed_without_running_flash() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.handle_codex_event(hook_started_event( - "session-start:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::SessionStart, - Some("warming the shell"), - )); + handle_hook_started( + &mut chat, + hook_started_run( + "session-start:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::SessionStart, + Some("warming the shell"), + ), + ); let started_hidden_snapshot = active_hook_blob(&chat); - chat.handle_codex_event(hook_completed_event( - "session-start:0:/tmp/hooks.json", - codex_app_server_protocol::HookEventName::SessionStart, - codex_app_server_protocol::HookRunStatus::Completed, - vec![codex_app_server_protocol::HookOutputEntry { - kind: codex_app_server_protocol::HookOutputEntryKind::Context, - text: "session context".to_string(), - }], - )); + handle_hook_completed( + &mut chat, + hook_completed_run( + "session-start:0:/tmp/hooks.json", + codex_app_server_protocol::HookEventName::SessionStart, + codex_app_server_protocol::HookRunStatus::Completed, + vec![codex_app_server_protocol::HookOutputEntry { + kind: codex_app_server_protocol::HookOutputEntryKind::Context, + text: "session context".to_string(), + }], + ), + ); let history = drain_insert_history(&mut rx) .iter() @@ -2708,41 +2585,29 @@ async fn session_start_hook_events_render_snapshot() { .await; } -fn hook_started_event( +fn hook_started_run( id: &str, event_name: codex_app_server_protocol::HookEventName, status_message: Option<&str>, -) -> Event { - Event { - id: id.to_string(), - msg: EventMsg::HookStarted(crate::tool_activity::HookStartedEvent { - turn_id: None, - run: hook_run_summary( - id, - event_name, - codex_app_server_protocol::HookRunStatus::Running, - status_message, - Vec::new(), - ), - }), - } +) -> codex_app_server_protocol::HookRunSummary { + hook_run_summary( + id, + event_name, + codex_app_server_protocol::HookRunStatus::Running, + status_message, + Vec::new(), + ) } -fn hook_completed_event( +fn hook_completed_run( id: &str, event_name: codex_app_server_protocol::HookEventName, status: codex_app_server_protocol::HookRunStatus, entries: Vec, -) -> Event { - Event { - id: id.to_string(), - msg: EventMsg::HookCompleted(crate::tool_activity::HookCompletedEvent { - turn_id: None, - run: hook_run_summary( - id, event_name, status, /*status_message*/ None, entries, - ), - }), - } +) -> codex_app_server_protocol::HookRunSummary { + hook_run_summary( + id, event_name, status, /*status_message*/ None, entries, + ) } fn hook_run_summary( @@ -2809,9 +2674,9 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { }, ]; let cwd = AbsolutePathBuf::current_dir().expect("current dir"); - chat.handle_codex_event(Event { - id: "c1".into(), - msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent { + handle_exec_begin( + &mut chat, + ExecCommandBeginEvent { call_id: "c1".into(), process_id: None, turn_id: "turn-1".into(), @@ -2820,11 +2685,11 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { parsed_cmd: parsed_cmd.clone(), source: ExecCommandSource::Agent, interaction_input: None, - }), - }); - chat.handle_codex_event(Event { - id: "c1".into(), - msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { + }, + ); + handle_exec_end( + &mut chat, + ExecCommandEndEvent { call_id: "c1".into(), process_id: None, turn_id: "turn-1".into(), @@ -2839,24 +2704,11 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { exit_code: 0, duration: std::time::Duration::from_millis(16000), formatted_output: String::new(), - status: CoreExecCommandStatus::Completed, - }), - }); - chat.handle_codex_event(Event { - id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); - chat.handle_codex_event(Event { - id: "t1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "**Investigating rendering code**".into(), - }), - }); + status: AppServerCommandExecutionStatus::Completed, + }, + ); + handle_turn_started(&mut chat, "turn-1"); + handle_agent_reasoning_delta(&mut chat, "**Investigating rendering code**"); chat.bottom_pane.set_composer_text( "Summarize recent commits".to_string(), Vec::new(), @@ -2895,15 +2747,7 @@ async fn chatwidget_markdown_code_blocks_vt100_snapshot() { // Simulate a final agent message via streaming deltas instead of a single message - chat.handle_codex_event(Event { - id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); // Build a vt100 visual from the history insertions only (no UI overlay) let width: u16 = 80; let height: u16 = 50; @@ -2946,10 +2790,7 @@ printf 'fenced within fenced\n' delta.push(c2); } - chat.handle_codex_event(Event { - id: "t1".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }), - }); + handle_agent_message_delta(&mut chat, delta); // Drive commit ticks and drain emitted history lines into the vt100 buffer. loop { chat.on_commit_tick(); @@ -2969,16 +2810,7 @@ printf 'fenced within fenced\n' } // Finalize the stream without sending a final AgentMessage, to flush any tail. - chat.handle_codex_event(Event { - id: "t1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - last_agent_message: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - }), - }); + handle_turn_completed(&mut chat, "turn-1", None); for lines in drain_insert_history(&mut rx) { crate::insert_history::insert_history_lines(&mut term, lines) .expect("Failed to insert history lines in test"); @@ -2994,15 +2826,7 @@ printf 'fenced within fenced\n' async fn chatwidget_tall() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.thread_id = Some(ThreadId::new()); - chat.handle_codex_event(Event { - id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-1".to_string(), - started_at: None, - model_context_window: None, - collaboration_mode_kind: ModeKind::Default, - }), - }); + handle_turn_started(&mut chat, "turn-1"); for i in 0..30 { chat.queue_user_message(format!("Hello, world! {i}").into()); } diff --git a/codex-rs/tui/src/chatwidget/tests/terminal_title.rs b/codex-rs/tui/src/chatwidget/tests/terminal_title.rs index 724f7d26d7..e2ee3e908e 100644 --- a/codex-rs/tui/src/chatwidget/tests/terminal_title.rs +++ b/codex-rs/tui/src/chatwidget/tests/terminal_title.rs @@ -23,10 +23,7 @@ async fn terminal_title_shows_action_required_while_exec_approval_is_pending() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-action-required".into(), - msg: EventMsg::ExecApprovalRequest(request), - }); + handle_exec_approval_request(&mut chat, "sub-action-required", request); chat.pre_draw_tick(); @@ -69,10 +66,7 @@ async fn terminal_title_action_required_respects_spinner_setting() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-no-spinner".into(), - msg: EventMsg::ExecApprovalRequest(request), - }); + handle_exec_approval_request(&mut chat, "sub-no-spinner", request); chat.pre_draw_tick(); @@ -101,10 +95,7 @@ async fn terminal_title_action_required_blinks_when_animations_are_enabled() { available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-blink".into(), - msg: EventMsg::ExecApprovalRequest(request), - }); + handle_exec_approval_request(&mut chat, "sub-blink", request); chat.pre_draw_tick(); @@ -140,10 +131,7 @@ async fn terminal_title_activity_indicators_do_not_animate_when_animations_are_d available_decisions: None, parsed_cmd: vec![], }; - chat.handle_codex_event(Event { - id: "sub-no-animations".into(), - msg: EventMsg::ExecApprovalRequest(request), - }); + handle_exec_approval_request(&mut chat, "sub-no-animations", request); chat.pre_draw_tick(); diff --git a/codex-rs/tui/src/chatwidget/user_messages.rs b/codex-rs/tui/src/chatwidget/user_messages.rs index 8e5d539039..f3d4421acc 100644 --- a/codex-rs/tui/src/chatwidget/user_messages.rs +++ b/codex-rs/tui/src/chatwidget/user_messages.rs @@ -3,7 +3,7 @@ //! App-server turn items and queued TUI submissions describe user input in //! slightly different shapes. This module keeps the display-only representation //! and comparison keys together so chat rendering can avoid duplicate user rows -//! without depending on core protocol event wrappers. +//! while preserving local-only attachment metadata. use std::path::PathBuf; diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 471c0b4bdf..94aec5d7f1 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -148,11 +148,6 @@ impl StatusIndicatorWidget { self.show_interrupt_hint = visible; } - #[cfg(test)] - pub(crate) fn interrupt_hint_visible(&self) -> bool { - self.show_interrupt_hint - } - pub(crate) fn pause_timer(&mut self) { self.pause_timer_at(Instant::now()); } diff --git a/codex-rs/tui/src/token_usage.rs b/codex-rs/tui/src/token_usage.rs index 612a09498d..e98eb6852c 100644 --- a/codex-rs/tui/src/token_usage.rs +++ b/codex-rs/tui/src/token_usage.rs @@ -57,13 +57,6 @@ pub(crate) struct TokenUsageInfo { pub(crate) model_context_window: Option, } -#[cfg(test)] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct TokenCountEvent { - pub(crate) info: Option, - pub(crate) rate_limits: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FinalOutput { pub token_usage: TokenUsage, diff --git a/codex-rs/tui/src/turn_state.rs b/codex-rs/tui/src/turn_state.rs index 699bf7a4ac..f3a5f5b99a 100644 --- a/codex-rs/tui/src/turn_state.rs +++ b/codex-rs/tui/src/turn_state.rs @@ -7,7 +7,5 @@ use serde::Serialize; #[serde(rename_all = "snake_case")] pub(crate) enum TurnAbortReason { Interrupted, - Replaced, - ReviewEnded, BudgetLimited, }