Simplify error message and provide a route to Zed log (#48301)

KyleBarton created

Closes #46780

Creates a better flow for handling errors when a devcontainer fails, by
shortening the message and giving the user a direct route to the Zed
log. Additionally, the error from `stderr` is printed with proper line
endings, making the log more legible

<img width="1716" height="1093" alt="Screenshot 2026-02-03 at 2 54
50 PM"
src="https://github.com/user-attachments/assets/08d7847b-c9b8-49e9-9936-6ae417f82fb2"
/>
<img width="1711" height="908" alt="Screenshot 2026-02-03 at 2 55 07 PM"
src="https://github.com/user-attachments/assets/a2676419-a118-432e-8e8a-32c6e92f4f3b"
/>
<img width="2901" height="542" alt="Screenshot 2026-02-03 at 2 55 48 PM"
src="https://github.com/user-attachments/assets/ea9de533-c1c6-4cb7-bd79-e44bd035537c"
/>


Release Notes:

- Improved error messaging and handling in the event of a devcontainer
launch failure

Change summary

crates/dev_container/src/devcontainer_api.rs  |  21 -
crates/dev_container/src/lib.rs               |   3 
crates/recent_projects/src/recent_projects.rs |   2 
crates/recent_projects/src/remote_servers.rs  | 172 +++++++++++---------
4 files changed, 107 insertions(+), 91 deletions(-)

Detailed changes

crates/dev_container/src/devcontainer_api.rs 🔗

@@ -78,14 +78,14 @@ impl Display for DevContainerError {
             "{}",
             match self {
                 DevContainerError::DockerNotAvailable =>
-                    "Docker CLI not found on $PATH".to_string(),
+                    "docker CLI not found on $PATH".to_string(),
                 DevContainerError::DevContainerCliNotAvailable =>
-                    "Docker not found on path".to_string(),
-                DevContainerError::DevContainerUpFailed(message) => {
-                    format!("DevContainer creation failed with error: {}", message)
+                    "devcontainer CLI not found on path".to_string(),
+                DevContainerError::DevContainerUpFailed(_) => {
+                    "DevContainer creation failed".to_string()
                 }
-                DevContainerError::DevContainerTemplateApplyFailed(message) => {
-                    format!("DevContainer template apply failed with error: {}", message)
+                DevContainerError::DevContainerTemplateApplyFailed(_) => {
+                    "DevContainer template apply failed".to_string()
                 }
                 DevContainerError::DevContainerNotFound =>
                     "No valid dev container definition found in project".to_string(),
@@ -257,13 +257,6 @@ pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContaine
     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,
@@ -479,7 +472,7 @@ async fn devcontainer_up(
                 parse_json_from_cli(&raw)
             } else {
                 let message = format!(
-                    "Non-success status running devcontainer up for workspace: out: {:?}, err: {:?}",
+                    "Non-success status running devcontainer up for workspace: out: {}, err: {}",
                     String::from_utf8_lossy(&output.stdout),
                     String::from_utf8_lossy(&output.stderr)
                 );

crates/dev_container/src/lib.rs 🔗

@@ -47,8 +47,7 @@ use crate::devcontainer_api::DevContainerError;
 use crate::devcontainer_api::apply_dev_container_template;
 
 pub use devcontainer_api::{
-    DevContainerConfig, find_devcontainer_configs, start_dev_container,
-    start_dev_container_with_config,
+    DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config,
 };
 
 #[derive(RegisterSetting)]

crates/recent_projects/src/recent_projects.rs 🔗

@@ -238,7 +238,7 @@ pub fn init(cx: &mut App) {
                 if !is_local {
                     cx.prompt(
                         gpui::PromptLevel::Critical,
-                        "Cannot open Dev Container from remote  project",
+                        "Cannot open Dev Container from remote project",
                         None,
                         &["Ok"],
                     )

crates/recent_projects/src/remote_servers.rs 🔗

@@ -51,7 +51,7 @@ use util::{
     rel_path::RelPath,
 };
 use workspace::{
-    ModalView, OpenOptions, Toast, Workspace,
+    ModalView, OpenLog, OpenOptions, Toast, Workspace,
     notifications::{DetachAndPromptErr, NotificationId},
     open_remote_project_with_existing_connection,
 };
@@ -99,14 +99,17 @@ enum DevContainerCreationProgress {
 
 #[derive(Clone)]
 struct CreateRemoteDevContainer {
+    view_logs_entry: NavigableEntry,
     back_entry: NavigableEntry,
     progress: DevContainerCreationProgress,
 }
 
 impl CreateRemoteDevContainer {
     fn new(progress: DevContainerCreationProgress, cx: &mut Context<RemoteServerProjects>) -> Self {
+        let view_logs_entry = NavigableEntry::focusable(cx);
         let back_entry = NavigableEntry::focusable(cx);
         Self {
+            view_logs_entry,
             back_entry,
             progress,
         }
@@ -1293,6 +1296,12 @@ impl RemoteServerProjects {
                 self.mode = Mode::CreateRemoteServer(new_state);
                 cx.notify();
             }
+            Mode::CreateRemoteDevContainer(CreateRemoteDevContainer {
+                progress: DevContainerCreationProgress::Error(_),
+                ..
+            }) => {
+                cx.emit(DismissEvent);
+            }
             _ => {
                 self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
                 self.focus_handle(cx).focus(window, cx);
@@ -1839,23 +1848,24 @@ impl RemoteServerProjects {
                     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(
-                                        DevContainerCreationProgress::Error(format!("{:?}", e)),
-                                        cx,
-                                    ));
-                            })
-                            .log_err();
                         cx.prompt(
                             gpui::PromptLevel::Critical,
-                            "Failed to start Dev Container",
-                            Some(&format!("{:?}", e)),
+                            "Failed to start Dev Container. See logs for details",
+                            Some(&format!("{e}")),
                             &["Ok"],
                         )
                         .await
                         .ok();
+                        entity
+                            .update_in(cx, |remote_server_projects, window, cx| {
+                                remote_server_projects.mode =
+                                    Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
+                                        DevContainerCreationProgress::Error(format!("{e}")),
+                                        cx,
+                                    ));
+                                remote_server_projects.focus_handle(cx).focus(window, cx);
+                            })
+                            .ok();
                         return;
                     }
                 };
@@ -1899,69 +1909,83 @@ impl RemoteServerProjects {
     ) -> impl IntoElement {
         match &state.progress {
             DevContainerCreationProgress::Error(message) => {
-                self.focus_handle(cx).focus(window, cx);
-                div()
-                    .track_focus(&self.focus_handle(cx))
-                    .size_full()
-                    .child(
-                        v_flex()
-                            .py_1()
-                            .child(
-                                ListItem::new("Error")
-                                    .inset(true)
-                                    .selectable(false)
-                                    .spacing(ui::ListItemSpacing::Sparse)
-                                    .start_slot(Icon::new(IconName::XCircle).color(Color::Error))
-                                    .child(Label::new("Error Creating Dev Container:"))
-                                    .child(Label::new(message).buffer_font(cx)),
-                            )
-                            .child(ListSeparator)
-                            .child(
-                                div()
-                                    .id("devcontainer-go-back")
-                                    .track_focus(&state.back_entry.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
-                                                    .back_entry
-                                                    .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();
-                                            })),
-                                    ),
+                let view = Navigable::new(
+                    div()
+                        .child(
+                            div().track_focus(&self.focus_handle(cx)).size_full().child(
+                                v_flex().py_1().child(
+                                    ListItem::new("Error")
+                                        .inset(true)
+                                        .selectable(false)
+                                        .spacing(ui::ListItemSpacing::Sparse)
+                                        .start_slot(
+                                            Icon::new(IconName::XCircle).color(Color::Error),
+                                        )
+                                        .child(Label::new("Error Creating Dev Container:"))
+                                        .child(Label::new(message).buffer_font(cx)),
+                                ),
                             ),
-                    )
-                    .into_any_element()
+                        )
+                        .child(ListSeparator)
+                        .child(
+                            div()
+                                .id("devcontainer-see-log")
+                                .track_focus(&state.view_logs_entry.focus_handle)
+                                .on_action(cx.listener(|_, _: &menu::Confirm, window, cx| {
+                                    window.dispatch_action(Box::new(OpenLog), cx);
+                                    cx.emit(DismissEvent);
+                                    cx.notify();
+                                }))
+                                .child(
+                                    ListItem::new("li-devcontainer-see-log")
+                                        .toggle_state(
+                                            state
+                                                .view_logs_entry
+                                                .focus_handle
+                                                .contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ui::ListItemSpacing::Sparse)
+                                        .start_slot(Icon::new(IconName::File).color(Color::Muted))
+                                        .child(Label::new("Open Zed Log"))
+                                        .on_click(cx.listener(|_, _, window, cx| {
+                                            window.dispatch_action(Box::new(OpenLog), cx);
+                                            cx.emit(DismissEvent);
+                                            cx.notify();
+                                        })),
+                                ),
+                        )
+                        .child(
+                            div()
+                                .id("devcontainer-go-back")
+                                .track_focus(&state.back_entry.focus_handle)
+                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
+                                    this.cancel(&menu::Cancel, window, cx);
+                                    cx.notify();
+                                }))
+                                .child(
+                                    ListItem::new("li-devcontainer-go-back")
+                                        .toggle_state(
+                                            state
+                                                .back_entry
+                                                .focus_handle
+                                                .contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ui::ListItemSpacing::Sparse)
+                                        .start_slot(Icon::new(IconName::Exit).color(Color::Muted))
+                                        .child(Label::new("Exit"))
+                                        .on_click(cx.listener(|this, _, window, cx| {
+                                            this.cancel(&menu::Cancel, window, cx);
+                                            cx.notify();
+                                        })),
+                                ),
+                        )
+                        .into_any_element(),
+                )
+                .entry(state.view_logs_entry.clone())
+                .entry(state.back_entry.clone());
+                view.render(window, cx).into_any_element()
             }
             DevContainerCreationProgress::SelectingConfig => {
                 self.render_config_selection(window, cx).into_any_element()