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>
This commit is contained in:
l0ng-ai
2026-08-08 20:07:07 +08:00
committed by GitHub
co-authored by l0ng-ai
parent 5e6bc1246f
commit 12cfc18632
3 changed files with 110 additions and 4 deletions
+75
View File
@@ -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<PathBuf>) {
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> {
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();
+13 -4
View File
@@ -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);
}
+22
View File
@@ -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<Option<PathBuf>> {
let host = self
.window_workspace(cx)
.as_ref()
.map_or(crate::ui::host_ops::HostId::LOCAL, |ws| ws.target.host_id());
cx.try_global::<GitStatusCache>()?
.known_repo_for(host, cwd?)
}
}
#[derive(Debug, PartialEq)]