Fix TUI stream cleanup after turn errors (#23128)

## Summary

Fixes #22726.

After a Responses stream disconnect, the live TUI could keep accepting
prompts while leaving partially streamed assistant output in its
transient streaming-cell form. That made fenced diffs or SVG/XML-like
content appear as raw transcript text until the user closed the TUI and
resumed the same session, which rebuilt the transcript from saved
history.

This change finalizes the active answer stream before generic
failed-turn cleanup clears the stream controller, so the live transcript
takes the same source-backed markdown consolidation path as a successful
turn.

## Reviewer repro

1. Start a local Codex TUI session.
2. Trigger an assistant turn that streams markdown content, especially a
fenced diff or SVG/XML-like block.
3. Force or encounter a non-retry stream disconnect before the turn
completes.
4. Continue using the same still-open TUI session.
5. Before this fix, the live history can stay raw/plain even though
`codex resume` renders the same session normally.
6. After this fix, the failed-turn path consolidates the partial stream
before rendering the error, so the live TUI keeps normal transcript
rendering.
This commit is contained in:
Eric Traut
2026-05-18 09:00:57 -07:00
committed by GitHub
parent af6ffb6ebb
commit deb159d9ff
2 changed files with 35 additions and 0 deletions

View File

@@ -636,6 +636,40 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
assert!(!chat.bottom_pane.is_task_running());
}
#[tokio::test]
async fn live_app_server_failed_turn_consolidates_streamed_answer() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
handle_turn_started(&mut chat, "turn-1");
while rx.try_recv().is_ok() {}
handle_agent_message_delta(&mut chat, "```diff\n+ streamed patch\n```\n");
chat.run_commit_tick();
while rx.try_recv().is_ok() {}
handle_error(
&mut chat,
"stream disconnected before completion",
/*codex_error_info*/ None,
);
let mut saw_consolidate = false;
while let Ok(event) = rx.try_recv() {
if let AppEvent::ConsolidateAgentMessage { source, .. } = event {
saw_consolidate = true;
assert!(
source.contains("streamed patch"),
"expected partial stream source to be consolidated, got {source:?}"
);
}
}
assert!(
saw_consolidate,
"failed turn should consolidate streamed cells before clearing the stream controller"
);
}
#[tokio::test]
async fn live_app_server_stream_recovery_restores_previous_status_header() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

View File

@@ -339,6 +339,7 @@ impl ChatWidget {
pub(super) fn on_error(&mut self, message: String) {
self.input_queue.submit_pending_steers_after_interrupt = false;
self.flush_answer_stream_with_separator();
self.finalize_turn();
self.add_to_history(history_cell::new_error_event(message));
self.set_ambient_pet_notification(