Avoid redundant reads and screen clears when switching side chats (#60675)

## Why

A live parent thread continues receiving lifecycle notifications while its side chat has focus, so returning to it does not need another liveness read.

## What changed

- Skip the parent liveness read when returning from a side chat while the parent is still attached and online. Keep the read when reattachment is needed.
- Stop scheduling a deferred screen clear when handling a transcript reset that preserves the screen.

## Testing

Add regression coverage for parent `thread/read` requests with and without reattachment, and assert that side-chat navigation leaves no deferred screen clear.

GitOrigin-RevId: a07f161ec86b0ae51250c5a0156402d328c42bdf
This commit is contained in:
Felipe Coury
2026-11-10 22:32:59 +00:00
committed by copyberry
parent d4b19eca27
commit ad3d3521ed
4 changed files with 60 additions and 15 deletions
-11
View File
@@ -1059,17 +1059,6 @@ impl App {
AppEvent::ResetTranscriptForThreadSwitchPreservingScreen => {
self.reset_transcript_state_after_clear();
tui.clear_pending_history_lines();
let switched_tab = self.conversation_frame.chat_strip.is_some()
&& self.active_side_parent_thread_id().is_some()
&& self.conversation_frame.sidebar_input.is_some_and(|painted| {
painted.selected_chat != self.current_displayed_thread_id()
});
if self.conversation_frame.chooser_area().is_none()
&& self.chooser_focus().is_none()
&& !switched_tab
{
tui.defer_thread_switch_clear();
}
self.pending_thread_switch_resets -= 1;
}
AppEvent::CommitRealtimeTranscriptHistory => {
+10 -4
View File
@@ -638,10 +638,16 @@ impl App {
self.store_active_thread_receiver().await;
}
// A tracked side thread stays loaded until it is explicitly discarded and already has a
// replay channel, so another liveness read cannot add anything before selection.
if !(self.side_threads.contains_key(&thread_id)
&& self.thread_event_channels.contains_key(&thread_id)
let known_side = self.side_threads.contains_key(&thread_id)
&& self.thread_event_channels.contains_key(&thread_id);
// The live parent already receives lifecycle notifications while its child has focus.
let live_parent = self.side_retention_enabled(tui)
&& !self.reconnect.offline
&& self.primary_thread_id == Some(thread_id)
&& self.active_side_parent_thread_id() == Some(thread_id)
&& !self.should_attach_live_thread_for_selection(thread_id);
if !(known_side
|| live_parent
|| self
.refresh_agent_picker_thread_liveness(app_server, thread_id)
.await)
+2
View File
@@ -4554,6 +4554,7 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
keys!(toggle);
assert_eq!(app.current_displayed_thread_id(), Some(side_thread_id));
assert!(!tui.has_deferred_thread_switch_clear());
let wide = side_frame(&mut app, &mut tui, size)?;
assert!(
wide.contains(parent_row) && wide.contains(child_row),
@@ -4616,6 +4617,7 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
};
// Park an interaction on the parent so both hide shortcuts must clear it on return.
keys!(prefix, main);
assert!(!tui.has_deferred_thread_switch_clear());
app.transcript_view.begin_search();
app.select_agent_thread(&mut tui, &mut app_server, side_thread_id)
.await?;
@@ -1104,6 +1104,54 @@ async fn complete_managed_worktree_creation(
}
}
#[tokio::test]
async fn returning_from_side_chat_reads_parent_only_when_reattaching() -> Result<()> {
let (mut app, _home) = make_history_test_app().await?;
enable_side_retention(&mut app)?;
let mut tui = crate::tui::test_support::make_test_tui()?;
tui.set_owned_screen(/*owned*/ true)?;
let (mut server, requests, proxy) = start_recording_app_server(
&app.config,
/*blocked_thread_list*/ None,
/*failed_thread_name*/ None,
)
.await?;
let started = server.start_thread(&app.config).await?;
let parent = started.session.thread_id;
app.enqueue_primary_thread_session(started.session, started.turns)
.await?;
let side = server.start_thread(&app.config).await?;
let child = side.session.thread_id;
app.side_threads.insert(child, SideThreadState::new(parent));
app.ensure_thread_channel(child)
.store
.lock()
.await
.set_session(side.session, side.turns);
for needs_reattach in [false, true] {
app.select_agent_thread(&mut tui, &mut server, child)
.await?;
assert_eq!(app.current_displayed_thread_id(), Some(child));
if needs_reattach {
app.ensure_thread_channel(parent).mark_replay_only();
}
app.select_agent_thread(&mut tui, &mut server, parent)
.await?;
assert_eq!(app.current_displayed_thread_id(), Some(parent));
let reads = recorded_params(&requests, "thread/read");
assert_eq!(
reads
.iter()
.any(|params| params["threadId"] == parent.to_string()),
needs_reattach
);
}
server.shutdown().await?;
proxy.await??;
Ok(())
}
#[tokio::test]
async fn same_thread_retry_keeps_subscription_and_restores_draft() -> Result<()> {
let (mut app, codex_home) = make_history_test_app().await?;