diff --git a/src/ui/app.rs b/src/ui/app.rs index ce2ed9ad..565494fa 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -8632,6 +8632,54 @@ mod ssh_rebuild_gpui_tests { ); }); } + + /// A remote window's tab that is native-SSH through and through is + /// unrepresentable in the machine's tree **forever** — so it must be + /// invisible to the diff, not *held*. Held means "spawns are landing, + /// wait"; a tab that can never land would make every diff return before + /// the ordering and active-tab passes, freezing tab order and activation + /// sync for the whole window for as long as the tab exists. + #[gpui::test] + fn a_pure_native_ssh_tab_is_invisible_to_the_tree_not_held(cx: &mut TestAppContext) { + let (app, mut vcx, _remote_pane_stream) = harness_with_pane(cx); + + let remote = WindowView::on_remote(RemoteRef::new( + RemoteTarget::Alias { + alias: "build-box".into(), + }, + WorkspaceId::new(), + )); + let remote_id = remote.id; + let _ssh_stream = app.update_in(&mut vcx, |app, window, cx| { + WorkspaceStore::install_for_test( + cx, + WindowViews { + views: vec![remote], + active: None, + }, + ); + app.workspace = remote_id; + // A second tab holding only a native-SSH pane. + let (ssh_view, stream) = crate::terminal::view::quiet_test_ssh_pane(2, window, cx); + app.tabs + .push(super::Tab::new(Pane::leaf(PaneSlot::Ready(ssh_view)))); + stream + }); + + let (desired, _active, held) = app.update_in(&mut vcx, |app, _, cx| { + crate::ui::tree_sync::desired_tabs(app, cx) + }); + assert_eq!( + desired.len(), + 1, + "only the remote-backed tab can be named in the machine's tree" + ); + assert!( + held.is_empty(), + "the pure-SSH tab is permanently invisible, not held — holding it \ + would freeze ordering and active-tab sync for the whole window" + ); + } } #[cfg(test)] diff --git a/src/ui/tree_sync.rs b/src/ui/tree_sync.rs index 460ce094..aaa1cef6 100644 --- a/src/ui/tree_sync.rs +++ b/src/ui/tree_sync.rs @@ -193,6 +193,13 @@ impl DesiredNode { /// occupied, its panes just have no ids yet, and a diff that read its absence /// as "closed" would delete the daemon tab (and spend the very records) a /// revival in flight is about to replace. +/// +/// Held is strictly for the *transient* case. A remote window's tab that is +/// native-SSH through and through is unrepresentable **forever** — its panes +/// live in this client's daemon — and is neither desired nor held: as far as +/// this machine's tree is concerned, it does not exist. Holding it instead +/// would freeze the whole window's ordering and active-tab sync permanently, +/// because [`diff`] waits out held tabs before touching either. pub(crate) fn desired_tabs( app: &Tty7App, cx: &App, @@ -205,7 +212,16 @@ pub(crate) fn desired_tabs( let mut held = Vec::new(); for (index, tab) in app.tabs.iter().enumerate() { let Some(root) = desired_node(&tab.pane, remote, cx) else { - held.push(tab.tree_id.get()); + // No root means every leaf is individually unrepresentable. If + // even one of them is merely *pending* (a spawn or an empty slot + // still to fill), the tab is held; a pure native-SSH tab is + // permanently invisible instead. The distinction also lets a + // mixed tab whose last tree-visible pane was closed fall out of + // `desired` entirely, so a Full diff closes its daemon tab + // rather than leaving a dead leaf on the machine for ever. + if !(remote && every_leaf_is_native_ssh(&tab.pane, cx)) { + held.push(tab.tree_id.get()); + } continue; }; let id = tab.tree_id.get(); @@ -226,6 +242,22 @@ pub(crate) fn desired_tabs( (out, active, held) } +/// Whether every leaf of `pane` is a *ready* native-SSH view — the one kind +/// of leaf a remote window can never name in its machine's tree, because the +/// pane lives in this client's own daemon. Only meaningful for a tab whose +/// desired root came out `None`: it decides permanently-invisible versus +/// held (see [`desired_tabs`]). A connecting or empty leaf answers `false` — +/// those are pending, not foreign. +fn every_leaf_is_native_ssh(pane: &Pane, cx: &App) -> bool { + match pane { + Pane::Leaf(PaneSlot::Ready(view)) => view.read(cx).ssh_spec().is_some(), + Pane::Leaf(PaneSlot::Connecting(_)) | Pane::Empty => false, + Pane::Split { a, b, .. } => { + every_leaf_is_native_ssh(a, cx) && every_leaf_is_native_ssh(b, cx) + } + } +} + /// One GUI pane node, in tree shape. `None` for the unrepresentable: a fresh /// spawn with no pane id yet, and — in a remote window — a native-SSH leaf, /// whose pane lives in *this* client's daemon and so cannot be named in the