Persist dev container suggestion dismissal across git worktrees (#58576)

Opening a new git worktree of a repository that has a dev container
configuration re-prompted you to reopen in a dev container, even after
you'd clicked "Don't Show Again" in another worktree of the same repo.
The dismissal was keyed on each worktree's own absolute path, so sibling
worktrees never saw the stored choice.

This keys the dismissal on the repository's common Git directory
instead, which is shared across all linked git worktrees of the same
repo, so dismissing in one worktree now suppresses the prompt in the
others. Projects that aren't Git repositories fall back to the worktree
path as before.

Closes AI-357

Release Notes:

- Fixed the "Reopen in Dev Container" suggestion re-appearing in new git
worktrees after choosing "Don't Show Again"
This commit is contained in:
Richard Feldman authored and GitHub committed 2026-06-11 20:36:42 +00:00
1 parent 674cf4ff2f
commit 24d400d04c
2 files changed
+50 -3

No files matched your search

+8 -1
View File
@@ -745,7 +745,14 @@ impl GitStore {
.values()
.filter(|repo| {
let repo_path = &repo.read(cx).work_directory_abs_path;
*repo_path == worktree_abs_path || worktree_abs_path.starts_with(repo_path.as_ref())
// The folder opened in Zed isn't necessarily the repo root; it may be
// a subdirectory of it, e.g. opening `~/code/myrepo/backend` when the
// repo lives at `~/code/myrepo`. So match any repo whose work directory
// contains the folder. Nested repos can produce multiple matches, e.g.
// opening `~/code/myrepo/vendor/lib` where `vendor/lib` is a submodule
// matches both `myrepo` and the submodule; `max_by_key` then picks the
// innermost match (the submodule), which the folder actually belongs to.
worktree_abs_path.starts_with(repo_path.as_ref())
})
.max_by_key(|repo| repo.read(cx).work_directory_abs_path.as_os_str().len())
.map(|repo| repo.read(cx).id)
@@ -1,7 +1,8 @@
use db::kvp::KeyValueStore;
use dev_container::find_configs_in_snapshot;
use gpui::{SharedString, Window};
use gpui::{App, SharedString, Window};
use project::{Project, WorktreeId};
use std::path::Path;
use std::sync::LazyLock;
use ui::Tooltip;
use ui::prelude::*;
@@ -30,6 +31,44 @@ fn project_devcontainer_key(project_path: &str) -> String {
format!("{}_{}", DEV_CONTAINER_SUGGEST_KEY, project_path)
}
/// Returns the path used to remember the user's "Don't Show Again" choice for a
/// worktree's dev container suggestion. This is keyed on the repository's common
/// Git directory rather than the worktree's own path, so that dismissing the
/// suggestion in one git worktree also suppresses it in sibling worktrees of the
/// same repository. Falls back to the worktree path when it isn't part of a Git
/// repository.
fn dismiss_path_for_worktree(
project: &gpui::Entity<Project>,
worktree_abs_path: &Path,
cx: &App,
) -> String {
let common_dir = project
.read(cx)
.repositories(cx)
.values()
.filter_map(|repo| {
let repo = repo.read(cx);
let work_dir = repo.work_directory_abs_path.clone();
// The folder opened in Zed isn't necessarily the repo root; it may be
// a subdirectory of it, e.g. opening `~/code/myrepo/backend` when the
// repo lives at `~/code/myrepo`. So match any repo whose work directory
// contains the folder. Nested repos can produce multiple matches, e.g.
// opening `~/code/myrepo/vendor/lib` where `vendor/lib` is a submodule
// matches both `myrepo` and the submodule; `max_by_key` then picks the
// innermost match (the submodule), which the folder actually belongs to.
worktree_abs_path
.starts_with(work_dir.as_ref())
.then(|| (work_dir.as_os_str().len(), repo.common_dir_abs_path.clone()))
})
.max_by_key(|(work_dir_len, _)| *work_dir_len)
.map(|(_, common_dir)| common_dir);
match common_dir {
Some(common_dir) => common_dir.to_string_lossy().to_string(),
None => worktree_abs_path.to_string_lossy().to_string(),
}
}
pub fn suggest_on_worktree_updated(
workspace: &mut Workspace,
worktree_id: WorktreeId,
@@ -93,7 +132,8 @@ pub fn suggest_on_worktree_updated(
let abs_path = worktree.abs_path();
let project_path = abs_path.to_string_lossy().to_string();
let worktree_name = worktree.root_name_str().to_string();
let key_for_dismiss = project_devcontainer_key(&project_path);
let dismiss_path = dismiss_path_for_worktree(project, abs_path.as_ref(), cx);
let key_for_dismiss = project_devcontainer_key(&dismiss_path);
let already_dismissed = KeyValueStore::global(cx)
.read_kvp(&key_for_dismiss)