git: Add create git worktree hook to task system (#51337)

Anthony Eid , Remco Smits , and Richard Feldman created

### Summary

This PR starts work on adding basic hook support in the `TaskStore`. To
enable users to setup tasks that are ran when the agent panel creates a
new git worktree to start a thread in. It also adds a new task variable
called `ZED_MAIN_GIT_WORKTREE` that's the absolute path to the main repo
that the newly created linked worktree is based off of.

### Follow Ups 

- Get this hook working with the git worktree picker as well
- Make a more general approach to the hook system in `TaskStore`
- Add `ZED_PORT_{1..10}` task variables
- Migrate the task context creation code from `task_ui` to the basic
context provider

Before you mark this PR as ready for review, make sure that you have:
- [ ] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>

Change summary

.zed/tasks.json                              |   6 
crates/agent_ui/src/agent_panel.rs           |   6 +
crates/project/src/git_store.rs              |  20 ++++
crates/project/src/project.rs                |  93 +++++++++---------
crates/project/src/task_inventory.rs         |  38 ++++++
crates/project/src/task_store.rs             |  15 ++
crates/project/tests/integration/debugger.rs |   4 
crates/remote_server/src/headless_project.rs |   1 
crates/settings/src/settings_store.rs        |   4 
crates/task/src/task.rs                      |  10 +
crates/task/src/task_template.rs             |  11 ++
crates/tasks_ui/src/tasks_ui.rs              |   6 +
crates/workspace/src/tasks.rs                | 108 +++++++++++++++++++++
13 files changed, 259 insertions(+), 63 deletions(-)

Detailed changes

.zed/tasks.json 🔗

@@ -4,13 +4,13 @@
     "command": "./script/clippy",
     "args": [],
     "allow_concurrent_runs": true,
-    "use_new_terminal": false
+    "use_new_terminal": false,
   },
   {
     "label": "cargo run --profile release-fast",
     "command": "cargo",
     "args": ["run", "--profile", "release-fast"],
     "allow_concurrent_runs": true,
-    "use_new_terminal": false
-  }
+    "use_new_terminal": false,
+  },
 ]

crates/agent_ui/src/agent_panel.rs 🔗

@@ -2588,7 +2588,7 @@ impl AgentPanel {
             anyhow::Ok(())
         });
 
-        self._worktree_creation_task = Some(cx.foreground_executor().spawn(async move {
+        self._worktree_creation_task = Some(cx.background_spawn(async move {
             task.await.log_err();
         }));
     }
@@ -2745,6 +2745,10 @@ impl AgentPanel {
 
         new_window_handle.update(cx, |multi_workspace, window, cx| {
             multi_workspace.activate(new_workspace.clone(), window, cx);
+
+            new_workspace.update(cx, |workspace, cx| {
+                workspace.run_create_worktree_tasks(window, cx);
+            })
         })?;
 
         this.update_in(cx, |this, window, cx| {

crates/project/src/git_store.rs 🔗

@@ -1799,6 +1799,26 @@ impl GitStore {
         &self.repositories
     }
 
+    /// Returns the original (main) repository working directory for the given worktree.
+    /// For normal checkouts this equals the worktree's own path; for linked
+    /// worktrees it points back to the original repo.
+    pub fn original_repo_path_for_worktree(
+        &self,
+        worktree_id: WorktreeId,
+        cx: &App,
+    ) -> Option<Arc<Path>> {
+        self.active_repo_id
+            .iter()
+            .chain(self.worktree_ids.keys())
+            .find(|repo_id| {
+                self.worktree_ids
+                    .get(repo_id)
+                    .is_some_and(|ids| ids.contains(&worktree_id))
+            })
+            .and_then(|repo_id| self.repositories.get(repo_id))
+            .map(|repo| repo.read(cx).snapshot().original_repo_abs_path)
+    }
+
     pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
         let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
         let status = repo.read(cx).snapshot.status_for_path(&path)?;

crates/project/src/project.rs 🔗

@@ -1230,12 +1230,23 @@ impl Project {
                 )
             });
 
+            let git_store = cx.new(|cx| {
+                GitStore::local(
+                    &worktree_store,
+                    buffer_store.clone(),
+                    environment.clone(),
+                    fs.clone(),
+                    cx,
+                )
+            });
+
             let task_store = cx.new(|cx| {
                 TaskStore::local(
                     buffer_store.downgrade(),
                     worktree_store.clone(),
                     toolchain_store.read(cx).as_language_toolchain_store(),
                     environment.clone(),
+                    git_store.clone(),
                     cx,
                 )
             });
@@ -1271,16 +1282,6 @@ impl Project {
                 )
             });
 
-            let git_store = cx.new(|cx| {
-                GitStore::local(
-                    &worktree_store,
-                    buffer_store.clone(),
-                    environment.clone(),
-                    fs.clone(),
-                    cx,
-                )
-            });
-
             let agent_server_store = cx.new(|cx| {
                 AgentServerStore::local(
                     node.clone(),
@@ -1415,30 +1416,6 @@ impl Project {
                 )
             });
 
-            let task_store = cx.new(|cx| {
-                TaskStore::remote(
-                    buffer_store.downgrade(),
-                    worktree_store.clone(),
-                    toolchain_store.read(cx).as_language_toolchain_store(),
-                    remote.read(cx).proto_client(),
-                    REMOTE_SERVER_PROJECT_ID,
-                    cx,
-                )
-            });
-
-            let settings_observer = cx.new(|cx| {
-                SettingsObserver::new_remote(
-                    fs.clone(),
-                    worktree_store.clone(),
-                    task_store.clone(),
-                    Some(remote_proto.clone()),
-                    false,
-                    cx,
-                )
-            });
-            cx.subscribe(&settings_observer, Self::on_settings_observer_event)
-                .detach();
-
             let context_server_store = cx.new(|cx| {
                 ContextServerStore::remote(
                     rpc::proto::REMOTE_SERVER_PROJECT_ID,
@@ -1503,6 +1480,31 @@ impl Project {
                 )
             });
 
+            let task_store = cx.new(|cx| {
+                TaskStore::remote(
+                    buffer_store.downgrade(),
+                    worktree_store.clone(),
+                    toolchain_store.read(cx).as_language_toolchain_store(),
+                    remote.read(cx).proto_client(),
+                    REMOTE_SERVER_PROJECT_ID,
+                    git_store.clone(),
+                    cx,
+                )
+            });
+
+            let settings_observer = cx.new(|cx| {
+                SettingsObserver::new_remote(
+                    fs.clone(),
+                    worktree_store.clone(),
+                    task_store.clone(),
+                    Some(remote_proto.clone()),
+                    false,
+                    cx,
+                )
+            });
+            cx.subscribe(&settings_observer, Self::on_settings_observer_event)
+                .detach();
+
             let agent_server_store = cx.new(|_| {
                 AgentServerStore::remote(
                     REMOTE_SERVER_PROJECT_ID,
@@ -1732,6 +1734,17 @@ impl Project {
             )
         });
 
+        let git_store = cx.new(|cx| {
+            GitStore::remote(
+                // In this remote case we pass None for the environment
+                &worktree_store,
+                buffer_store.clone(),
+                client.clone().into(),
+                remote_id,
+                cx,
+            )
+        });
+
         let task_store = cx.new(|cx| {
             if run_tasks {
                 TaskStore::remote(
@@ -1740,6 +1753,7 @@ impl Project {
                     Arc::new(EmptyToolchainStore),
                     client.clone().into(),
                     remote_id,
+                    git_store.clone(),
                     cx,
                 )
             } else {
@@ -1758,17 +1772,6 @@ impl Project {
             )
         });
 
-        let git_store = cx.new(|cx| {
-            GitStore::remote(
-                // In this remote case we pass None for the environment
-                &worktree_store,
-                buffer_store.clone(),
-                client.clone().into(),
-                remote_id,
-                cx,
-            )
-        });
-
         let agent_server_store = cx.new(|_cx| AgentServerStore::collab());
         let replica_id = ReplicaId::new(response.payload.replica_id as u16);
 

crates/project/src/task_inventory.rs 🔗

@@ -21,14 +21,14 @@ use lsp::{LanguageServerId, LanguageServerName};
 use paths::{debug_task_file_name, task_file_name};
 use settings::{InvalidSettingsError, parse_json_with_comments};
 use task::{
-    DebugScenario, ResolvedTask, SharedTaskContext, TaskContext, TaskId, TaskTemplate,
+    DebugScenario, ResolvedTask, SharedTaskContext, TaskContext, TaskHook, TaskId, TaskTemplate,
     TaskTemplates, TaskVariables, VariableName,
 };
 use text::{BufferId, Point, ToPoint};
 use util::{NumericPrefixWithSuffix, ResultExt as _, post_inc, rel_path::RelPath};
 use worktree::WorktreeId;
 
-use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
+use crate::{git_store::GitStore, task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
 
 #[derive(Clone, Debug, Default)]
 pub struct DebugScenarioContext {
@@ -644,6 +644,19 @@ impl Inventory {
         self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
     }
 
+    /// Returns all task templates (worktree and global) that have at least one
+    /// hook in the provided set.
+    pub fn templates_with_hooks(
+        &self,
+        hooks: &HashSet<TaskHook>,
+        worktree: WorktreeId,
+    ) -> Vec<(TaskSourceKind, TaskTemplate)> {
+        self.worktree_templates_from_settings(worktree)
+            .chain(self.global_templates_from_settings())
+            .filter(|(_, template)| !template.hooks.is_disjoint(hooks))
+            .collect()
+    }
+
     fn global_templates_from_settings(
         &self,
     ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
@@ -918,11 +931,15 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
 /// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
 pub struct BasicContextProvider {
     worktree_store: Entity<WorktreeStore>,
+    git_store: Entity<GitStore>,
 }
 
 impl BasicContextProvider {
-    pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
-        Self { worktree_store }
+    pub fn new(worktree_store: Entity<WorktreeStore>, git_store: Entity<GitStore>) -> Self {
+        Self {
+            worktree_store,
+            git_store,
+        }
     }
 }
 
@@ -1002,6 +1019,19 @@ impl ContextProvider for BasicContextProvider {
             }
         }
 
+        if let Some(worktree_id) = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)) {
+            if let Some(path) = self
+                .git_store
+                .read(cx)
+                .original_repo_path_for_worktree(worktree_id, cx)
+            {
+                task_variables.insert(
+                    VariableName::MainGitWorktree,
+                    path.to_string_lossy().into_owned(),
+                );
+            }
+        }
+
         if let Some(current_file) = current_file {
             let path = current_file.abs_path(cx);
             if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {

crates/project/src/task_store.rs 🔗

@@ -19,7 +19,7 @@ use util::ResultExt;
 
 use crate::{
     BasicContextProvider, Inventory, ProjectEnvironment, buffer_store::BufferStore,
-    worktree_store::WorktreeStore,
+    git_store::GitStore, worktree_store::WorktreeStore,
 };
 
 // platform-dependent warning
@@ -33,6 +33,7 @@ pub struct StoreState {
     task_inventory: Entity<Inventory>,
     buffer_store: WeakEntity<BufferStore>,
     worktree_store: Entity<WorktreeStore>,
+    git_store: Entity<GitStore>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
 }
 
@@ -163,6 +164,7 @@ impl TaskStore {
         worktree_store: Entity<WorktreeStore>,
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         environment: Entity<ProjectEnvironment>,
+        git_store: Entity<GitStore>,
         cx: &mut Context<Self>,
     ) -> Self {
         Self::Functional(StoreState {
@@ -172,6 +174,7 @@ impl TaskStore {
             },
             task_inventory: Inventory::new(cx),
             buffer_store,
+            git_store,
             toolchain_store,
             worktree_store,
         })
@@ -183,6 +186,7 @@ impl TaskStore {
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         upstream_client: AnyProtoClient,
         project_id: u64,
+        git_store: Entity<GitStore>,
         cx: &mut Context<Self>,
     ) -> Self {
         Self::Functional(StoreState {
@@ -192,6 +196,7 @@ impl TaskStore {
             },
             task_inventory: Inventory::new(cx),
             buffer_store,
+            git_store,
             toolchain_store,
             worktree_store,
         })
@@ -207,6 +212,7 @@ impl TaskStore {
             TaskStore::Functional(state) => match &state.mode {
                 StoreMode::Local { environment, .. } => local_task_context_for_location(
                     state.worktree_store.clone(),
+                    state.git_store.clone(),
                     state.toolchain_store.clone(),
                     environment.clone(),
                     captured_variables,
@@ -220,6 +226,7 @@ impl TaskStore {
                     *project_id,
                     upstream_client.clone(),
                     state.worktree_store.clone(),
+                    state.git_store.clone(),
                     captured_variables,
                     location,
                     state.toolchain_store.clone(),
@@ -302,6 +309,7 @@ impl TaskStore {
 
 fn local_task_context_for_location(
     worktree_store: Entity<WorktreeStore>,
+    git_store: Entity<GitStore>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
     environment: Entity<ProjectEnvironment>,
     captured_variables: TaskVariables,
@@ -329,7 +337,7 @@ fn local_task_context_for_location(
                     worktree_store.clone(),
                     location,
                     project_env.clone(),
-                    BasicContextProvider::new(worktree_store),
+                    BasicContextProvider::new(worktree_store, git_store),
                     toolchain_store,
                     cx,
                 )
@@ -351,6 +359,7 @@ fn remote_task_context_for_location(
     project_id: u64,
     upstream_client: AnyProtoClient,
     worktree_store: Entity<WorktreeStore>,
+    git_store: Entity<GitStore>,
     captured_variables: TaskVariables,
     location: Location,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
@@ -362,7 +371,7 @@ fn remote_task_context_for_location(
             .update(|cx| {
                 let worktree_root = worktree_root(&worktree_store, &location, cx);
 
-                BasicContextProvider::new(worktree_store).build_context(
+                BasicContextProvider::new(worktree_store, git_store).build_context(
                     &TaskVariables::default(),
                     ContextLocation {
                         fs: None,

crates/project/tests/integration/debugger.rs 🔗

@@ -23,6 +23,7 @@ mod go_locator {
             show_summary: true,
             show_command: true,
             save: SaveStrategy::default(),
+            hooks: Default::default(),
         };
 
         let scenario = locator
@@ -51,6 +52,7 @@ mod go_locator {
             show_summary: true,
             show_command: true,
             save: SaveStrategy::default(),
+            hooks: Default::default(),
         };
 
         let scenario = locator
@@ -190,6 +192,7 @@ mod go_locator {
             show_summary: true,
             show_command: true,
             save: SaveStrategy::default(),
+            hooks: Default::default(),
         };
 
         let scenario = locator
@@ -225,6 +228,7 @@ mod python_locator {
             show_summary: false,
             show_command: false,
             save: task::SaveStrategy::default(),
+            hooks: Default::default(),
         };
 
         let expected_scenario = DebugScenario {

crates/remote_server/src/headless_project.rs 🔗

@@ -191,6 +191,7 @@ impl HeadlessProject {
                 worktree_store.clone(),
                 toolchain_store.read(cx).as_language_toolchain_store(),
                 environment.clone(),
+                git_store.clone(),
                 cx,
             );
             task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);

crates/settings/src/settings_store.rs 🔗

@@ -1431,9 +1431,7 @@ impl std::fmt::Display for InvalidSettingsError {
             | InvalidSettingsError::DefaultSettings { message }
             | InvalidSettingsError::Tasks { message, .. }
             | InvalidSettingsError::Editorconfig { message, .. }
-            | InvalidSettingsError::Debug { message, .. } => {
-                write!(f, "{message}")
-            }
+            | InvalidSettingsError::Debug { message, .. } => write!(f, "{message}"),
         }
     }
 }

crates/task/src/task.rs 🔗

@@ -23,8 +23,8 @@ pub use debug_format::{
     Request, TcpArgumentsTemplate, ZedDebugConfig,
 };
 pub use task_template::{
-    DebugArgsRequest, HideStrategy, RevealStrategy, SaveStrategy, TaskTemplate, TaskTemplates,
-    substitute_variables_in_map, substitute_variables_in_str,
+    DebugArgsRequest, HideStrategy, RevealStrategy, SaveStrategy, TaskHook, TaskTemplate,
+    TaskTemplates, substitute_variables_in_map, substitute_variables_in_str,
 };
 pub use util::shell::{Shell, ShellKind};
 pub use util::shell_builder::ShellBuilder;
@@ -181,6 +181,10 @@ pub enum VariableName {
     /// Open a Picker to select a process ID to use in place
     /// Can only be used to debug configurations
     PickProcessId,
+    /// An absolute path of the main (original) git worktree for the current repository.
+    /// For normal checkouts, this equals the worktree root. For linked worktrees,
+    /// this is the original repo's working directory.
+    MainGitWorktree,
     /// Custom variable, provided by the plugin or other external source.
     /// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables.
     Custom(Cow<'static, str>),
@@ -216,6 +220,7 @@ impl FromStr for VariableName {
             "LANGUAGE" => Self::Language,
             "ROW" => Self::Row,
             "COLUMN" => Self::Column,
+            "MAIN_GIT_WORKTREE" => Self::MainGitWorktree,
             _ => {
                 if let Some(custom_name) =
                     without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX)
@@ -251,6 +256,7 @@ impl std::fmt::Display for VariableName {
             Self::Language => write!(f, "{ZED_VARIABLE_NAME_PREFIX}LANGUAGE"),
             Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
             Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"),
+            Self::MainGitWorktree => write!(f, "{ZED_VARIABLE_NAME_PREFIX}MAIN_GIT_WORKTREE"),
             Self::Custom(s) => write!(
                 f,
                 "{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"

crates/task/src/task_template.rs 🔗

@@ -75,6 +75,9 @@ pub struct TaskTemplate {
     /// Which edited buffers to save before running the task.
     #[serde(default)]
     pub save: SaveStrategy,
+    /// Hooks that this task runs when emitted.
+    #[serde(default)]
+    pub hooks: HashSet<TaskHook>,
 }
 
 #[derive(Deserialize, Eq, PartialEq, Clone, Debug)]
@@ -86,6 +89,14 @@ pub enum DebugArgsRequest {
     Attach(AttachRequest),
 }
 
+/// What to do with the terminal pane and tab, after the command was started.
+#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TaskHook {
+    #[serde(alias = "create_git_worktree")]
+    CreateWorktree,
+}
+
 /// What to do with the terminal pane and tab, after the command was started.
 #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -434,7 +434,9 @@ mod tests {
         )
         .await;
         let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
+        let (worktree_store, git_store) = project.read_with(cx, |project, _| {
+            (project.worktree_store(), project.git_store().clone())
+        });
         let rust_language = Arc::new(
             Language::new(
                 LanguageConfig {
@@ -451,6 +453,7 @@ mod tests {
             .unwrap()
             .with_context_provider(Some(Arc::new(BasicContextProvider::new(
                 worktree_store.clone(),
+                git_store.clone(),
             )))),
         );
 
@@ -474,6 +477,7 @@ mod tests {
             .unwrap()
             .with_context_provider(Some(Arc::new(BasicContextProvider::new(
                 worktree_store.clone(),
+                git_store.clone(),
             )))),
         );
 

crates/workspace/src/tasks.rs 🔗

@@ -1,13 +1,14 @@
 use std::process::ExitStatus;
 
 use anyhow::Result;
+use collections::HashSet;
 use gpui::{AppContext, Context, Entity, Task};
 use language::Buffer;
 use project::{TaskSourceKind, WorktreeId};
 use remote::ConnectionState;
 use task::{
     DebugScenario, ResolvedTask, SaveStrategy, SharedTaskContext, SpawnInTerminal, TaskContext,
-    TaskTemplate,
+    TaskHook, TaskTemplate, TaskVariables, VariableName,
 };
 use ui::Window;
 use util::TryFutureExt;
@@ -164,6 +165,111 @@ impl Workspace {
             Task::ready(None)
         }
     }
+
+    pub fn run_create_worktree_tasks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let project = self.project().clone();
+        let hooks = HashSet::from_iter([TaskHook::CreateWorktree]);
+
+        let worktree_tasks: Vec<(WorktreeId, TaskContext, Vec<TaskTemplate>)> = {
+            let project = project.read(cx);
+            let task_store = project.task_store();
+            let Some(inventory) = task_store.read(cx).task_inventory().cloned() else {
+                return;
+            };
+
+            let git_store = project.git_store().read(cx);
+
+            let mut worktree_tasks = Vec::new();
+            for worktree in project.worktrees(cx) {
+                let worktree = worktree.read(cx);
+                let worktree_id = worktree.id();
+                let worktree_abs_path = worktree.abs_path();
+
+                let templates: Vec<TaskTemplate> = inventory
+                    .read(cx)
+                    .templates_with_hooks(&hooks, worktree_id)
+                    .into_iter()
+                    .map(|(_, template)| template)
+                    .collect();
+
+                if templates.is_empty() {
+                    continue;
+                }
+
+                let mut task_variables = TaskVariables::default();
+                task_variables.insert(
+                    VariableName::WorktreeRoot,
+                    worktree_abs_path.to_string_lossy().into_owned(),
+                );
+
+                if let Some(path) = git_store.original_repo_path_for_worktree(worktree_id, cx) {
+                    task_variables.insert(
+                        VariableName::MainGitWorktree,
+                        path.to_string_lossy().into_owned(),
+                    );
+                }
+
+                let task_context = TaskContext {
+                    cwd: Some(worktree_abs_path.to_path_buf()),
+                    task_variables,
+                    project_env: Default::default(),
+                };
+
+                worktree_tasks.push((worktree_id, task_context, templates));
+            }
+            worktree_tasks
+        };
+
+        if worktree_tasks.is_empty() {
+            return;
+        }
+
+        let task = cx.spawn_in(window, async move |workspace, cx| {
+            let mut tasks = Vec::new();
+            for (worktree_id, task_context, templates) in worktree_tasks {
+                let id_base = format!("worktree_setup_{worktree_id}");
+
+                tasks.push(cx.spawn({
+                    let workspace = workspace.clone();
+                    async move |cx| {
+                        for task_template in templates {
+                            let Some(resolved) =
+                                task_template.resolve_task(&id_base, &task_context)
+                            else {
+                                continue;
+                            };
+
+                            let status = workspace.update_in(cx, |workspace, window, cx| {
+                                workspace.spawn_in_terminal(resolved.resolved, window, cx)
+                            })?;
+
+                            if let Some(result) = status.await {
+                                match result {
+                                    Ok(exit_status) if !exit_status.success() => {
+                                        log::error!(
+                                            "Git worktree setup task failed with status: {:?}",
+                                            exit_status.code()
+                                        );
+                                        break;
+                                    }
+                                    Err(error) => {
+                                        log::error!("Git worktree setup task error: {error:#}");
+                                        break;
+                                    }
+                                    _ => {}
+                                }
+                            }
+                        }
+                        anyhow::Ok(())
+                    }
+                }));
+            }
+
+            futures::future::join_all(tasks).await;
+            anyhow::Ok(())
+        });
+        task.detach_and_log_err(cx);
+    }
 }
 
 #[cfg(test)]