From 45fdd53d1a8fbf3103f68a26f3608ded035d9809 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:48:51 +0800 Subject: [PATCH] fix(sidebar): group native SSH tabs by their remote folder (#891) A native SSH pane is owned by this machine's daemon, so its paths are kept away from every Host call and it never gets a git_status_cwd. The sidebar only grouped from git_status_cwd, which left every native SSH tab in Scratch under repo-or-directory grouping, even though the remote shell reports its cwd over OSC 7. Fall back to that reported cwd for native SSH panes (absolute POSIX paths only) and resolve it as a settled 'no repo': repo-or-directory files the tab under the folder, repo grouping keeps it in Scratch. Typed 'ssh' and WSL panes are unchanged. Fixes #891 --- src/terminal/view.rs | 54 +++++++++++++++++++++++++++++++++++++++++ src/ui/tab_sidebar.rs | 56 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/terminal/view.rs b/src/terminal/view.rs index ccc7cd50..5135c09a 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -263,6 +263,23 @@ fn cwd_is_on_host(pane_runs_remotely: bool, host_is_local: bool) -> bool { } } +/// The cwd a native SSH pane's remote shell reported, for the few readers that +/// only need a name for it and not a host to act on it. +/// +/// Such a pane belongs to this machine's daemon, so [`cwd_is_on_host`] rightly +/// turns its paths away from every `Host` call — there is no host to hand them +/// to. But the shell on the far end states them itself (OSC 7), unlike a shell +/// that ssh'd onward from a local prompt, whose directory is only ever a guess. +/// Only an absolute POSIX path counts: that is what a remote sshd's shell +/// reports, and anything else is not a directory worth naming. +fn native_ssh_cwd( + remote: Option<&RemoteContext>, + cwd: Option, +) -> Option { + remote.filter(|r| r.kind == crate::daemon::protocol::RemoteKind::NativeSsh)?; + cwd.filter(|c| c.to_string_lossy().starts_with('/')) +} + /// Which path dialect a pane's output is written in. /// /// A pane running on this machine spells paths the way this OS does, and that @@ -1950,6 +1967,12 @@ impl TerminalView { self.git_status_cwd.as_deref() } + /// See [`native_ssh_cwd`]. `None` for every pane that is not a native SSH + /// one — those either have a `git_status_cwd` or have no cwd to name. + pub fn native_ssh_cwd(&self) -> Option { + native_ssh_cwd(self.remote_context().as_ref(), self.cwd()) + } + /// Plant the cwd the git-status poll would have found. For tests that /// need a pane to look like it is sitting somewhere known — a real poll /// needs a live shell reporting a directory, which a quiet test pane has @@ -9754,6 +9777,37 @@ mod tests { assert!(!cwd_is_on_host(false, false)); } + #[test] + fn only_a_native_ssh_pane_names_its_remote_cwd() { + use super::{RemoteContext, native_ssh_cwd}; + use std::path::PathBuf; + let native = RemoteContext { + kind: RemoteKind::NativeSsh, + argv: Vec::new(), + target: "ubuntu@box".into(), + }; + let home = || Some(PathBuf::from("/home/ubuntu")); + assert_eq!(native_ssh_cwd(Some(&native), home()), home()); + + // `ssh` typed at a local prompt: the directory is a guess, not a + // report from the far end. + let typed = RemoteContext { + kind: RemoteKind::Ssh, + ..native.clone() + }; + assert_eq!(native_ssh_cwd(Some(&typed), home()), None); + assert_eq!(native_ssh_cwd(Some(&wsl_context("Ubuntu")), home()), None); + // A local pane has its own path through `git_status_cwd`. + assert_eq!(native_ssh_cwd(None, home()), None); + + assert_eq!(native_ssh_cwd(Some(&native), None), None); + assert_eq!( + native_ssh_cwd(Some(&native), Some(PathBuf::from("~"))), + None, + "only an absolute path names a directory" + ); + } + /// Which machine's spelling a pane's paths are read in. Ungated on /// purpose: the bug this settles was a Windows-only one that hid behind a /// `#[cfg(unix)]` on the test that covered it. diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index a95b80a4..98ac9174 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -1877,15 +1877,25 @@ impl Tty7App { if stated.as_ref().is_some_and(GroupKey::is_custom) { return stated; } - let cwd = tab.pane.first_leaf().and_then(|leaf| { + let resolved = tab.pane.first_leaf().and_then(|leaf| { let view = leaf.terminal()?.read(cx); - Some((view.host_id(), view.git_status_cwd()?.to_path_buf())) - }); - if let Some((id, cwd)) = cwd { - let known = cx.global::().known_repo_for(id, &cwd); - if let Some(group) = resolved_group(grouping, known, &cwd) { - *tab.sidebar_group.borrow_mut() = group; + match view.git_status_cwd() { + Some(cwd) => { + let known = cx + .global::() + .known_repo_for(view.host_id(), cwd); + resolved_group(grouping, known, cwd) + } + // A native SSH pane never gets a `git_status_cwd`: + // its paths are on a machine no `Host` reaches, so + // nothing probes them (#891). Its shell still says + // where it is, though, and that is enough for the + // folder fallback. + None => unprobed_group(grouping, &view.native_ssh_cwd()?), } + }); + if let Some(group) = resolved { + *tab.sidebar_group.borrow_mut() = group; } tab.sidebar_group.borrow().clone() }) @@ -1977,6 +1987,20 @@ fn resolved_group( }) } +/// The group for a cwd no repo probe can ever run in — a native SSH pane's. +/// "Never probed" would leave such a tab in Scratch for good, so it is read +/// as a settled "no repo": repo-or-directory grouping files it under the +/// folder, the same as a local shell in a plain directory, and repo grouping +/// leaves it in Scratch. A remote repo therefore groups by the folder the +/// shell is in, not its root — there is nothing to ask for the root. +/// +/// The key is the bare path, the same as every other derived group: a remote +/// `/home/ubuntu` and a local one share a header, as two remote workspaces' +/// identical paths already would. +fn unprobed_group(grouping: SidebarGrouping, cwd: &Path) -> Option> { + resolved_group(grouping, Some(None), cwd) +} + #[derive(Debug, PartialEq)] struct Section { key: Option, @@ -2697,6 +2721,24 @@ mod tests { ); } + #[test] + fn a_native_ssh_cwd_groups_by_its_remote_folder() { + let home = p("/home/ubuntu"); + assert_eq!( + unprobed_group(SidebarGrouping::RepoOrDirectory, &home), + Some(Some(g("/home/ubuntu"))) + ); + assert_eq!( + unprobed_group(SidebarGrouping::Repo, &home), + Some(None), + "under Repo there is no repo to find, so Scratch" + ); + // Named by the last segment, like a local folder group. + let keys = [Some(g("/home/ubuntu")), None]; + let sections = sidebar_sections(&keys); + assert_eq!(sections[0].name.as_deref(), Some("ubuntu")); + } + #[test] fn a_known_repo_home_wins_over_the_folder_in_both_modes() { for mode in [SidebarGrouping::Repo, SidebarGrouping::RepoOrDirectory] {