Fix TUI crash when resuming the current thread (#17086)

Problem: Resuming the live TUI thread through `/resume` could
unsubscribe and reconnect the same app-server thread, leaving the UI
crashed or disconnected.

Solution: No-op `/resume` only when the selected thread is the currently
attached active thread; keep the normal resume path for
stale/displayed-only threads so recovery and reattach still work.
This commit is contained in:
Eric Traut
2026-04-08 09:35:54 -07:00
committed by GitHub
Unverified
parent dc5feb916d
commit 5c95e4588e
+66
View File
@@ -1693,6 +1693,21 @@ impl App {
self.active_thread_id.or(self.chat_widget.thread_id())
}
fn ignore_same_thread_resume(
&mut self,
target_session: &crate::resume_picker::SessionTarget,
) -> bool {
if self.active_thread_id != Some(target_session.thread_id) {
return false;
};
self.chat_widget.add_info_message(
format!("Already viewing {}.", target_session.display_label()),
/*hint*/ None,
);
true
}
/// Mirrors the visible thread into the contextual footer row.
///
/// The footer sometimes shows ambient context instead of an instructional hint. In multi-agent
@@ -4074,6 +4089,10 @@ impl App {
.await?
{
SessionSelection::Resume(target_session) => {
if self.ignore_same_thread_resume(&target_session) {
tui.frame_requester().schedule_frame();
return Ok(AppRunControl::Continue);
}
let current_cwd = self.config.cwd.to_path_buf();
let resume_cwd = if self.remote_app_server_url.is_some() {
current_cwd.clone()
@@ -6520,6 +6539,53 @@ mod tests {
);
}
#[tokio::test]
async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
let thread_id = ThreadId::new();
let session = test_thread_session(thread_id, PathBuf::from("/tmp/project"));
app.chat_widget.handle_thread_session(session.clone());
app.thread_event_channels.insert(
thread_id,
ThreadEventChannel::new_with_session(
THREAD_EVENT_CHANNEL_CAPACITY,
session,
Vec::new(),
),
);
app.activate_thread_channel(thread_id).await;
while app_event_rx.try_recv().is_ok() {}
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
path: Some(PathBuf::from("/tmp/project")),
thread_id,
});
assert!(ignored);
let cell = match app_event_rx.try_recv() {
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
other => panic!("expected info message after same-thread resume, saw {other:?}"),
};
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(rendered.contains("Already viewing /tmp/project."));
}
#[tokio::test]
async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread() {
let mut app = make_test_app().await;
let thread_id = ThreadId::new();
let session = test_thread_session(thread_id, PathBuf::from("/tmp/project"));
app.chat_widget.handle_thread_session(session);
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
path: Some(PathBuf::from("/tmp/project")),
thread_id,
});
assert!(!ignored);
assert!(app.transcript_cells.is_empty());
}
#[tokio::test]
async fn enqueue_primary_thread_session_replays_buffered_approval_after_attach() -> Result<()> {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;