linux: Show warning if file picker portal is missing (#14401)

apricotbucket28 created

This PR adds a warning when the file chooser couldn't be opened on Linux

It's quite confusing when trying to open a file and apparently nothing
happens:

fixes https://github.com/zed-industries/zed/issues/11089,
https://github.com/zed-industries/zed/issues/14328,
https://github.com/zed-industries/zed/issues/13753#issuecomment-2225812703,
https://github.com/zed-industries/zed/issues/13766,
https://github.com/zed-industries/zed/issues/14384,
https://github.com/zed-industries/zed/issues/14353,
https://github.com/zed-industries/zed/issues/9209


![image](https://github.com/user-attachments/assets/5acabdaa-7a9d-4225-9480-e371d20387c3)


Release Notes:

- N/A

Change summary

Cargo.lock                                   |  5 
Cargo.toml                                   |  2 
crates/extensions_ui/src/extensions_ui.rs    | 19 +++
crates/gpui/src/app.rs                       |  9 +
crates/gpui/src/platform.rs                  |  4 
crates/gpui/src/platform/linux/platform.rs   | 77 +++++++++++------
crates/gpui/src/platform/mac/platform.rs     |  8 
crates/gpui/src/platform/test/platform.rs    |  8 
crates/gpui/src/platform/windows/platform.rs | 23 ++--
crates/workspace/src/notifications.rs        | 97 ++++++++++++++++++++-
crates/workspace/src/workspace.rs            | 71 ++++++++++++---
11 files changed, 248 insertions(+), 75 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -341,8 +341,9 @@ dependencies = [
 
 [[package]]
 name = "ashpd"
-version = "0.9.0"
-source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
 dependencies = [
  "async-fs 2.1.1",
  "async-net 2.0.0",

Cargo.toml 🔗

@@ -274,7 +274,7 @@ zed_actions = { path = "crates/zed_actions" }
 alacritty_terminal = "0.23"
 any_vec = "0.13"
 anyhow = "1.0.57"
-ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
+ashpd = "0.9.1"
 async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
 async-dispatcher = { version = "0.1" }
 async-fs = "1.6"

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -12,7 +12,7 @@ use editor::{Editor, EditorElement, EditorStyle};
 use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
+    actions, uniform_list, AnyElement, AppContext, EventEmitter, Flatten, FocusableView, FontStyle,
     InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
     UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
 };
@@ -24,7 +24,6 @@ use std::time::Duration;
 use std::{ops::Range, sync::Arc};
 use theme::ThemeSettings;
 use ui::{prelude::*, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
-use util::ResultExt as _;
 use workspace::item::TabContentParams;
 use workspace::{
     item::{Item, ItemEvent},
@@ -58,9 +57,23 @@ pub fn init(cx: &mut AppContext) {
                     multiple: false,
                 });
 
+                let workspace_handle = cx.view().downgrade();
                 cx.deref_mut()
                     .spawn(|mut cx| async move {
-                        let extension_path = prompt.await.log_err()??.pop()?;
+                        let extension_path =
+                            match Flatten::flatten(prompt.await.map_err(|e| e.into())) {
+                                Ok(Some(mut paths)) => paths.pop()?,
+                                Ok(None) => return None,
+                                Err(err) => {
+                                    workspace_handle
+                                        .update(&mut cx, |workspace, cx| {
+                                            workspace.show_portal_error(err.to_string(), cx);
+                                        })
+                                        .ok();
+                                    return None;
+                                }
+                            };
+
                         store
                             .update(&mut cx, |store, cx| {
                                 store

crates/gpui/src/app.rs 🔗

@@ -612,10 +612,11 @@ impl AppContext {
     /// Displays a platform modal for selecting paths.
     /// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
     /// If cancelled, a `None` will be relayed instead.
+    /// May return an error on Linux if the file picker couldn't be opened.
     pub fn prompt_for_paths(
         &self,
         options: PathPromptOptions,
-    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
         self.platform.prompt_for_paths(options)
     }
 
@@ -623,7 +624,11 @@ impl AppContext {
     /// The provided directory will be used to set the initial location.
     /// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
     /// If cancelled, a `None` will be relayed instead.
-    pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+    /// May return an error on Linux if the file picker couldn't be opened.
+    pub fn prompt_for_new_path(
+        &self,
+        directory: &Path,
+    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
         self.platform.prompt_for_new_path(directory)
     }
 

crates/gpui/src/platform.rs 🔗

@@ -137,8 +137,8 @@ pub(crate) trait Platform: 'static {
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,
-    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
-    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
+    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
     fn reveal_path(&self, path: &Path);
 
     fn on_quit(&self, callback: Box<dyn FnMut()>);

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -21,6 +21,7 @@ use std::{
 use anyhow::anyhow;
 use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
 use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
+use ashpd::desktop::ResponseError;
 use ashpd::{url, ActivationToken};
 use async_task::Runnable;
 use calloop::channel::Channel;
@@ -54,6 +55,9 @@ pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
 pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
 pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
 
+const FILE_PICKER_PORTAL_MISSING: &str =
+    "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
+
 pub trait LinuxClient {
     fn compositor_name(&self) -> &'static str;
     fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
@@ -256,7 +260,7 @@ impl<P: LinuxClient + 'static> Platform for P {
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,
-    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
         let (done_tx, done_rx) = oneshot::channel();
         self.foreground_executor()
             .spawn(async move {
@@ -274,7 +278,7 @@ impl<P: LinuxClient + 'static> Platform for P {
                     }
                 };
 
-                let result = OpenFileRequest::default()
+                let request = match OpenFileRequest::default()
                     .modal(true)
                     .title(title)
                     .accept_label("Select")
@@ -282,49 +286,68 @@ impl<P: LinuxClient + 'static> Platform for P {
                     .directory(options.directories)
                     .send()
                     .await
-                    .ok()
-                    .and_then(|request| request.response().ok())
-                    .and_then(|response| {
+                {
+                    Ok(request) => request,
+                    Err(err) => {
+                        let result = match err {
+                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
+                            err => err.into(),
+                        };
+                        done_tx.send(Err(result));
+                        return;
+                    }
+                };
+
+                let result = match request.response() {
+                    Ok(response) => Ok(Some(
                         response
                             .uris()
                             .iter()
-                            .map(|uri| uri.to_file_path().ok())
-                            .collect()
-                    });
-
+                            .filter_map(|uri| uri.to_file_path().ok())
+                            .collect::<Vec<_>>(),
+                    )),
+                    Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
+                    Err(e) => Err(e.into()),
+                };
                 done_tx.send(result);
             })
             .detach();
         done_rx
     }
 
-    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
         let (done_tx, done_rx) = oneshot::channel();
         let directory = directory.to_owned();
         self.foreground_executor()
             .spawn(async move {
-                let request = SaveFileRequest::default()
+                let request = match SaveFileRequest::default()
                     .modal(true)
                     .title("Select new path")
                     .accept_label("Accept")
-                    .current_folder(directory);
-
-                let result = if let Ok(request) = request {
-                    request
-                        .send()
-                        .await
-                        .ok()
-                        .and_then(|request| request.response().ok())
-                        .and_then(|response| {
-                            response
-                                .uris()
-                                .first()
-                                .and_then(|uri| uri.to_file_path().ok())
-                        })
-                } else {
-                    None
+                    .current_folder(directory)
+                    .expect("pathbuf should not be nul terminated")
+                    .send()
+                    .await
+                {
+                    Ok(request) => request,
+                    Err(err) => {
+                        let result = match err {
+                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
+                            err => err.into(),
+                        };
+                        done_tx.send(Err(result));
+                        return;
+                    }
                 };
 
+                let result = match request.response() {
+                    Ok(response) => Ok(response
+                        .uris()
+                        .first()
+                        .and_then(|uri| uri.to_file_path().ok())),
+                    Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
+                    Err(e) => Err(e.into()),
+                };
                 done_tx.send(result);
             })
             .detach();

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -602,7 +602,7 @@ impl Platform for MacPlatform {
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,
-    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
         let (done_tx, done_rx) = oneshot::channel();
         self.foreground_executor()
             .spawn(async move {
@@ -632,7 +632,7 @@ impl Platform for MacPlatform {
                         };
 
                         if let Some(done_tx) = done_tx.take() {
-                            let _ = done_tx.send(result);
+                            let _ = done_tx.send(Ok(result));
                         }
                     });
                     let block = block.copy();
@@ -643,7 +643,7 @@ impl Platform for MacPlatform {
         done_rx
     }
 
-    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
         let directory = directory.to_owned();
         let (done_tx, done_rx) = oneshot::channel();
         self.foreground_executor()
@@ -665,7 +665,7 @@ impl Platform for MacPlatform {
                         }
 
                         if let Some(done_tx) = done_tx.take() {
-                            let _ = done_tx.send(result);
+                            let _ = done_tx.send(Ok(result));
                         }
                     });
                     let block = block.copy();

crates/gpui/src/platform/test/platform.rs 🔗

@@ -34,7 +34,7 @@ pub(crate) struct TestPlatform {
 #[derive(Default)]
 pub(crate) struct TestPrompts {
     multiple_choice: VecDeque<oneshot::Sender<usize>>,
-    new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
+    new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
 }
 
 impl TestPlatform {
@@ -80,7 +80,7 @@ impl TestPlatform {
             .new_path
             .pop_front()
             .expect("no pending new path prompt");
-        tx.send(select_path(&path)).ok();
+        tx.send(Ok(select_path(&path))).ok();
     }
 
     pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
@@ -216,14 +216,14 @@ impl Platform for TestPlatform {
     fn prompt_for_paths(
         &self,
         _options: crate::PathPromptOptions,
-    ) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+    ) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
         unimplemented!()
     }
 
     fn prompt_for_new_path(
         &self,
         directory: &std::path::Path,
-    ) -> oneshot::Receiver<Option<std::path::PathBuf>> {
+    ) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
         let (tx, rx) = oneshot::channel();
         self.prompts
             .borrow_mut()

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -316,7 +316,10 @@ impl Platform for WindowsPlatform {
         self.state.borrow_mut().callbacks.open_urls = Some(callback);
     }
 
-    fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
+    fn prompt_for_paths(
+        &self,
+        options: PathPromptOptions,
+    ) -> Receiver<Result<Option<Vec<PathBuf>>>> {
         let (tx, rx) = oneshot::channel();
 
         self.foreground_executor()
@@ -355,7 +358,7 @@ impl Platform for WindowsPlatform {
                     if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
                         // user canceled error
                         if let Some(tx) = tx.take() {
-                            tx.send(None).unwrap();
+                            tx.send(Ok(None)).unwrap();
                         }
                         return;
                     }
@@ -374,10 +377,10 @@ impl Platform for WindowsPlatform {
                 }
 
                 if let Some(tx) = tx.take() {
-                    if paths.len() == 0 {
-                        tx.send(None).unwrap();
+                    if paths.is_empty() {
+                        tx.send(Ok(None)).unwrap();
                     } else {
-                        tx.send(Some(paths)).unwrap();
+                        tx.send(Ok(Some(paths))).unwrap();
                     }
                 }
             })
@@ -386,27 +389,27 @@ impl Platform for WindowsPlatform {
         rx
     }
 
-    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
+    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
         let directory = directory.to_owned();
         let (tx, rx) = oneshot::channel();
         self.foreground_executor()
             .spawn(async move {
                 unsafe {
                     let Ok(dialog) = show_savefile_dialog(directory) else {
-                        let _ = tx.send(None);
+                        let _ = tx.send(Ok(None));
                         return;
                     };
                     let Ok(_) = dialog.Show(None) else {
-                        let _ = tx.send(None); // user cancel
+                        let _ = tx.send(Ok(None)); // user cancel
                         return;
                     };
                     if let Ok(shell_item) = dialog.GetResult() {
                         if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
-                            let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
+                            let _ = tx.send(Ok(Some(PathBuf::from(file.to_string().unwrap()))));
                             return;
                         }
                     }
-                    let _ = tx.send(None);
+                    let _ = tx.send(Ok(None));
                 }
             })
             .detach();

crates/workspace/src/notifications.rs 🔗

@@ -160,14 +160,23 @@ impl Workspace {
         self.show_notification(
             NotificationId::unique::<WorkspaceErrorNotification>(),
             cx,
-            |cx| {
-                cx.new_view(|_cx| {
-                    simple_message_notification::MessageNotification::new(format!("Error: {err:#}"))
-                })
-            },
+            |cx| cx.new_view(|_cx| ErrorMessagePrompt::new(format!("Error: {err:#}"))),
         );
     }
 
+    pub fn show_portal_error(&mut self, err: String, cx: &mut ViewContext<Self>) {
+        struct PortalError;
+
+        self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
+            cx.new_view(|_cx| {
+                ErrorMessagePrompt::new(err.to_string()).with_link_button(
+                    "See docs",
+                    "https://zed.dev/docs/linux#i-cant-open-any-files",
+                )
+            })
+        });
+    }
+
     pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
         self.dismiss_notification_internal(id, cx)
     }
@@ -349,6 +358,84 @@ impl Render for LanguageServerPrompt {
 
 impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
 
+pub struct ErrorMessagePrompt {
+    message: SharedString,
+    label_and_url_button: Option<(SharedString, SharedString)>,
+}
+
+impl ErrorMessagePrompt {
+    pub fn new<S>(message: S) -> Self
+    where
+        S: Into<SharedString>,
+    {
+        Self {
+            message: message.into(),
+            label_and_url_button: None,
+        }
+    }
+
+    pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
+    where
+        S: Into<SharedString>,
+    {
+        self.label_and_url_button = Some((label.into(), url.into()));
+        self
+    }
+}
+
+impl Render for ErrorMessagePrompt {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        h_flex()
+            .id("error_message_prompt_notification")
+            .occlude()
+            .elevation_3(cx)
+            .items_start()
+            .justify_between()
+            .p_2()
+            .gap_2()
+            .w_full()
+            .child(
+                v_flex()
+                    .w_full()
+                    .child(
+                        h_flex()
+                            .w_full()
+                            .justify_between()
+                            .child(
+                                svg()
+                                    .size(cx.text_style().font_size)
+                                    .flex_none()
+                                    .mr_2()
+                                    .mt(px(-2.0))
+                                    .map(|icon| {
+                                        icon.path(IconName::ExclamationTriangle.path())
+                                            .text_color(Color::Error.color(cx))
+                                    }),
+                            )
+                            .child(
+                                ui::IconButton::new("close", ui::IconName::Close)
+                                    .on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .max_w_80()
+                            .child(Label::new(self.message.clone()).size(LabelSize::Small)),
+                    )
+                    .when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
+                        elm.child(
+                            div().mt_2().child(
+                                ui::Button::new("error_message_prompt_notification_button", label)
+                                    .on_click(move |_, cx| cx.open_url(&url)),
+                            ),
+                        )
+                    }),
+            )
+    }
+}
+
+impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
+
 pub mod simple_message_notification {
     use gpui::{
         div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,

crates/workspace/src/workspace.rs 🔗

@@ -30,10 +30,10 @@ use gpui::{
     action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
     transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
     AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
-    EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
-    Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
-    Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
-    WindowOptions,
+    EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
+    ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
+    ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds,
+    WindowHandle, WindowOptions,
 };
 use item::{
     FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@@ -307,13 +307,31 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
 
             if let Some(app_state) = app_state.upgrade() {
                 cx.spawn(move |cx| async move {
-                    if let Some(paths) = paths.await.log_err().flatten() {
-                        cx.update(|cx| {
-                            open_paths(&paths, app_state, OpenOptions::default(), cx)
-                                .detach_and_log_err(cx)
-                        })
-                        .ok();
-                    }
+                    match Flatten::flatten(paths.await.map_err(|e| e.into())) {
+                        Ok(Some(paths)) => {
+                            cx.update(|cx| {
+                                open_paths(&paths, app_state, OpenOptions::default(), cx)
+                                    .detach_and_log_err(cx)
+                            })
+                            .ok();
+                        }
+                        Ok(None) => {}
+                        Err(err) => {
+                            cx.update(|cx| {
+                                if let Some(workspace_window) = cx
+                                    .active_window()
+                                    .and_then(|window| window.downcast::<Workspace>())
+                                {
+                                    workspace_window
+                                        .update(cx, |workspace, cx| {
+                                            workspace.show_portal_error(err.to_string(), cx);
+                                        })
+                                        .ok();
+                                }
+                            })
+                            .ok();
+                        }
+                    };
                 })
                 .detach();
             }
@@ -1296,7 +1314,15 @@ impl Workspace {
             let (tx, rx) = oneshot::channel();
             let abs_path = cx.prompt_for_new_path(&start_abs_path);
             cx.spawn(|this, mut cx| async move {
-                let abs_path = abs_path.await?;
+                let abs_path: Option<PathBuf> =
+                    Flatten::flatten(abs_path.await.map_err(|e| e.into())).map_err(|err| {
+                        this.update(&mut cx, |this, cx| {
+                            this.show_portal_error(err.to_string(), cx);
+                        })
+                        .ok();
+                        err
+                    })?;
+
                 let project_path = abs_path.and_then(|abs_path| {
                     this.update(&mut cx, |this, cx| {
                         this.project.update(cx, |project, cx| {
@@ -1585,8 +1611,16 @@ impl Workspace {
         });
 
         cx.spawn(|this, mut cx| async move {
-            let Some(paths) = paths.await.log_err().flatten() else {
-                return;
+            let paths = match Flatten::flatten(paths.await.map_err(|e| e.into())) {
+                Ok(Some(paths)) => paths,
+                Ok(None) => return,
+                Err(err) => {
+                    this.update(&mut cx, |this, cx| {
+                        this.show_portal_error(err.to_string(), cx);
+                    })
+                    .ok();
+                    return;
+                }
             };
 
             if let Some(task) = this
@@ -1748,7 +1782,14 @@ impl Workspace {
             multiple: true,
         });
         cx.spawn(|this, mut cx| async move {
-            if let Some(paths) = paths.await.log_err().flatten() {
+            let paths = Flatten::flatten(paths.await.map_err(|e| e.into())).map_err(|err| {
+                this.update(&mut cx, |this, cx| {
+                    this.show_portal_error(err.to_string(), cx);
+                })
+                .ok();
+                err
+            })?;
+            if let Some(paths) = paths {
                 let results = this
                     .update(&mut cx, |this, cx| {
                         this.open_paths(paths, OpenVisible::All, None, cx)