From 523d834eb8ffdd155b85a176c8fe6fbc282099bd Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:36:26 +0800 Subject: [PATCH] fix(switcher): defer host-menu disconnect so it cannot abort the app (#1162) The host menu's Disconnect ran RemoteLinks::disconnect inside an update of the window's own Tty7App. The disconnect detaches the panes of every window on that machine by reading each window's app, this one included, which panicked on the double lease. The panic happened inside the mouse-up platform callback, where it cannot unwind, so the process aborted and every window went down with it. Defer the disconnect until the update is over, and add a regression test that reproduces the original panic without the fix. --- src/ui/switcher.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/ui/switcher.rs b/src/ui/switcher.rs index 58e4cbf2..0278b7e1 100644 --- a/src/ui/switcher.rs +++ b/src/ui/switcher.rs @@ -1178,7 +1178,13 @@ impl Tty7App { } fn switcher_disconnect(&mut self, target: &RemoteTarget, cx: &mut Context) { - crate::ui::remote_workspace::RemoteLinks::disconnect(cx, target.host_id()); + // Deferred: the disconnect detaches the panes of every window on that + // machine, and reads each window's app to find them — this one among + // them, which is mid-update right here. Reading it now panics inside + // the menu's mouse-up handler, and that panic cannot unwind out of + // the platform callback, so the whole process aborts. + let host = target.host_id(); + cx.defer(move |cx| crate::ui::remote_workspace::RemoteLinks::disconnect(cx, host)); if self .connect .as_ref() @@ -3544,6 +3550,48 @@ fn tab_count(n: usize) -> String { mod tests { use super::*; + /// The host menu's Disconnect runs inside an update of this window's own + /// app, and the disconnect reads that app to find the panes to let go of. + /// Done in place, that read panicked on the double lease — inside a + /// mouse-up handler, where a panic aborts the process and takes every + /// window down with it. + #[gpui::test] + fn disconnecting_the_machine_this_window_is_on_does_not_crash(cx: &mut gpui::TestAppContext) { + use crate::core::session::{RemoteRef, WindowView, WindowViews}; + use crate::ui::windows::WindowRegistry; + + let (app, mut vcx) = crate::ui::app::test_window::harness(cx); + let target = RemoteTarget::Alias { + alias: "build-box".into(), + }; + let handle = vcx.window_handle(); + let workspace = vcx.update(|_, cx| app.read(cx).workspace); + vcx.update(|_, cx| { + WindowRegistry::init(cx); + WindowRegistry::register(cx, workspace, handle, app.downgrade()); + let mut entry = + WindowView::on_remote(RemoteRef::new(target.clone(), WorkspaceId::new())); + entry.id = workspace; + entry.open = true; + WorkspaceStore::install_for_test( + cx, + WindowViews { + views: vec![entry], + active: None, + }, + ); + }); + + app.update(&mut vcx, |this, cx| this.switcher_disconnect(&target, cx)); + vcx.background_executor.run_until_parked(); + + assert_eq!( + vcx.update(|_, cx| RemoteLinks::status_of(cx, workspace)), + Some(crate::ui::remote_workspace::RemoteStatus::Disconnected), + "the disconnect still lands once the menu's update is over" + ); + } + /// The workspace row's shape: a `flex_1` column, and in it a truncating /// name sized by its own text beside the slot number. gpui measures that /// name once against the column's zero basis, and used to answer the