diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4860787e8..0d77e3c7f 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -4174,7 +4174,11 @@ impl App { AppEvent::UpdateRecordingMeter { id, text } => { // Update in place to preserve the element id for subsequent frames. let updated = self.chat_widget.update_transcription_in_place(&id, &text); - if updated { + if updated + || self + .chat_widget + .stop_realtime_conversation_for_deleted_meter(&id) + { tui.frame_requester().schedule_frame(); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3f548fc23..b6ccf7118 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4569,7 +4569,7 @@ impl ChatWidget { return; } if self.realtime_conversation.is_live() { - self.request_realtime_conversation_close(/*info_message*/ None); + self.stop_realtime_conversation_from_ui(); } else { self.start_realtime_conversation(); } @@ -8718,7 +8718,7 @@ impl ChatWidget { self.bottom_pane.clear_quit_shortcut_hint(); self.quit_shortcut_expires_at = None; self.quit_shortcut_key = None; - self.request_realtime_conversation_close(/*info_message*/ None); + self.stop_realtime_conversation_from_ui(); return; } let modal_or_popup_active = !self.bottom_pane.no_modal_or_popup_active(); @@ -9394,13 +9394,6 @@ impl ChatWidget { } pub(crate) fn remove_transcription_placeholder(&mut self, id: &str) { - #[cfg(not(target_os = "linux"))] - if self.realtime_conversation.is_live() - && self.realtime_conversation.meter_placeholder_id.as_deref() == Some(id) - { - self.realtime_conversation.meter_placeholder_id = None; - self.request_realtime_conversation_close(/*info_message*/ None); - } self.bottom_pane.remove_transcription_placeholder(id); // Ensure the UI redraws to reflect placeholder removal. self.request_redraw(); diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index 2e4ab70e7..df51a7231 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -106,6 +106,23 @@ pub(super) struct PendingSteerCompareKey { } impl ChatWidget { + pub(super) fn stop_realtime_conversation_from_ui(&mut self) { + self.request_realtime_conversation_close(/*info_message*/ None); + } + + #[cfg(not(target_os = "linux"))] + pub(crate) fn stop_realtime_conversation_for_deleted_meter(&mut self, id: &str) -> bool { + if self.realtime_conversation.is_live() + && self.realtime_conversation.meter_placeholder_id.as_deref() == Some(id) + { + self.realtime_conversation.meter_placeholder_id = None; + self.stop_realtime_conversation_from_ui(); + return true; + } + + false + } + pub(super) fn rendered_user_message_event_from_parts( message: String, text_elements: Vec, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 8e9ccb040..806050569 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -5208,13 +5208,13 @@ async fn realtime_error_closes_without_followup_closed_info() { #[cfg(not(target_os = "linux"))] #[tokio::test] -async fn removing_active_realtime_placeholder_closes_realtime_conversation() { +async fn deleted_realtime_meter_uses_shared_stop_path() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.realtime_conversation.phase = RealtimeConversationPhase::Active; let placeholder_id = chat.bottom_pane.insert_transcription_placeholder("⠤⠤⠤⠤"); chat.realtime_conversation.meter_placeholder_id = Some(placeholder_id.clone()); - chat.remove_transcription_placeholder(&placeholder_id); + assert!(chat.stop_realtime_conversation_for_deleted_meter(&placeholder_id)); next_realtime_close_op(&mut op_rx); assert_eq!(chat.realtime_conversation.meter_placeholder_id, None); diff --git a/codex-rs/tui_app_server/src/app.rs b/codex-rs/tui_app_server/src/app.rs index 97c1e549f..6de3a2a34 100644 --- a/codex-rs/tui_app_server/src/app.rs +++ b/codex-rs/tui_app_server/src/app.rs @@ -4699,7 +4699,11 @@ impl App { AppEvent::UpdateRecordingMeter { id, text } => { // Update in place to preserve the element id for subsequent frames. let updated = self.chat_widget.update_transcription_in_place(&id, &text); - if updated { + if updated + || self + .chat_widget + .stop_realtime_conversation_for_deleted_meter(&id) + { tui.frame_requester().schedule_frame(); } } diff --git a/codex-rs/tui_app_server/src/chatwidget.rs b/codex-rs/tui_app_server/src/chatwidget.rs index 3242046bf..4796401cc 100644 --- a/codex-rs/tui_app_server/src/chatwidget.rs +++ b/codex-rs/tui_app_server/src/chatwidget.rs @@ -4725,7 +4725,7 @@ impl ChatWidget { return; } if self.realtime_conversation.is_live() { - self.request_realtime_conversation_close(/*info_message*/ None); + self.stop_realtime_conversation_from_ui(); } else { self.start_realtime_conversation(); } @@ -9952,7 +9952,7 @@ impl ChatWidget { self.bottom_pane.clear_quit_shortcut_hint(); self.quit_shortcut_expires_at = None; self.quit_shortcut_key = None; - self.request_realtime_conversation_close(/*info_message*/ None); + self.stop_realtime_conversation_from_ui(); return; } let modal_or_popup_active = !self.bottom_pane.no_modal_or_popup_active(); @@ -10565,13 +10565,6 @@ impl ChatWidget { } pub(crate) fn remove_transcription_placeholder(&mut self, id: &str) { - #[cfg(not(target_os = "linux"))] - if self.realtime_conversation.is_live() - && self.realtime_conversation.meter_placeholder_id.as_deref() == Some(id) - { - self.realtime_conversation.meter_placeholder_id = None; - self.request_realtime_conversation_close(/*info_message*/ None); - } self.bottom_pane.remove_transcription_placeholder(id); // Ensure the UI redraws to reflect placeholder removal. self.request_redraw(); diff --git a/codex-rs/tui_app_server/src/chatwidget/realtime.rs b/codex-rs/tui_app_server/src/chatwidget/realtime.rs index 0d5363daa..6860a7ca0 100644 --- a/codex-rs/tui_app_server/src/chatwidget/realtime.rs +++ b/codex-rs/tui_app_server/src/chatwidget/realtime.rs @@ -202,6 +202,23 @@ impl ChatWidget { vec![("/realtime".to_string(), "stop live voice".to_string())] } + pub(super) fn stop_realtime_conversation_from_ui(&mut self) { + self.request_realtime_conversation_close(/*info_message*/ None); + } + + #[cfg(not(target_os = "linux"))] + pub(crate) fn stop_realtime_conversation_for_deleted_meter(&mut self, id: &str) -> bool { + if self.realtime_conversation.is_live() + && self.realtime_conversation.meter_placeholder_id.as_deref() == Some(id) + { + self.realtime_conversation.meter_placeholder_id = None; + self.stop_realtime_conversation_from_ui(); + return true; + } + + false + } + pub(super) fn start_realtime_conversation(&mut self) { self.realtime_conversation.phase = RealtimeConversationPhase::Starting; self.realtime_conversation.requested_close = false; diff --git a/codex-rs/tui_app_server/src/chatwidget/tests.rs b/codex-rs/tui_app_server/src/chatwidget/tests.rs index 2da8ff77a..97ce07f37 100644 --- a/codex-rs/tui_app_server/src/chatwidget/tests.rs +++ b/codex-rs/tui_app_server/src/chatwidget/tests.rs @@ -5833,13 +5833,13 @@ async fn realtime_error_closes_without_followup_closed_info() { #[cfg(not(target_os = "linux"))] #[tokio::test] -async fn removing_active_realtime_placeholder_closes_realtime_conversation() { +async fn deleted_realtime_meter_uses_shared_stop_path() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.realtime_conversation.phase = RealtimeConversationPhase::Active; let placeholder_id = chat.bottom_pane.insert_transcription_placeholder("⠤⠤⠤⠤"); chat.realtime_conversation.meter_placeholder_id = Some(placeholder_id.clone()); - chat.remove_transcription_placeholder(&placeholder_id); + assert!(chat.stop_realtime_conversation_for_deleted_meter(&placeholder_id)); next_realtime_close_op(&mut op_rx); assert_eq!(chat.realtime_conversation.meter_placeholder_id, None);