Surface parent thread status in side conversations (#18591)

## Summary

Side conversations can hide important state changes from the parent
conversation while the user is focused on the side thread. In
particular, the parent may finish, fail, need user input, or require an
approval while the side conversation remains visible. Users need a
lightweight signal for those states, but parent approval overlays should
not interrupt the side conversation itself.

This change adds parent-conversation status to the side conversation
context label and defers parent interactive overlays while side mode is
active. When the user exits side mode, pending parent approvals and
input requests are restored in the main thread. The pending approval
footer avoids duplicating the same parent approval status, and replayed
notice cells are filtered when restoring a pending interactive request
so tips or warnings do not crowd out the approval prompt.

The change is contained to the TUI side-conversation and thread replay
paths.

Example 1: Approval pending
<img width="752" height="35" alt="Screenshot 2026-04-19 at 12 56 07 PM"
src="https://github.com/user-attachments/assets/1cc0f1a3-9cab-4d60-aed2-96523ccafc20"
/>

Example 2: Turn complete
<img width="754" height="35" alt="Screenshot 2026-04-19 at 12 56 27 PM"
src="https://github.com/user-attachments/assets/653521a5-e298-4366-ae1c-72b56eb88eeb"
/>
This commit is contained in:
Eric Traut
2026-04-20 09:00:44 -07:00
committed by GitHub
Unverified
parent 43a69c50eb
commit 0dc503ba6e
7 changed files with 701 additions and 39 deletions
@@ -0,0 +1,9 @@
---
source: tui/src/chatwidget/tests/side.rs
expression: terminal.backend()
---
" "
" "
" Check recently modified functions for compatibility "
" "
" gpt-5.3-codex defa… Side from main thread · main needs input · Esc to return "
+18
View File
@@ -290,3 +290,21 @@ async fn side_context_label_preserves_status_line_snapshot() {
terminal.backend()
);
}
#[tokio::test]
async fn side_context_label_shows_parent_status_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.show_welcome_banner = false;
chat.set_side_conversation_active(/*active*/ true);
chat.set_side_conversation_context_label(Some(
"Side from main thread · main needs input · Esc to return".to_string(),
));
let width = 80;
let height = chat.desired_height(width);
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw side conversation footer");
assert_chatwidget_snapshot!("side_context_label_shows_parent_status", terminal.backend());
}