Replace modal error dialogs with status toasts

Richard Feldman created

Error prompts were showing intrusive modal dialogs for background
operations. Now errors show as non-blocking status toasts with the
worktree directory name, while detailed diagnostic info (full paths,
error chains, step that failed) is logged via log::error!().

The one remaining prompt is the 'Delete Anyway / Cancel' decision
dialog when WIP commit creation fails — that's a user choice, not
an error notification.

Change summary

Cargo.lock                    |   1 
crates/sidebar/Cargo.toml     |   1 
crates/sidebar/src/sidebar.rs | 234 +++++++++++++++++++++++-------------
3 files changed, 151 insertions(+), 85 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15880,6 +15880,7 @@ dependencies = [
  "language_model",
  "log",
  "menu",
+ "notifications",
  "platform_title_bar",
  "pretty_assertions",
  "project",

crates/sidebar/Cargo.toml 🔗

@@ -31,6 +31,7 @@ git.workspace = true
 gpui.workspace = true
 log.workspace = true
 menu.workspace = true
+notifications.workspace = true
 platform_title_bar.workspace = true
 project.workspace = true
 recent_projects.workspace = true

crates/sidebar/src/sidebar.rs 🔗

@@ -24,6 +24,7 @@ use gpui::{
 use menu::{
     Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,
 };
+use notifications::status_toast::{StatusToast, ToastIcon};
 use project::git_store;
 use project::{AgentId, AgentRegistryStore, Event as ProjectEvent, linked_worktree_short_name};
 use recent_projects::sidebar_recent_projects::SidebarRecentProjects;
@@ -2331,24 +2332,43 @@ impl Sidebar {
                         }
                     }
                     Err(err) => {
-                        log::error!("Failed to restore worktree: {err}");
+                        let worktree_dir = row
+                            .worktree_path
+                            .file_name()
+                            .map(|n| n.to_string_lossy().to_string())
+                            .unwrap_or_else(|| row.worktree_path.to_string_lossy().to_string());
+                        log::error!(
+                            "Failed to restore worktree: {err}\n\
+                             worktree path: {}\n\
+                             main repo path: {}\n\
+                             step: git worktree restoration",
+                            row.worktree_path.display(),
+                            main_repo_path.display(),
+                        );
 
                         // Clear pending state — leave thread on main repo.
                         store.update(cx, |store, cx| {
                             store.set_pending_worktree_restore(&metadata.session_id, None, cx);
                         });
 
-                        cx.prompt(
-                            PromptLevel::Warning,
-                            "Worktree restoration failed",
-                            Some(&format!(
-                                "Could not restore the git worktree. \
-                                 The thread has been associated with {} instead.",
-                                main_repo_path.display()
-                            )),
-                            &["OK"],
-                        )
-                        .await
+                        this.update_in(cx, |this, _window, cx| {
+                            let Some(multi_workspace) = this.multi_workspace.upgrade() else {
+                                return;
+                            };
+                            let workspace = multi_workspace.read(cx).workspace().clone();
+                            workspace.update(cx, |workspace, cx| {
+                                let toast = StatusToast::new(
+                                    format!("Failed to restore worktree \"{worktree_dir}\""),
+                                    cx,
+                                    |this, _cx| {
+                                        this.icon(
+                                            ToastIcon::new(IconName::XCircle).color(Color::Error),
+                                        )
+                                    },
+                                );
+                                workspace.toggle_status_toast(toast, cx);
+                            });
+                        })
                         .ok();
                     }
                 }
@@ -3144,6 +3164,10 @@ impl Sidebar {
 
         let worktree_path_str = worktree_path.to_string_lossy().to_string();
         let main_repo_path_str = main_repo_path.to_string_lossy().to_string();
+        let worktree_dir_name = worktree_path
+            .file_name()
+            .map(|n| n.to_string_lossy().to_string())
+            .unwrap_or_else(|| worktree_path_str.clone());
 
         let mut archived_worktree_id: Option<i64> = None;
 
@@ -3187,26 +3211,36 @@ impl Sidebar {
                         Err(_) => "HEAD SHA operation was canceled".into(),
                         Ok(Ok(Some(_))) => unreachable!(),
                     };
-                    log::error!("{reason} after WIP commits; attempting to undo");
+                    log::error!(
+                        "{reason} after WIP commits; attempting to undo\n\
+                         worktree path: {worktree_path_str}\n\
+                         main repo path: {main_repo_path_str}\n\
+                         step: reading HEAD SHA after WIP commits",
+                    );
                     let undo_ok = undo_wip_commits(cx).await;
                     unarchive(cx);
-                    let detail = if undo_ok {
-                        "Could not read the commit hash after creating \
-                         the WIP commit. The commit has been undone and \
-                         the thread has been restored to the sidebar."
-                    } else {
-                        "Could not read the commit hash after creating \
-                         the WIP commit. The commit could not be automatically \
-                         undone \u{2014} you may need to manually run `git reset HEAD~2` \
-                         on the worktree. The thread has been restored to the sidebar."
-                    };
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        "Failed to archive worktree",
-                        Some(detail),
-                        &["OK"],
-                    )
-                    .await
+                    if !undo_ok {
+                        log::error!(
+                            "Failed to undo WIP commits during rollback for worktree: {worktree_path_str}"
+                        );
+                    }
+                    let worktree_dir_name = worktree_dir_name.clone();
+                    cx.update(|window, cx| {
+                        if let Some(workspace) = Workspace::for_window(window, cx) {
+                            workspace.update(cx, |workspace, cx| {
+                                let toast = StatusToast::new(
+                                    format!("Failed to archive worktree \"{worktree_dir_name}\""),
+                                    cx,
+                                    |this, _cx| {
+                                        this.icon(
+                                            ToastIcon::new(IconName::XCircle).color(Color::Error),
+                                        )
+                                    },
+                                );
+                                workspace.toggle_status_toast(toast, cx);
+                            });
+                        }
+                    })
                     .ok();
                     return anyhow::Ok(());
                 }
@@ -3216,7 +3250,7 @@ impl Sidebar {
                 .update(cx, |store, cx| {
                     store.create_archived_worktree(
                         worktree_path_str.clone(),
-                        main_repo_path_str,
+                        main_repo_path_str.clone(),
                         branch_name,
                         commit_hash.clone(),
                         cx,
@@ -3250,30 +3284,43 @@ impl Sidebar {
                     }
 
                     if let Err(err) = link_result {
-                        log::error!("Failed to link thread to archived worktree: {err}");
+                        log::error!(
+                            "Failed to link thread to archived worktree: {err}\n\
+                             worktree path: {worktree_path_str}\n\
+                             main repo path: {main_repo_path_str}\n\
+                             step: linking thread to archived worktree record",
+                        );
                         store
                             .update(cx, |store, cx| store.delete_archived_worktree(id, cx))
                             .await
                             .log_err();
                         let undo_ok = undo_wip_commits(cx).await;
                         unarchive(cx);
-                        let detail = if undo_ok {
-                            "Could not link the thread to the archived worktree record. \
-                             The WIP commit has been undone and the thread \
-                             has been restored to the sidebar."
-                        } else {
-                            "Could not link the thread to the archived worktree record. \
-                             The WIP commit could not be automatically \
-                             undone \u{2014} you may need to manually run `git reset HEAD~2` \
-                             on the worktree. The thread has been restored to the sidebar."
-                        };
-                        cx.prompt(
-                            PromptLevel::Warning,
-                            "Failed to archive worktree",
-                            Some(detail),
-                            &["OK"],
-                        )
-                        .await
+                        if !undo_ok {
+                            log::error!(
+                                "Failed to undo WIP commits during rollback for worktree: {worktree_path_str}"
+                            );
+                        }
+                        let worktree_dir_name = worktree_dir_name.clone();
+                        cx.update(|window, cx| {
+                            if let Some(workspace) = Workspace::for_window(window, cx) {
+                                workspace.update(cx, |workspace, cx| {
+                                    let toast = StatusToast::new(
+                                        format!(
+                                            "Failed to archive worktree \"{worktree_dir_name}\""
+                                        ),
+                                        cx,
+                                        |this, _cx| {
+                                            this.icon(
+                                                ToastIcon::new(IconName::XCircle)
+                                                    .color(Color::Error),
+                                            )
+                                        },
+                                    );
+                                    workspace.toggle_status_toast(toast, cx);
+                                });
+                            }
+                        })
                         .ok();
                         return anyhow::Ok(());
                     }
@@ -3296,26 +3343,36 @@ impl Sidebar {
                     }
                 }
                 Err(err) => {
-                    log::error!("Failed to create archived worktree record: {err}");
+                    log::error!(
+                        "Failed to create archived worktree record: {err}\n\
+                         worktree path: {worktree_path_str}\n\
+                         main repo path: {main_repo_path_str}\n\
+                         step: creating archived worktree database record",
+                    );
                     let undo_ok = undo_wip_commits(cx).await;
                     unarchive(cx);
-                    let detail = if undo_ok {
-                        "Could not save the archived worktree record. \
-                         The WIP commit has been undone and the thread \
-                         has been restored to the sidebar."
-                    } else {
-                        "Could not save the archived worktree record. \
-                         The WIP commit could not be automatically \
-                         undone \u{2014} you may need to manually run `git reset HEAD~2` \
-                         on the worktree. The thread has been restored to the sidebar."
-                    };
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        "Failed to archive worktree",
-                        Some(detail),
-                        &["OK"],
-                    )
-                    .await
+                    if !undo_ok {
+                        log::error!(
+                            "Failed to undo WIP commits during rollback for worktree: {worktree_path_str}"
+                        );
+                    }
+                    let worktree_dir_name = worktree_dir_name.clone();
+                    cx.update(|window, cx| {
+                        if let Some(workspace) = Workspace::for_window(window, cx) {
+                            workspace.update(cx, |workspace, cx| {
+                                let toast = StatusToast::new(
+                                    format!("Failed to archive worktree \"{worktree_dir_name}\""),
+                                    cx,
+                                    |this, _cx| {
+                                        this.icon(
+                                            ToastIcon::new(IconName::XCircle).color(Color::Error),
+                                        )
+                                    },
+                                );
+                                workspace.toggle_status_toast(toast, cx);
+                            });
+                        }
+                    })
                     .ok();
                     return anyhow::Ok(());
                 }
@@ -3388,24 +3445,31 @@ impl Sidebar {
                     .log_err();
             }
             unarchive(cx);
-            let detail = if undo_ok {
-                "Could not remove the worktree directory from disk. \
-                 Any WIP commits and archive records have been rolled \
-                 back, and the thread has been restored to the sidebar."
-            } else {
-                "Could not remove the worktree directory from disk. \
-                 The archive records have been rolled back, but the WIP \
-                 commits could not be automatically undone \u{2014} you may need \
-                 to manually run `git reset HEAD~2` on the worktree. \
-                 The thread has been restored to the sidebar."
-            };
-            cx.prompt(
-                PromptLevel::Warning,
-                "Failed to delete worktree",
-                Some(detail),
-                &["OK"],
-            )
-            .await
+            if !undo_ok {
+                log::error!(
+                    "Failed to undo WIP commits during rollback for worktree: {worktree_path_str}"
+                );
+            }
+            log::error!(
+                "Failed to delete worktree directory from disk\n\
+                 worktree path: {worktree_path_str}\n\
+                 main repo path: {main_repo_path_str}\n\
+                 step: removing worktree directory",
+            );
+            cx.update(|window, cx| {
+                if let Some(workspace) = Workspace::for_window(window, cx) {
+                    workspace.update(cx, |workspace, cx| {
+                        let toast = StatusToast::new(
+                            format!("Failed to delete worktree \"{worktree_dir_name}\""),
+                            cx,
+                            |this, _cx| {
+                                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
+                            },
+                        );
+                        workspace.toggle_status_toast(toast, cx);
+                    });
+                }
+            })
             .ok();
         }