Add detection of devcontainers in subfolders (#47411)

Caio Piccirillo and KyleBarton created

Release Notes:

- Add detection of devcontainers in subfolders

---------

Co-authored-by: KyleBarton <kjb@initialcapacity.io>

Change summary

crates/dev_container/src/devcontainer_api.rs  | 150 +++++
crates/dev_container/src/lib.rs               |   5 
crates/recent_projects/src/recent_projects.rs |  69 +
crates/recent_projects/src/remote_servers.rs  | 559 ++++++++++++--------
4 files changed, 542 insertions(+), 241 deletions(-)

Detailed changes

crates/dev_container/src/devcontainer_api.rs 🔗

@@ -10,10 +10,29 @@ use node_runtime::NodeRuntime;
 use serde::Deserialize;
 use settings::{DevContainerConnection, Settings as _};
 use smol::{fs, process::Command};
+use util::rel_path::RelPath;
 use workspace::Workspace;
 
 use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate};
 
+/// Represents a discovered devcontainer configuration
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DevContainerConfig {
+    /// Display name for the configuration (subfolder name or "default")
+    pub name: String,
+    /// Relative path to the devcontainer.json file from the project root
+    pub config_path: PathBuf,
+}
+
+impl DevContainerConfig {
+    pub fn default_config() -> Self {
+        Self {
+            name: "default".to_string(),
+            config_path: PathBuf::from(".devcontainer/devcontainer.json"),
+        }
+    }
+}
+
 #[derive(Debug, Deserialize)]
 #[serde(rename_all = "camelCase")]
 struct DevContainerUp {
@@ -95,6 +114,7 @@ pub(crate) async fn read_devcontainer_configuration_for_project(
         found_in_path,
         node_runtime,
         &directory,
+        None,
         use_podman(cx),
     )
     .await
@@ -131,9 +151,123 @@ fn use_podman(cx: &mut AsyncWindowContext) -> bool {
         .unwrap_or(false)
 }
 
+/// Finds all available devcontainer configurations in the project.
+///
+/// This function scans for:
+/// 1. `.devcontainer/devcontainer.json` (the default location)
+/// 2. `.devcontainer/<subfolder>/devcontainer.json` (named configurations)
+///
+/// Returns a list of found configurations, or an empty list if none are found.
+pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> {
+    let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+        log::debug!("find_devcontainer_configs: No workspace found");
+        return Vec::new();
+    };
+
+    let Ok(configs) = workspace.update(cx, |workspace, _, cx| {
+        let project = workspace.project().read(cx);
+
+        let worktree = project
+            .visible_worktrees(cx)
+            .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
+
+        let Some(worktree) = worktree else {
+            log::debug!("find_devcontainer_configs: No worktree found");
+            return Vec::new();
+        };
+
+        let worktree = worktree.read(cx);
+        let mut configs = Vec::new();
+
+        let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path");
+
+        let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else {
+            log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree");
+            return Vec::new();
+        };
+
+        if !devcontainer_entry.is_dir() {
+            log::debug!("find_devcontainer_configs: .devcontainer is not a directory");
+            return Vec::new();
+        }
+
+        log::debug!("find_devcontainer_configs: Scanning .devcontainer directory");
+        let devcontainer_json_path =
+            RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
+        for entry in worktree.child_entries(devcontainer_path) {
+            log::debug!(
+                "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}",
+                entry.path.as_unix_str(),
+                entry.is_file(),
+                entry.is_dir()
+            );
+
+            if entry.is_file() && entry.path.as_ref() == devcontainer_json_path {
+                log::debug!("find_devcontainer_configs: Found default devcontainer.json");
+                configs.push(DevContainerConfig::default_config());
+            } else if entry.is_dir() {
+                let subfolder_name = entry
+                    .path
+                    .file_name()
+                    .map(|n| n.to_string())
+                    .unwrap_or_default();
+
+                let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str());
+                if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
+                    if worktree.entry_for_path(rel_config_path).is_some() {
+                        log::debug!(
+                            "find_devcontainer_configs: Found config in subfolder: {}",
+                            subfolder_name
+                        );
+                        configs.push(DevContainerConfig {
+                            name: subfolder_name,
+                            config_path: PathBuf::from(&config_json_path),
+                        });
+                    } else {
+                        log::debug!(
+                            "find_devcontainer_configs: Subfolder {} has no devcontainer.json",
+                            subfolder_name
+                        );
+                    }
+                }
+            }
+        }
+
+        log::info!(
+            "find_devcontainer_configs: Found {} configurations",
+            configs.len()
+        );
+
+        configs.sort_by(|a, b| {
+            if a.name == "default" {
+                std::cmp::Ordering::Less
+            } else if b.name == "default" {
+                std::cmp::Ordering::Greater
+            } else {
+                a.name.cmp(&b.name)
+            }
+        });
+
+        configs
+    }) else {
+        log::debug!("find_devcontainer_configs: Failed to update workspace");
+        return Vec::new();
+    };
+
+    configs
+}
+
 pub async fn start_dev_container(
     cx: &mut AsyncWindowContext,
     node_runtime: NodeRuntime,
+) -> Result<(DevContainerConnection, String), DevContainerError> {
+    start_dev_container_with_config(cx, node_runtime, None).await
+}
+
+pub async fn start_dev_container_with_config(
+    cx: &mut AsyncWindowContext,
+    node_runtime: NodeRuntime,
+    config: Option<DevContainerConfig>,
 ) -> Result<(DevContainerConnection, String), DevContainerError> {
     let use_podman = use_podman(cx);
     check_for_docker(use_podman).await?;
@@ -144,11 +278,14 @@ pub async fn start_dev_container(
         return Err(DevContainerError::NotInValidProject);
     };
 
+    let config_path = config.map(|c| directory.join(&c.config_path));
+
     match devcontainer_up(
         &path_to_devcontainer_cli,
         found_in_path,
         &node_runtime,
         directory.clone(),
+        config_path.clone(),
         use_podman,
     )
     .await
@@ -164,6 +301,7 @@ pub async fn start_dev_container(
                 found_in_path,
                 &node_runtime,
                 &directory,
+                config_path.as_ref(),
                 use_podman,
             )
             .await
@@ -313,6 +451,7 @@ async fn devcontainer_up(
     found_in_path: bool,
     node_runtime: &NodeRuntime,
     path: Arc<Path>,
+    config_path: Option<PathBuf>,
     use_podman: bool,
 ) -> Result<DevContainerUp, DevContainerError> {
     let Ok(node_runtime_path) = node_runtime.binary_path().await else {
@@ -326,6 +465,11 @@ async fn devcontainer_up(
     command.arg("--workspace-folder");
     command.arg(path.display().to_string());
 
+    if let Some(config) = config_path {
+        command.arg("--config");
+        command.arg(config.display().to_string());
+    }
+
     log::info!("Running full devcontainer up command: {:?}", command);
 
     match command.output().await {
@@ -357,6 +501,7 @@ async fn devcontainer_read_configuration(
     found_in_path: bool,
     node_runtime: &NodeRuntime,
     path: &Arc<Path>,
+    config_path: Option<&PathBuf>,
     use_podman: bool,
 ) -> Result<DevContainerConfigurationOutput, DevContainerError> {
     let Ok(node_runtime_path) = node_runtime.binary_path().await else {
@@ -370,6 +515,11 @@ async fn devcontainer_read_configuration(
     command.arg("--workspace-folder");
     command.arg(path.display().to_string());
 
+    if let Some(config) = config_path {
+        command.arg("--config");
+        command.arg(config.display().to_string());
+    }
+
     match command.output().await {
         Ok(output) => {
             if output.status.success() {

crates/dev_container/src/lib.rs 🔗

@@ -46,7 +46,10 @@ use devcontainer_api::read_devcontainer_configuration_for_project;
 use crate::devcontainer_api::DevContainerError;
 use crate::devcontainer_api::apply_dev_container_template;
 
-pub use devcontainer_api::start_dev_container;
+pub use devcontainer_api::{
+    DevContainerConfig, find_devcontainer_configs, start_dev_container,
+    start_dev_container_with_config,
+};
 
 #[derive(RegisterSetting)]
 struct DevContainerSettings {

crates/recent_projects/src/recent_projects.rs 🔗

@@ -9,7 +9,7 @@ use std::path::PathBuf;
 #[cfg(target_os = "windows")]
 mod wsl_picker;
 
-use dev_container::start_dev_container;
+use dev_container::{find_devcontainer_configs, start_dev_container_with_config};
 use remote::RemoteConnectionOptions;
 pub use remote_connection::{RemoteConnectionModal, connect};
 pub use remote_connections::open_remote_project;
@@ -239,7 +239,7 @@ pub fn init(cx: &mut App) {
             let replace_window = window.window_handle().downcast::<Workspace>();
             let is_local = workspace.project().read(cx).is_local();
 
-            cx.spawn_in(window, async move |_, mut cx| {
+            cx.spawn_in(window, async move |_, cx| {
                 if !is_local {
                     cx.prompt(
                         gpui::PromptLevel::Critical,
@@ -251,22 +251,47 @@ pub fn init(cx: &mut App) {
                     .ok();
                     return;
                 }
-                let (connection, starting_dir) =
-                    match start_dev_container(&mut cx, app_state.node_runtime.clone()).await {
-                        Ok((c, s)) => (Connection::DevContainer(c), s),
-                        Err(e) => {
-                            log::error!("Failed to start Dev Container: {:?}", e);
-                            cx.prompt(
-                                gpui::PromptLevel::Critical,
-                                "Failed to start Dev Container",
-                                Some(&format!("{:?}", e)),
-                                &["Ok"],
-                            )
-                            .await
-                            .ok();
-                            return;
-                        }
-                    };
+
+                let configs = find_devcontainer_configs(cx);
+
+                if configs.len() > 1 {
+                    // Multiple configs found - show modal for selection
+                    cx.update(|_, cx| {
+                        with_active_or_new_workspace(cx, move |workspace, window, cx| {
+                            let fs = workspace.project().read(cx).fs().clone();
+                            let handle = cx.entity().downgrade();
+                            workspace.toggle_modal(window, cx, |window, cx| {
+                                RemoteServerProjects::new_dev_container(fs, window, handle, cx)
+                            });
+                        });
+                    })
+                    .log_err();
+                    return;
+                }
+
+                // Single or no config - proceed with opening directly
+                let config = configs.into_iter().next();
+                let (connection, starting_dir) = match start_dev_container_with_config(
+                    cx,
+                    app_state.node_runtime.clone(),
+                    config,
+                )
+                .await
+                {
+                    Ok((c, s)) => (Connection::DevContainer(c), s),
+                    Err(e) => {
+                        log::error!("Failed to start Dev Container: {:?}", e);
+                        cx.prompt(
+                            gpui::PromptLevel::Critical,
+                            "Failed to start Dev Container",
+                            Some(&format!("{:?}", e)),
+                            &["Ok"],
+                        )
+                        .await
+                        .ok();
+                        return;
+                    }
+                };
 
                 let result = open_remote_project(
                     connection.into(),
@@ -276,7 +301,7 @@ pub fn init(cx: &mut App) {
                         replace_window,
                         ..OpenOptions::default()
                     },
-                    &mut cx,
+                    cx,
                 )
                 .await;
 
@@ -293,12 +318,6 @@ pub fn init(cx: &mut App) {
                 }
             })
             .detach();
-
-            let fs = workspace.project().read(cx).fs().clone();
-            let handle = cx.entity().downgrade();
-            workspace.toggle_modal(window, cx, |window, cx| {
-                RemoteServerProjects::new_dev_container(fs, window, handle, cx)
-            });
         });
     });
 

crates/recent_projects/src/remote_servers.rs 🔗

@@ -5,20 +5,22 @@ use crate::{
     },
     ssh_config::parse_ssh_config_hosts,
 };
-use dev_container::start_dev_container;
+use dev_container::{
+    DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config,
+};
 use editor::Editor;
 
 use futures::{FutureExt, channel::oneshot, future::Shared};
 use gpui::{
-    AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter,
-    FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window,
-    canvas,
+    Action, AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity,
+    EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task,
+    WeakEntity, Window, canvas,
 };
 use language::Point;
 use log::{debug, info};
 use open_path_prompt::OpenPathDelegate;
 use paths::{global_ssh_config_file, user_ssh_config_file};
-use picker::Picker;
+use picker::{Picker, PickerDelegate};
 use project::{Fs, Project};
 use remote::{
     RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
@@ -62,6 +64,7 @@ pub struct RemoteServerProjects {
     ssh_config_updates: Task<()>,
     ssh_config_servers: BTreeSet<SharedString>,
     create_new_window: bool,
+    dev_container_picker: Option<Entity<Picker<DevContainerPickerDelegate>>>,
     _subscription: Subscription,
 }
 
@@ -89,35 +92,25 @@ impl CreateRemoteServer {
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
 enum DevContainerCreationProgress {
-    Initial,
+    SelectingConfig,
     Creating,
     Error(String),
 }
 
 #[derive(Clone)]
 struct CreateRemoteDevContainer {
-    // 3 Navigable Options
-    // - Create from devcontainer.json
-    // - Edit devcontainer.json
-    // - Go back
-    entries: [NavigableEntry; 3],
+    back_entry: NavigableEntry,
     progress: DevContainerCreationProgress,
 }
 
 impl CreateRemoteDevContainer {
-    fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self {
-        let entries = std::array::from_fn(|_| NavigableEntry::focusable(cx));
-        entries[0].focus_handle.focus(window, cx);
+    fn new(progress: DevContainerCreationProgress, cx: &mut Context<RemoteServerProjects>) -> Self {
+        let back_entry = NavigableEntry::focusable(cx);
         Self {
-            entries,
-            progress: DevContainerCreationProgress::Initial,
+            back_entry,
+            progress,
         }
     }
-
-    fn progress(&mut self, progress: DevContainerCreationProgress) -> Self {
-        self.progress = progress;
-        self.clone()
-    }
 }
 
 #[cfg(target_os = "windows")]
@@ -182,6 +175,164 @@ struct EditNicknameState {
     editor: Entity<Editor>,
 }
 
+struct DevContainerPickerDelegate {
+    selected_index: usize,
+    candidates: Vec<DevContainerConfig>,
+    matching_candidates: Vec<DevContainerConfig>,
+    parent_modal: WeakEntity<RemoteServerProjects>,
+}
+impl DevContainerPickerDelegate {
+    fn new(
+        candidates: Vec<DevContainerConfig>,
+        parent_modal: WeakEntity<RemoteServerProjects>,
+    ) -> Self {
+        Self {
+            selected_index: 0,
+            matching_candidates: candidates.clone(),
+            candidates,
+            parent_modal,
+        }
+    }
+}
+
+impl PickerDelegate for DevContainerPickerDelegate {
+    type ListItem = AnyElement;
+
+    fn match_count(&self) -> usize {
+        self.matching_candidates.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) {
+        self.selected_index = ix;
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Select Dev Container Configuration".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        let query_lower = query.to_lowercase();
+        self.matching_candidates = self
+            .candidates
+            .iter()
+            .filter(|c| {
+                c.name.to_lowercase().contains(&query_lower)
+                    || c.config_path
+                        .to_string_lossy()
+                        .to_lowercase()
+                        .contains(&query_lower)
+            })
+            .cloned()
+            .collect();
+
+        self.selected_index = std::cmp::min(
+            self.selected_index,
+            self.matching_candidates.len().saturating_sub(1),
+        );
+
+        Task::ready(())
+    }
+
+    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        let selected_config = self.matching_candidates.get(self.selected_index).cloned();
+        self.parent_modal
+            .update(cx, move |modal, cx| {
+                if secondary {
+                    modal.edit_in_dev_container_json(selected_config.clone(), window, cx);
+                } else {
+                    modal.open_dev_container(selected_config, window, cx);
+                    modal.view_in_progress_dev_container(window, cx);
+                }
+            })
+            .ok();
+    }
+
+    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        self.parent_modal
+            .update(cx, |modal, cx| {
+                modal.cancel(&menu::Cancel, window, cx);
+            })
+            .ok();
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let candidate = self.matching_candidates.get(ix)?;
+        let config_path = candidate.config_path.display().to_string();
+        Some(
+            ListItem::new(SharedString::from(format!("li-devcontainer-config-{}", ix)))
+                .inset(true)
+                .spacing(ui::ListItemSpacing::Sparse)
+                .toggle_state(selected)
+                .start_slot(Icon::new(IconName::FileToml).color(Color::Muted))
+                .child(
+                    v_flex().child(Label::new(candidate.name.clone())).child(
+                        Label::new(config_path)
+                            .size(ui::LabelSize::Small)
+                            .color(Color::Muted),
+                    ),
+                )
+                .into_any_element(),
+        )
+    }
+
+    fn render_footer(
+        &self,
+        _window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Option<AnyElement> {
+        Some(
+            h_flex()
+                .w_full()
+                .p_1p5()
+                .gap_1()
+                .justify_start()
+                .border_t_1()
+                .border_color(cx.theme().colors().border_variant)
+                .child(
+                    Button::new("run-action", "Start Dev Container")
+                        .key_binding(
+                            KeyBinding::for_action(&menu::Confirm, cx)
+                                .map(|kb| kb.size(rems_from_px(12.))),
+                        )
+                        .on_click(|_, window, cx| {
+                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
+                        }),
+                )
+                .child(
+                    Button::new("run-action-secondary", "Open devcontainer.json")
+                        .key_binding(
+                            KeyBinding::for_action(&menu::SecondaryConfirm, cx)
+                                .map(|kb| kb.size(rems_from_px(12.))),
+                        )
+                        .on_click(|_, window, cx| {
+                            window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
+                        }),
+                )
+                .into_any_element(),
+        )
+    }
+}
+
 impl EditNicknameState {
     fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self {
         let this = Self {
@@ -654,17 +805,49 @@ impl RemoteServerProjects {
         workspace: WeakEntity<Workspace>,
         cx: &mut Context<Self>,
     ) -> Self {
-        Self::new_inner(
-            Mode::CreateRemoteDevContainer(
-                CreateRemoteDevContainer::new(window, cx)
-                    .progress(DevContainerCreationProgress::Creating),
-            ),
+        let this = Self::new_inner(
+            Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
+                DevContainerCreationProgress::Creating,
+                cx,
+            )),
             false,
             fs,
             window,
             workspace,
             cx,
-        )
+        );
+
+        // Spawn a task to scan for configs and then start the container
+        cx.spawn_in(window, async move |entity, cx| {
+            let configs = find_devcontainer_configs(cx);
+
+            entity
+                .update_in(cx, |this, window, cx| {
+                    if configs.len() > 1 {
+                        // Multiple configs found - show selection UI
+                        let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
+                        this.dev_container_picker = Some(
+                            cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)),
+                        );
+
+                        let state = CreateRemoteDevContainer::new(
+                            DevContainerCreationProgress::SelectingConfig,
+                            cx,
+                        );
+                        this.mode = Mode::CreateRemoteDevContainer(state);
+                        cx.notify();
+                    } else {
+                        // Single or no config - proceed with opening
+                        let config = configs.into_iter().next();
+                        this.open_dev_container(config, window, cx);
+                        this.view_in_progress_dev_container(window, cx);
+                    }
+                })
+                .log_err();
+        })
+        .detach();
+
+        this
     }
 
     pub fn popover(
@@ -725,6 +908,7 @@ impl RemoteServerProjects {
             ssh_config_updates,
             ssh_config_servers: BTreeSet::new(),
             create_new_window,
+            dev_container_picker: None,
             _subscription,
         }
     }
@@ -946,10 +1130,10 @@ impl RemoteServerProjects {
     }
 
     fn view_in_progress_dev_container(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        self.mode = Mode::CreateRemoteDevContainer(
-            CreateRemoteDevContainer::new(window, cx)
-                .progress(DevContainerCreationProgress::Creating),
-        );
+        self.mode = Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
+            DevContainerCreationProgress::Creating,
+            cx,
+        ));
         self.focus_handle(cx).focus(window, cx);
         cx.notify();
     }
@@ -1549,13 +1733,22 @@ impl RemoteServerProjects {
         });
     }
 
-    fn edit_in_dev_container_json(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+    fn edit_in_dev_container_json(
+        &mut self,
+        config: Option<DevContainerConfig>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
         let Some(workspace) = self.workspace.upgrade() else {
             cx.emit(DismissEvent);
             cx.notify();
             return;
         };
 
+        let config_path = config
+            .map(|c| c.config_path)
+            .unwrap_or_else(|| PathBuf::from(".devcontainer/devcontainer.json"));
+
         workspace.update(cx, |workspace, cx| {
             let project = workspace.project().clone();
 
@@ -1566,7 +1759,18 @@ impl RemoteServerProjects {
 
             if let Some(worktree) = worktree {
                 let tree_id = worktree.read(cx).id();
-                let devcontainer_path = RelPath::unix(".devcontainer/devcontainer.json").unwrap();
+                let devcontainer_path =
+                    match RelPath::new(&config_path, util::paths::PathStyle::Posix) {
+                        Ok(path) => path.into_owned(),
+                        Err(error) => {
+                            log::error!(
+                                "Invalid devcontainer path: {} - {}",
+                                config_path.display(),
+                                error
+                            );
+                            return;
+                        }
+                    };
                 cx.spawn_in(window, async move |workspace, cx| {
                     workspace
                         .update_in(cx, |workspace, window, cx| {
@@ -1589,7 +1793,34 @@ impl RemoteServerProjects {
         cx.notify();
     }
 
-    fn open_dev_container(&self, window: &mut Window, cx: &mut Context<Self>) {
+    fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        cx.spawn_in(window, async move |entity, cx| {
+            let configs = find_devcontainer_configs(cx);
+
+            entity
+                .update_in(cx, |this, window, cx| {
+                    let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
+                    this.dev_container_picker =
+                        Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
+
+                    let state = CreateRemoteDevContainer::new(
+                        DevContainerCreationProgress::SelectingConfig,
+                        cx,
+                    );
+                    this.mode = Mode::CreateRemoteDevContainer(state);
+                    cx.notify();
+                })
+                .log_err();
+        })
+        .detach();
+    }
+
+    fn open_dev_container(
+        &self,
+        config: Option<DevContainerConfig>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
         let Some(app_state) = self
             .workspace
             .read_with(cx, |workspace, _| workspace.app_state().clone())
@@ -1602,19 +1833,29 @@ impl RemoteServerProjects {
 
         cx.spawn_in(window, async move |entity, cx| {
             let (connection, starting_dir) =
-                match start_dev_container(cx, app_state.node_runtime.clone()).await {
+                match start_dev_container_with_config(cx, app_state.node_runtime.clone(), config)
+                    .await
+                {
                     Ok((c, s)) => (Connection::DevContainer(c), s),
                     Err(e) => {
                         log::error!("Failed to start dev container: {:?}", e);
                         entity
-                            .update_in(cx, |remote_server_projects, window, cx| {
-                                remote_server_projects.mode = Mode::CreateRemoteDevContainer(
-                                    CreateRemoteDevContainer::new(window, cx).progress(
+                            .update_in(cx, |remote_server_projects, _window, cx| {
+                                remote_server_projects.mode =
+                                    Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
                                         DevContainerCreationProgress::Error(format!("{:?}", e)),
-                                    ),
-                                );
+                                        cx,
+                                    ));
                             })
                             .log_err();
+                        cx.prompt(
+                            gpui::PromptLevel::Critical,
+                            "Failed to start Dev Container",
+                            Some(&format!("{:?}", e)),
+                            &["Ok"],
+                        )
+                        .await
+                        .ok();
                         return;
                     }
                 };
@@ -1659,7 +1900,7 @@ impl RemoteServerProjects {
         match &state.progress {
             DevContainerCreationProgress::Error(message) => {
                 self.focus_handle(cx).focus(window, cx);
-                return div()
+                div()
                     .track_focus(&self.focus_handle(cx))
                     .size_full()
                     .child(
@@ -1678,7 +1919,7 @@ impl RemoteServerProjects {
                             .child(
                                 div()
                                     .id("devcontainer-go-back")
-                                    .track_focus(&state.entries[0].focus_handle)
+                                    .track_focus(&state.back_entry.focus_handle)
                                     .on_action(cx.listener(
                                         |this, _: &menu::Confirm, window, cx| {
                                             this.mode =
@@ -1690,7 +1931,8 @@ impl RemoteServerProjects {
                                     .child(
                                         ListItem::new("li-devcontainer-go-back")
                                             .toggle_state(
-                                                state.entries[0]
+                                                state
+                                                    .back_entry
                                                     .focus_handle
                                                     .contains_focused(window, cx),
                                             )
@@ -1709,180 +1951,73 @@ impl RemoteServerProjects {
                                                 .size(rems_from_px(12.)),
                                             )
                                             .on_click(cx.listener(|this, _, window, cx| {
-                                                let state =
-                                                    CreateRemoteDevContainer::new(window, cx);
-                                                this.mode = Mode::CreateRemoteDevContainer(state);
-
+                                                this.mode = Mode::default_mode(
+                                                    &this.ssh_config_servers,
+                                                    cx,
+                                                );
+                                                cx.focus_self(window);
                                                 cx.notify();
                                             })),
                                     ),
                             ),
                     )
-                    .into_any_element();
+                    .into_any_element()
             }
-            _ => {}
+            DevContainerCreationProgress::SelectingConfig => {
+                self.render_config_selection(window, cx).into_any_element()
+            }
+            DevContainerCreationProgress::Creating => {
+                self.focus_handle(cx).focus(window, cx);
+                div()
+                    .track_focus(&self.focus_handle(cx))
+                    .size_full()
+                    .child(
+                        v_flex()
+                            .pb_1()
+                            .child(
+                                ModalHeader::new().child(
+                                    Headline::new("Dev Containers").size(HeadlineSize::XSmall),
+                                ),
+                            )
+                            .child(ListSeparator)
+                            .child(
+                                ListItem::new("creating")
+                                    .inset(true)
+                                    .spacing(ui::ListItemSpacing::Sparse)
+                                    .disabled(true)
+                                    .start_slot(
+                                        Icon::new(IconName::ArrowCircle)
+                                            .color(Color::Muted)
+                                            .with_rotate_animation(2),
+                                    )
+                                    .child(
+                                        h_flex()
+                                            .opacity(0.6)
+                                            .gap_1()
+                                            .child(Label::new("Creating Dev Container"))
+                                            .child(LoadingLabel::new("")),
+                                    ),
+                            ),
+                    )
+                    .into_any_element()
+            }
+        }
+    }
+
+    fn render_config_selection(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let Some(picker) = &self.dev_container_picker else {
+            return div().into_any_element();
         };
 
-        let mut view = Navigable::new(
-            div()
-                .track_focus(&self.focus_handle(cx))
-                .size_full()
-                .child(
-                    v_flex()
-                        .pb_1()
-                        .child(
-                            ModalHeader::new()
-                                .child(Headline::new("Dev Containers").size(HeadlineSize::XSmall)),
-                        )
-                        .child(ListSeparator)
-                        .child(
-                            div()
-                                .id("confirm-create-from-devcontainer-json")
-                                .track_focus(&state.entries[0].focus_handle)
-                                .on_action(cx.listener({
-                                    move |this, _: &menu::Confirm, window, cx| {
-                                        this.open_dev_container(window, cx);
-                                        this.view_in_progress_dev_container(window, cx);
-                                    }
-                                }))
-                                .map(|this| {
-                                    if state.progress == DevContainerCreationProgress::Creating {
-                                        this.child(
-                                            ListItem::new("creating")
-                                                .inset(true)
-                                                .spacing(ui::ListItemSpacing::Sparse)
-                                                .disabled(true)
-                                                .start_slot(
-                                                    Icon::new(IconName::ArrowCircle)
-                                                        .color(Color::Muted)
-                                                        .with_rotate_animation(2),
-                                                )
-                                                .child(
-                                                    h_flex()
-                                                        .opacity(0.6)
-                                                        .gap_1()
-                                                        .child(Label::new("Creating From"))
-                                                        .child(
-                                                            Label::new("devcontainer.json")
-                                                                .buffer_font(cx),
-                                                        )
-                                                        .child(LoadingLabel::new("")),
-                                                ),
-                                        )
-                                    } else {
-                                        this.child(
-                                            ListItem::new(
-                                                "li-confirm-create-from-devcontainer-json",
-                                            )
-                                            .toggle_state(
-                                                state.entries[0]
-                                                    .focus_handle
-                                                    .contains_focused(window, cx),
-                                            )
-                                            .inset(true)
-                                            .spacing(ui::ListItemSpacing::Sparse)
-                                            .start_slot(
-                                                Icon::new(IconName::Plus).color(Color::Muted),
-                                            )
-                                            .child(
-                                                h_flex()
-                                                    .gap_1()
-                                                    .child(Label::new("Open or Create New From"))
-                                                    .child(
-                                                        Label::new("devcontainer.json")
-                                                            .buffer_font(cx),
-                                                    ),
-                                            )
-                                            .on_click(
-                                                cx.listener({
-                                                    move |this, _, window, cx| {
-                                                        this.open_dev_container(window, cx);
-                                                        this.view_in_progress_dev_container(
-                                                            window, cx,
-                                                        );
-                                                        cx.notify();
-                                                    }
-                                                }),
-                                            ),
-                                        )
-                                    }
-                                }),
-                        )
-                        .child(
-                            div()
-                                .id("edit-devcontainer-json")
-                                .track_focus(&state.entries[1].focus_handle)
-                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
-                                    this.edit_in_dev_container_json(window, cx);
-                                }))
-                                .child(
-                                    ListItem::new("li-edit-devcontainer-json")
-                                        .toggle_state(
-                                            state.entries[1]
-                                                .focus_handle
-                                                .contains_focused(window, cx),
-                                        )
-                                        .inset(true)
-                                        .spacing(ui::ListItemSpacing::Sparse)
-                                        .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
-                                        .child(
-                                            h_flex().gap_1().child(Label::new("Edit")).child(
-                                                Label::new("devcontainer.json").buffer_font(cx),
-                                            ),
-                                        )
-                                        .on_click(cx.listener(move |this, _, window, cx| {
-                                            this.edit_in_dev_container_json(window, cx);
-                                        })),
-                                ),
-                        )
-                        .child(ListSeparator)
-                        .child(
-                            div()
-                                .id("devcontainer-go-back")
-                                .track_focus(&state.entries[2].focus_handle)
-                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
-                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
-                                    cx.focus_self(window);
-                                    cx.notify();
-                                }))
-                                .child(
-                                    ListItem::new("li-devcontainer-go-back")
-                                        .toggle_state(
-                                            state.entries[2]
-                                                .focus_handle
-                                                .contains_focused(window, cx),
-                                        )
-                                        .inset(true)
-                                        .spacing(ui::ListItemSpacing::Sparse)
-                                        .start_slot(
-                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
-                                        )
-                                        .child(Label::new("Go Back"))
-                                        .end_slot(
-                                            KeyBinding::for_action_in(
-                                                &menu::Cancel,
-                                                &self.focus_handle,
-                                                cx,
-                                            )
-                                            .size(rems_from_px(12.)),
-                                        )
-                                        .on_click(cx.listener(|this, _, window, cx| {
-                                            this.mode =
-                                                Mode::default_mode(&this.ssh_config_servers, cx);
-                                            cx.focus_self(window);
-                                            cx.notify()
-                                        })),
-                                ),
-                        ),
-                )
-                .into_any_element(),
-        );
+        let content = v_flex().pb_1().child(picker.clone().into_any_element());
 
-        view = view.entry(state.entries[0].clone());
-        view = view.entry(state.entries[1].clone());
-        view = view.entry(state.entries[2].clone());
+        picker.focus_handle(cx).focus(window, cx);
 
-        view.render(window, cx).into_any_element()
+        content.into_any_element()
     }
 
     fn render_create_remote_server(
@@ -2469,17 +2604,11 @@ impl RemoteServerProjects {
                     .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
                     .child(Label::new("Connect Dev Container"))
                     .on_click(cx.listener(|this, _, window, cx| {
-                        let state = CreateRemoteDevContainer::new(window, cx);
-                        this.mode = Mode::CreateRemoteDevContainer(state);
-
-                        cx.notify();
+                        this.init_dev_container_mode(window, cx);
                     })),
             )
             .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
-                let state = CreateRemoteDevContainer::new(window, cx);
-                this.mode = Mode::CreateRemoteDevContainer(state);
-
-                cx.notify();
+                this.init_dev_container_mode(window, cx);
             }));
 
         #[cfg(target_os = "windows")]