fix(ui): a pure native-SSH tab is invisible to the tree, not held forever

Held means 'spawns are landing, wait before ordering' — but a remote
window's tab that is native-SSH through and through can never land: its
panes live in this client's daemon and are deliberately unnameable in
the remote machine's tree. Filing it as held made 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 existed —
and a mixed tab whose last remote pane was closed kept its dead leaf on
the machine for ever, because the held id shielded the daemon tab from
the close.

Such tabs are now classified permanently invisible: not desired, not
held. Ordering resumes, and the mixed tab's daemon twin closes when its
last tree-visible pane goes. Pending leaves (a connecting spawn, an
empty slot) still read as held.
This commit is contained in:
l0ng-ai
2026-07-30 01:44:59 +08:00
parent 917735f3c0
commit d531e876cc
2 changed files with 81 additions and 1 deletions
+48
View File
@@ -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)]
+33 -1
View File
@@ -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