From 12cfc18632b1fb5507ef8a45e17d88b3e7f07e89 Mon Sep 17 00:00:00 2001 From: l0ng-ai Date: Sat, 8 Aug 2026 20:07:07 +0800 Subject: [PATCH] fix(sidebar): put a new tab in its repo group on the first frame (#411) Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> --- src/terminal/remote.rs | 75 ++++++++++++++++++++++++++++++++++++++++++ src/ui/app.rs | 17 +++++++--- src/ui/tab_sidebar.rs | 22 +++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/terminal/remote.rs b/src/terminal/remote.rs index 09cd71a9..22c1928d 100644 --- a/src/terminal/remote.rs +++ b/src/terminal/remote.rs @@ -275,6 +275,7 @@ impl RemoteTerminal { let win = win_size(size, cell_w, cell_h); let workspace = spawn_workspace(owner.as_deref(), route); + let spawned_in = cwd.clone(); let owner = owner.filter(|_| { route.is_local() @@ -305,9 +306,24 @@ impl RemoteTerminal { let mut term = Self::from_stream(stream, size)?; term.route = route.clone(); + term.seed_cwd(spawned_in); Ok((term, pane_id)) } + /// Remember the directory the daemon was asked to spawn in, so everything + /// that keys off a pane's cwd — the sidebar's repo grouping above all — + /// has an answer before the shell gets far enough to report its own via + /// OSC 7. The reader thread is already running, so a report that beat us + /// here wins: it describes where the shell actually landed, which is not + /// always where we asked (a missing directory sends the daemon home, an + /// rc file may `cd` on its own). + fn seed_cwd(&self, cwd: Option) { + let Some(cwd) = cwd else { return }; + if let Ok(mut guard) = self.cwd.lock() { + guard.get_or_insert(cwd); + } + } + pub fn attach(size: TermSize, cell_w: u16, cell_h: u16, pane_id: u64) -> anyhow::Result { Self::attach_on(&PaneRoute::Local, size, cell_w, cell_h, pane_id) } @@ -3210,6 +3226,65 @@ mod tests { assert!(poll(&|s| s.is_none()), "a None report clears the session"); } + #[test] + fn the_spawn_directory_answers_until_the_shell_reports_its_own() { + crate::core::config::pin_test_config_dir(); + let (client_side, mut daemon_side) = UnixStream::pair().unwrap(); + let term = RemoteTerminal::from_stream(client_side, TermSize::new(80, 24)).unwrap(); + let poll = |want: Option<&str>| { + let want = want.map(PathBuf::from); + for _ in 0..200 { + if term.foreground_cwd() == want { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + false + }; + + assert_eq!(term.foreground_cwd(), None, "nothing known before the seed"); + term.seed_cwd(Some(PathBuf::from("/repo/tty7"))); + assert_eq!( + term.foreground_cwd(), + Some(PathBuf::from("/repo/tty7")), + "the sidebar can group this pane without waiting for the shell" + ); + + DaemonMsg::Cwd(PathBuf::from("/repo/tty7/crates")) + .encode(&mut daemon_side) + .unwrap(); + daemon_side.flush().unwrap(); + assert!( + poll(Some("/repo/tty7/crates")), + "the shell's own report replaces the seed" + ); + } + + #[test] + fn a_shell_report_that_beat_the_seed_wins() { + crate::core::config::pin_test_config_dir(); + let (client_side, mut daemon_side) = UnixStream::pair().unwrap(); + let term = RemoteTerminal::from_stream(client_side, TermSize::new(80, 24)).unwrap(); + + DaemonMsg::Cwd(PathBuf::from("/somewhere/else")) + .encode(&mut daemon_side) + .unwrap(); + daemon_side.flush().unwrap(); + for _ in 0..200 { + if term.foreground_cwd().is_some() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + + term.seed_cwd(Some(PathBuf::from("/repo/tty7"))); + assert_eq!( + term.foreground_cwd(), + Some(PathBuf::from("/somewhere/else")), + "where the shell actually landed beats where we asked it to" + ); + } + #[test] fn marks_record_the_row_each_one_landed_on() { let (client_side, mut daemon_side) = UnixStream::pair().unwrap(); diff --git a/src/ui/app.rs b/src/ui/app.rs index 3a50e86f..b53cbed9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -2312,9 +2312,9 @@ impl Tty7App { if !self.guard_local_spawn(window, cx) { return; } - let pane_ws = self.window_workspace(cx); + let group = self.spawn_group(cwd.as_deref(), cx); let tab = match new_terminal( - pane_ws, + self.window_workspace(cx), Some(self.workspace), self.font_size, cwd, @@ -2336,7 +2336,11 @@ impl Tty7App { self.remember_active_pane(window, cx); self.maximized = None; let insert_at = self.new_tab_insert_at(cx); - self.tabs.insert(insert_at, Tab::new(Pane::leaf(tab))); + let new_tab = Tab::new(Pane::leaf(tab)); + if let Some(group) = group { + *new_tab.sidebar_group.borrow_mut() = group; + } + self.tabs.insert(insert_at, new_tab); self.active = insert_at; self.focus_active(window, cx); self.save_session(cx); @@ -2968,6 +2972,7 @@ impl Tty7App { let view = source.read(cx); (view.local_cwd(), view.shell_spec()) }; + let group = self.spawn_group(cwd.as_deref(), cx); let new = match new_terminal( self.window_workspace(cx), Some(self.workspace), @@ -3000,7 +3005,11 @@ impl Tty7App { self.remember_active_pane(window, cx); self.maximized = None; let insert_at = self.new_tab_insert_at(cx); - self.tabs.insert(insert_at, Tab::new(Pane::leaf(new))); + let tab = Tab::new(Pane::leaf(new)); + if let Some(group) = group { + *tab.sidebar_group.borrow_mut() = group; + } + self.tabs.insert(insert_at, tab); self.active = insert_at; self.focus_active(window, cx); } diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index 0a7bbe7f..c0234f14 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -746,6 +746,28 @@ impl Tty7App { self.activate(i, window, cx); } } + + /// Which group a tab about to be spawned in `cwd` belongs to, when the + /// repo probe for that directory has already landed. `Some(None)` means + /// the cache knows it is not a repo; a bare `None` means it never looked. + /// + /// A tab's group otherwise starts empty and only fills in once its shell + /// has started and reported a cwd, which parks every new tab in the + /// scratch group at the bottom of the sidebar until then. A tab spawned + /// from one already sitting in a repo inherits a warm cache, so seeding + /// it here lands the tab in its group on the first frame. + pub(crate) fn spawn_group( + &self, + cwd: Option<&Path>, + cx: &gpui::App, + ) -> Option> { + let host = self + .window_workspace(cx) + .as_ref() + .map_or(crate::ui::host_ops::HostId::LOCAL, |ws| ws.target.host_id()); + cx.try_global::()? + .known_repo_for(host, cwd?) + } } #[derive(Debug, PartialEq)]