Unify realtime stop handling in TUI (#15529)

## Summary
- route /realtime, Ctrl+C, and deleted realtime meters through the same
realtime stop path
- keep generic transcription placeholder cleanup free of realtime
shutdown side effects

## Testing
- Ran 
- Relied on CI for verification; did not run local tests

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-03-23 13:47:33 -07:00
committed by GitHub
Unverified
parent 9a33e5c0a0
commit 7b92a90612
8 changed files with 52 additions and 24 deletions
+5 -1
View File
@@ -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();
}
}
+2 -9
View File
@@ -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();
+17
View File
@@ -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<TextElement>,
+2 -2
View File
@@ -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);
+5 -1
View File
@@ -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();
}
}
+2 -9
View File
@@ -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();
@@ -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;
@@ -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);