Move task centering code closer to user input (#22082)

Kirill Bulatov created

Follow-up of https://github.com/zed-industries/zed/pull/22004 

* Reuse center terminals for tasks, when requested
* Extend task templates with `RevealTarget`, moving it from
`TaskSpawnTarget` into the core library
* Use `reveal_target` instead of `target` to avoid misinterpretations in
the task template context
* Do not expose `SpawnInTerminal` to user interface, avoid it
implementing `Serialize` and `Deserialize`
* Remove `NewCenterTask` action, extending `task::Spawn` interface
instead
* Do not require any extra unrelated parameters during task resolution,
instead, use task overrides on the resolved tasks on the modal side
* Add keybindings for opening the task modal in the
`RevealTarget::Center` mode

Release Notes:

- N/A

Change summary

Cargo.lock                                 |   2 
assets/keymaps/default-linux.json          |   5 
assets/keymaps/default-macos.json          |   5 
assets/settings/initial_tasks.json         |  10 
crates/editor/src/editor.rs                |   2 
crates/project/src/task_inventory.rs       |  45 ++--
crates/task/src/lib.rs                     |  19 -
crates/task/src/task_template.rs           |  49 ++--
crates/tasks_ui/src/lib.rs                 |  57 ++++-
crates/tasks_ui/src/modal.rs               |  64 +++++-
crates/terminal_view/src/terminal_panel.rs | 218 +++++++++++++++++------
crates/terminal_view/src/terminal_view.rs  |  94 ----------
crates/workspace/Cargo.toml                |   1 
crates/workspace/src/tasks.rs              |  22 -
crates/zed_actions/Cargo.toml              |   1 
crates/zed_actions/src/lib.rs              |  41 ++-
docs/src/tasks.md                          |   6 
17 files changed, 356 insertions(+), 285 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15603,7 +15603,6 @@ dependencies = [
  "ui",
  "util",
  "uuid",
- "zed_actions",
 ]
 
 [[package]]
@@ -16108,6 +16107,7 @@ name = "zed_actions"
 version = "0.1.0"
 dependencies = [
  "gpui",
+ "schemars",
  "serde",
 ]
 

assets/keymaps/default-linux.json 🔗

@@ -426,7 +426,10 @@
       "ctrl-shift-r": "task::Rerun",
       "ctrl-alt-r": "task::Rerun",
       "alt-t": "task::Rerun",
-      "alt-shift-t": "task::Spawn"
+      "alt-shift-t": "task::Spawn",
+      "alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
+      // also possible to spawn tasks by name:
+      // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
     }
   },
   // Bindings from Sublime Text

assets/keymaps/default-macos.json 🔗

@@ -495,8 +495,9 @@
     "bindings": {
       "cmd-shift-r": "task::Spawn",
       "cmd-alt-r": "task::Rerun",
-      "alt-t": "task::Spawn",
-      "alt-shift-t": "task::Spawn"
+      "ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
+      // also possible to spawn tasks by name:
+      // "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
     }
   },
   // Bindings from Sublime Text

assets/settings/initial_tasks.json 🔗

@@ -15,10 +15,14 @@
     // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
     "allow_concurrent_runs": false,
     // What to do with the terminal pane and tab, after the command was started:
-    // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
-    // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
-    // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
+    // * `always` — always show the task's pane, and focus the corresponding tab in it (default)
+    // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
+    // * `never` — do not alter focus, but still add/reuse the task's tab in its pane
     "reveal": "always",
+    // Where to place the task's terminal item after starting the task:
+    // * `dock` — in the terminal dock, "regular" terminal items' place (default)
+    // * `center` — in the central pane group, "main" editor area
+    "reveal_target": "dock",
     // What to do with the terminal pane and tab, after the command had finished:
     // * `never` — Do nothing when the command finishes (default)
     // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it

crates/editor/src/editor.rs 🔗

@@ -522,7 +522,7 @@ impl RunnableTasks {
     ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
         self.templates.iter().filter_map(|(kind, template)| {
             template
-                .resolve_task(&kind.to_id_base(), Default::default(), cx)
+                .resolve_task(&kind.to_id_base(), cx)
                 .map(|task| (kind.clone(), task))
         })
     }

crates/project/src/task_inventory.rs 🔗

@@ -184,7 +184,7 @@ impl Inventory {
                 let id_base = kind.to_id_base();
                 Some((
                     kind,
-                    task.resolve_task(&id_base, Default::default(), task_context)?,
+                    task.resolve_task(&id_base, task_context)?,
                     not_used_score,
                 ))
             })
@@ -378,7 +378,7 @@ mod test_inventory {
 
     use crate::Inventory;
 
-    use super::{task_source_kind_preference, TaskSourceKind};
+    use super::TaskSourceKind;
 
     pub(super) fn task_template_names(
         inventory: &Model<Inventory>,
@@ -409,7 +409,7 @@ mod test_inventory {
             let id_base = task_source_kind.to_id_base();
             inventory.task_scheduled(
                 task_source_kind.clone(),
-                task.resolve_task(&id_base, Default::default(), &TaskContext::default())
+                task.resolve_task(&id_base, &TaskContext::default())
                     .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
             );
         });
@@ -427,31 +427,12 @@ mod test_inventory {
                 .into_iter()
                 .filter_map(|(source_kind, task)| {
                     let id_base = source_kind.to_id_base();
-                    Some((
-                        source_kind,
-                        task.resolve_task(&id_base, Default::default(), task_context)?,
-                    ))
+                    Some((source_kind, task.resolve_task(&id_base, task_context)?))
                 })
                 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
                 .collect()
         })
     }
-
-    pub(super) async fn list_tasks_sorted_by_last_used(
-        inventory: &Model<Inventory>,
-        worktree: Option<WorktreeId>,
-        cx: &mut TestAppContext,
-    ) -> Vec<(TaskSourceKind, String)> {
-        let (used, current) = inventory.update(cx, |inventory, cx| {
-            inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
-        });
-        let mut all = used;
-        all.extend(current);
-        all.into_iter()
-            .map(|(source_kind, task)| (source_kind, task.resolved_label))
-            .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
-            .collect()
-    }
 }
 
 /// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
@@ -877,7 +858,7 @@ mod tests {
         TaskStore::init(None);
     }
 
-    pub(super) async fn resolved_task_names(
+    async fn resolved_task_names(
         inventory: &Model<Inventory>,
         worktree: Option<WorktreeId>,
         cx: &mut TestAppContext,
@@ -905,4 +886,20 @@ mod tests {
         ))
         .unwrap()
     }
+
+    async fn list_tasks_sorted_by_last_used(
+        inventory: &Model<Inventory>,
+        worktree: Option<WorktreeId>,
+        cx: &mut TestAppContext,
+    ) -> Vec<(TaskSourceKind, String)> {
+        let (used, current) = inventory.update(cx, |inventory, cx| {
+            inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
+        });
+        let mut all = used;
+        all.extend(current);
+        all.into_iter()
+            .map(|(source_kind, task)| (source_kind, task.resolved_label))
+            .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
+            .collect()
+    }
 }

crates/task/src/lib.rs 🔗

@@ -15,14 +15,15 @@ use std::str::FromStr;
 
 pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
 pub use vscode_format::VsCodeTaskFile;
+pub use zed_actions::RevealTarget;
 
 /// Task identifier, unique within the application.
 /// Based on it, task reruns and terminal tabs are managed.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
 pub struct TaskId(pub String);
 
 /// Contains all information needed by Zed to spawn a new terminal tab for the given task.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct SpawnInTerminal {
     /// Id of the task to use when determining task tab affinity.
     pub id: TaskId,
@@ -47,6 +48,8 @@ pub struct SpawnInTerminal {
     pub allow_concurrent_runs: bool,
     /// What to do with the terminal pane and tab, after the command was started.
     pub reveal: RevealStrategy,
+    /// Where to show tasks' terminal output.
+    pub reveal_target: RevealTarget,
     /// What to do with the terminal pane and tab, after the command had finished.
     pub hide: HideStrategy,
     /// Which shell to use when spawning the task.
@@ -57,15 +60,6 @@ pub struct SpawnInTerminal {
     pub show_command: bool,
 }
 
-/// An action for spawning a specific task
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct NewCenterTask {
-    /// The specification of the task to spawn.
-    pub action: SpawnInTerminal,
-}
-
-gpui::impl_actions!(tasks, [NewCenterTask]);
-
 /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task.
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct ResolvedTask {
@@ -84,9 +78,6 @@ pub struct ResolvedTask {
     /// Further actions that need to take place after the resolved task is spawned,
     /// with all task variables resolved.
     pub resolved: Option<SpawnInTerminal>,
-
-    /// where to sawn the task in the UI, either in the terminal panel or in the center pane
-    pub target: zed_actions::TaskSpawnTarget,
 }
 
 impl ResolvedTask {

crates/task/src/task_template.rs 🔗

@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
 use util::{truncate_and_remove_front, ResultExt};
 
 use crate::{
-    ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
+    ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
     ZED_VARIABLE_NAME_PREFIX,
 };
 
@@ -42,10 +42,16 @@ pub struct TaskTemplate {
     #[serde(default)]
     pub allow_concurrent_runs: bool,
     /// What to do with the terminal pane and tab, after the command was started:
-    /// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
-    /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
+    /// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
+    // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
+    // * `never` — do not alter focus, but still add/reuse the task's tab in its pane
     #[serde(default)]
     pub reveal: RevealStrategy,
+    /// Where to place the task's terminal item after starting the task.
+    /// * `dock` — in the terminal dock, "regular" terminal items' place (default).
+    /// * `center` — in the central pane group, "main" editor area.
+    #[serde(default)]
+    pub reveal_target: RevealTarget,
     /// What to do with the terminal pane and tab, after the command had finished:
     /// * `never` — do nothing when the command finishes (default)
     /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
@@ -70,12 +76,12 @@ pub struct TaskTemplate {
 #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum RevealStrategy {
-    /// Always show the terminal pane, add and focus the corresponding task's tab in it.
+    /// Always show the task's pane, and focus the corresponding tab in it.
     #[default]
     Always,
-    /// Always show the terminal pane, add the task's tab in it, but don't focus it.
+    /// Always show the task's pane, add the task's tab in it, but don't focus it.
     NoFocus,
-    /// Do not change terminal pane focus, but still add/reuse the task's tab there.
+    /// Do not alter focus, but still add/reuse the task's tab in its pane.
     Never,
 }
 
@@ -115,12 +121,7 @@ impl TaskTemplate {
     ///
     /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources),
     /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details.
-    pub fn resolve_task(
-        &self,
-        id_base: &str,
-        target: zed_actions::TaskSpawnTarget,
-        cx: &TaskContext,
-    ) -> Option<ResolvedTask> {
+    pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option<ResolvedTask> {
         if self.label.trim().is_empty() || self.command.trim().is_empty() {
             return None;
         }
@@ -219,7 +220,6 @@ impl TaskTemplate {
         Some(ResolvedTask {
             id: id.clone(),
             substituted_variables,
-            target,
             original_task: self.clone(),
             resolved_label: full_label.clone(),
             resolved: Some(SpawnInTerminal {
@@ -241,6 +241,7 @@ impl TaskTemplate {
                 use_new_terminal: self.use_new_terminal,
                 allow_concurrent_runs: self.allow_concurrent_runs,
                 reveal: self.reveal,
+                reveal_target: self.reveal_target,
                 hide: self.hide,
                 shell: self.shell.clone(),
                 show_summary: self.show_summary,
@@ -388,7 +389,7 @@ mod tests {
             },
         ] {
             assert_eq!(
-                task_with_blank_property.resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()),
+                task_with_blank_property.resolve_task(TEST_ID_BASE, &TaskContext::default()),
                 None,
                 "should not resolve task with blank label and/or command: {task_with_blank_property:?}"
             );
@@ -406,7 +407,7 @@ mod tests {
 
         let resolved_task = |task_template: &TaskTemplate, task_cx| {
             let resolved_task = task_template
-                .resolve_task(TEST_ID_BASE, Default::default(), task_cx)
+                .resolve_task(TEST_ID_BASE, task_cx)
                 .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
             assert_substituted_variables(&resolved_task, Vec::new());
             resolved_task
@@ -532,7 +533,6 @@ mod tests {
         for i in 0..15 {
             let resolved_task = task_with_all_variables.resolve_task(
                 TEST_ID_BASE,
-                Default::default(),
                 &TaskContext {
                     cwd: None,
                     task_variables: TaskVariables::from_iter(all_variables.clone()),
@@ -621,7 +621,6 @@ mod tests {
             let removed_variable = not_all_variables.remove(i);
             let resolved_task_attempt = task_with_all_variables.resolve_task(
                 TEST_ID_BASE,
-                Default::default(),
                 &TaskContext {
                     cwd: None,
                     task_variables: TaskVariables::from_iter(not_all_variables),
@@ -638,10 +637,10 @@ mod tests {
             label: "My task".into(),
             command: "echo".into(),
             args: vec!["$PATH".into()],
-            ..Default::default()
+            ..TaskTemplate::default()
         };
         let resolved_task = task
-            .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default())
+            .resolve_task(TEST_ID_BASE, &TaskContext::default())
             .unwrap();
         assert_substituted_variables(&resolved_task, Vec::new());
         let resolved = resolved_task.resolved.unwrap();
@@ -656,10 +655,10 @@ mod tests {
             label: "My task".into(),
             command: "echo".into(),
             args: vec!["$ZED_VARIABLE".into()],
-            ..Default::default()
+            ..TaskTemplate::default()
         };
         assert!(task
-            .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default())
+            .resolve_task(TEST_ID_BASE, &TaskContext::default())
             .is_none());
     }
 
@@ -709,7 +708,7 @@ mod tests {
         .enumerate()
         {
             let resolved = symbol_dependent_task
-                .resolve_task(TEST_ID_BASE, Default::default(), &cx)
+                .resolve_task(TEST_ID_BASE, &cx)
                 .unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}"));
             assert_eq!(
                 resolved.substituted_variables,
@@ -751,9 +750,7 @@ mod tests {
         context
             .task_variables
             .insert(VariableName::Symbol, "my-symbol".to_string());
-        assert!(faulty_go_test
-            .resolve_task("base", Default::default(), &context)
-            .is_some());
+        assert!(faulty_go_test.resolve_task("base", &context).is_some());
     }
 
     #[test]
@@ -812,7 +809,7 @@ mod tests {
         };
 
         let resolved = template
-            .resolve_task(TEST_ID_BASE, Default::default(), &context)
+            .resolve_task(TEST_ID_BASE, &context)
             .unwrap()
             .resolved
             .unwrap();

crates/tasks_ui/src/lib.rs 🔗

@@ -1,9 +1,9 @@
 use ::settings::Settings;
 use editor::{tasks::task_context, Editor};
 use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
-use modal::TasksModal;
+use modal::{TaskOverrides, TasksModal};
 use project::{Location, WorktreeId};
-use task::TaskId;
+use task::{RevealTarget, TaskId};
 use workspace::tasks::schedule_task;
 use workspace::{tasks::schedule_resolved_task, Workspace};
 
@@ -11,7 +11,6 @@ mod modal;
 mod settings;
 
 pub use modal::{Rerun, Spawn};
-use zed_actions::TaskSpawnTarget;
 
 pub fn init(cx: &mut AppContext) {
     settings::TaskSettings::register(cx);
@@ -54,7 +53,6 @@ pub fn init(cx: &mut AppContext) {
                                             task_source_kind,
                                             &original_task,
                                             &task_context,
-                                            Default::default(),
                                             false,
                                             cx,
                                         )
@@ -81,7 +79,7 @@ pub fn init(cx: &mut AppContext) {
                             );
                         }
                     } else {
-                        toggle_modal(workspace, cx).detach();
+                        toggle_modal(workspace, None, cx).detach();
                     };
                 });
         },
@@ -90,14 +88,25 @@ pub fn init(cx: &mut AppContext) {
 }
 
 fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
-    match &action.task_name {
-        Some(name) => spawn_task_with_name(name.clone(), action.target.unwrap_or_default(), cx)
-            .detach_and_log_err(cx),
-        None => toggle_modal(workspace, cx).detach(),
+    match action {
+        Spawn::ByName {
+            task_name,
+            reveal_target,
+        } => {
+            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
+                reveal_target: Some(reveal_target),
+            });
+            spawn_task_with_name(task_name.clone(), overrides, cx).detach_and_log_err(cx)
+        }
+        Spawn::ViaModal { reveal_target } => toggle_modal(workspace, *reveal_target, cx).detach(),
     }
 }
 
-fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
+fn toggle_modal(
+    workspace: &mut Workspace,
+    reveal_target: Option<RevealTarget>,
+    cx: &mut ViewContext<'_, Workspace>,
+) -> AsyncTask<()> {
     let task_store = workspace.project().read(cx).task_store().clone();
     let workspace_handle = workspace.weak_handle();
     let can_open_modal = workspace.project().update(cx, |project, cx| {
@@ -110,7 +119,15 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>)
             workspace
                 .update(&mut cx, |workspace, cx| {
                     workspace.toggle_modal(cx, |cx| {
-                        TasksModal::new(task_store.clone(), task_context, workspace_handle, cx)
+                        TasksModal::new(
+                            task_store.clone(),
+                            task_context,
+                            reveal_target.map(|target| TaskOverrides {
+                                reveal_target: Some(target),
+                            }),
+                            workspace_handle,
+                            cx,
+                        )
                     })
                 })
                 .ok();
@@ -122,7 +139,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>)
 
 fn spawn_task_with_name(
     name: String,
-    task_target: TaskSpawnTarget,
+    overrides: Option<TaskOverrides>,
     cx: &mut ViewContext<Workspace>,
 ) -> AsyncTask<anyhow::Result<()>> {
     cx.spawn(|workspace, mut cx| async move {
@@ -157,14 +174,18 @@ fn spawn_task_with_name(
 
         let did_spawn = workspace
             .update(&mut cx, |workspace, cx| {
-                let (task_source_kind, target_task) =
+                let (task_source_kind, mut target_task) =
                     tasks.into_iter().find(|(_, task)| task.label == name)?;
+                if let Some(overrides) = &overrides {
+                    if let Some(target_override) = overrides.reveal_target {
+                        target_task.reveal_target = target_override;
+                    }
+                }
                 schedule_task(
                     workspace,
                     task_source_kind,
                     &target_task,
                     &task_context,
-                    task_target,
                     false,
                     cx,
                 );
@@ -174,7 +195,13 @@ fn spawn_task_with_name(
         if !did_spawn {
             workspace
                 .update(&mut cx, |workspace, cx| {
-                    spawn_task_or_modal(workspace, &Spawn::default(), cx);
+                    spawn_task_or_modal(
+                        workspace,
+                        &Spawn::ViaModal {
+                            reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
+                        },
+                        cx,
+                    );
                 })
                 .ok();
         }

crates/tasks_ui/src/modal.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
 };
 use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
 use project::{task_store::TaskStore, TaskSourceKind};
-use task::{ResolvedTask, TaskContext, TaskTemplate};
+use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate};
 use ui::{
     div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
     FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
@@ -24,6 +24,7 @@ pub use zed_actions::{Rerun, Spawn};
 pub(crate) struct TasksModalDelegate {
     task_store: Model<TaskStore>,
     candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
+    task_overrides: Option<TaskOverrides>,
     last_used_candidate_index: Option<usize>,
     divider_index: Option<usize>,
     matches: Vec<StringMatch>,
@@ -34,12 +35,28 @@ pub(crate) struct TasksModalDelegate {
     placeholder_text: Arc<str>,
 }
 
+/// Task template amendments to do before resolving the context.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub(crate) struct TaskOverrides {
+    /// See [`RevealTarget`].
+    pub(crate) reveal_target: Option<RevealTarget>,
+}
+
 impl TasksModalDelegate {
     fn new(
         task_store: Model<TaskStore>,
         task_context: TaskContext,
+        task_overrides: Option<TaskOverrides>,
         workspace: WeakView<Workspace>,
     ) -> Self {
+        let placeholder_text = if let Some(TaskOverrides {
+            reveal_target: Some(RevealTarget::Center),
+        }) = &task_overrides
+        {
+            Arc::from("Find a task, or run a command in the central pane")
+        } else {
+            Arc::from("Find a task, or run a command")
+        };
         Self {
             task_store,
             workspace,
@@ -50,7 +67,8 @@ impl TasksModalDelegate {
             selected_index: 0,
             prompt: String::default(),
             task_context,
-            placeholder_text: Arc::from("Find a task, or run a command"),
+            task_overrides,
+            placeholder_text,
         }
     }
 
@@ -61,14 +79,20 @@ impl TasksModalDelegate {
 
         let source_kind = TaskSourceKind::UserInput;
         let id_base = source_kind.to_id_base();
-        let new_oneshot = TaskTemplate {
+        let mut new_oneshot = TaskTemplate {
             label: self.prompt.clone(),
             command: self.prompt.clone(),
             ..TaskTemplate::default()
         };
+        if let Some(TaskOverrides {
+            reveal_target: Some(reveal_target),
+        }) = &self.task_overrides
+        {
+            new_oneshot.reveal_target = *reveal_target;
+        }
         Some((
             source_kind,
-            new_oneshot.resolve_task(&id_base, Default::default(), &self.task_context)?,
+            new_oneshot.resolve_task(&id_base, &self.task_context)?,
         ))
     }
 
@@ -100,12 +124,13 @@ impl TasksModal {
     pub(crate) fn new(
         task_store: Model<TaskStore>,
         task_context: TaskContext,
+        task_overrides: Option<TaskOverrides>,
         workspace: WeakView<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let picker = cx.new_view(|cx| {
             Picker::uniform_list(
-                TasksModalDelegate::new(task_store, task_context, workspace),
+                TasksModalDelegate::new(task_store, task_context, task_overrides, workspace),
                 cx,
             )
         });
@@ -257,9 +282,17 @@ impl PickerDelegate for TasksModalDelegate {
                     .as_ref()
                     .map(|candidates| candidates[ix].clone())
             });
-        let Some((task_source_kind, task)) = task else {
+        let Some((task_source_kind, mut task)) = task else {
             return;
         };
+        if let Some(TaskOverrides {
+            reveal_target: Some(reveal_target),
+        }) = &self.task_overrides
+        {
+            if let Some(resolved_task) = &mut task.resolved {
+                resolved_task.reveal_target = *reveal_target;
+            }
+        }
 
         self.workspace
             .update(cx, |workspace, cx| {
@@ -396,9 +429,18 @@ impl PickerDelegate for TasksModalDelegate {
     }
 
     fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
-        let Some((task_source_kind, task)) = self.spawn_oneshot() else {
+        let Some((task_source_kind, mut task)) = self.spawn_oneshot() else {
             return;
         };
+
+        if let Some(TaskOverrides {
+            reveal_target: Some(reveal_target),
+        }) = self.task_overrides
+        {
+            if let Some(resolved_task) = &mut task.resolved {
+                resolved_task.reveal_target = reveal_target;
+            }
+        }
         self.workspace
             .update(cx, |workspace, cx| {
                 schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
@@ -682,9 +724,9 @@ mod tests {
             "No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)"
         );
 
-        cx.dispatch_action(Spawn {
-            task_name: Some("example task".to_string()),
-            target: None,
+        cx.dispatch_action(Spawn::ByName {
+            task_name: "example task".to_string(),
+            reveal_target: None,
         });
         let tasks_picker = workspace.update(cx, |workspace, cx| {
             workspace
@@ -995,7 +1037,7 @@ mod tests {
         workspace: &View<Workspace>,
         cx: &mut VisualTestContext,
     ) -> View<Picker<TasksModalDelegate>> {
-        cx.dispatch_action(Spawn::default());
+        cx.dispatch_action(Spawn::modal());
         workspace.update(cx, |workspace, cx| {
             workspace
                 .active_modal::<TasksModal>(cx)

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -12,7 +12,7 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use futures::future::join_all;
 use gpui::{
-    actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter,
+    actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter,
     ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
     Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
@@ -20,7 +20,7 @@ use itertools::Itertools;
 use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use settings::Settings;
-use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
+use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId};
 use terminal::{
     terminal_settings::{TerminalDockPosition, TerminalSettings},
     Terminal,
@@ -40,7 +40,7 @@ use workspace::{
     SplitUp, SwapPaneInDirection, ToggleZoom, Workspace,
 };
 
-use anyhow::Result;
+use anyhow::{anyhow, Context, Result};
 use zed_actions::InlineAssist;
 
 const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
@@ -53,11 +53,7 @@ pub fn init(cx: &mut AppContext) {
             workspace.register_action(TerminalPanel::new_terminal);
             workspace.register_action(TerminalPanel::open_terminal);
             workspace.register_action(|workspace, _: &ToggleFocus, cx| {
-                if workspace
-                    .panel::<TerminalPanel>(cx)
-                    .as_ref()
-                    .is_some_and(|panel| panel.read(cx).enabled)
-                {
+                if is_enabled_in_workspace(workspace, cx) {
                     workspace.toggle_panel_focus::<TerminalPanel>(cx);
                 }
             });
@@ -76,7 +72,6 @@ pub struct TerminalPanel {
     pending_serialization: Task<Option<()>>,
     pending_terminals_to_add: usize,
     deferred_tasks: HashMap<TaskId, Task<()>>,
-    enabled: bool,
     assistant_enabled: bool,
     assistant_tab_bar_button: Option<AnyView>,
 }
@@ -86,7 +81,6 @@ impl TerminalPanel {
         let project = workspace.project();
         let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
         let center = PaneGroup::new(pane.clone());
-        let enabled = project.read(cx).supports_terminal(cx);
         cx.focus_view(&pane);
         let terminal_panel = Self {
             center,
@@ -98,7 +92,6 @@ impl TerminalPanel {
             height: None,
             pending_terminals_to_add: 0,
             deferred_tasks: HashMap::default(),
-            enabled,
             assistant_enabled: false,
             assistant_tab_bar_button: None,
         };
@@ -492,8 +485,8 @@ impl TerminalPanel {
                     !use_new_terminal,
                     "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
                 );
-                this.update(&mut cx, |this, cx| {
-                    this.replace_terminal(
+                this.update(&mut cx, |terminal_panel, cx| {
+                    terminal_panel.replace_terminal(
                         spawn_task,
                         task_pane,
                         existing_item_index,
@@ -620,7 +613,17 @@ impl TerminalPanel {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Model<Terminal>>> {
         let reveal = spawn_task.reveal;
-        self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
+        let reveal_target = spawn_task.reveal_target;
+        let kind = TerminalKind::Task(spawn_task);
+        match reveal_target {
+            RevealTarget::Center => self
+                .workspace
+                .update(cx, |workspace, cx| {
+                    Self::add_center_terminal(workspace, kind, cx)
+                })
+                .unwrap_or_else(|e| Task::ready(Err(e))),
+            RevealTarget::Dock => self.add_terminal(kind, reveal, cx),
+        }
     }
 
     /// Create a new Terminal in the current working directory or the user's home directory
@@ -647,24 +650,40 @@ impl TerminalPanel {
         label: &str,
         cx: &mut AppContext,
     ) -> Vec<(usize, View<Pane>, View<TerminalView>)> {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Vec::new();
+        };
+
+        let pane_terminal_views = |pane: View<Pane>| {
+            pane.read(cx)
+                .items()
+                .enumerate()
+                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
+                .filter_map(|(index, terminal_view)| {
+                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
+                    if &task_state.full_label == label {
+                        Some((index, terminal_view))
+                    } else {
+                        None
+                    }
+                })
+                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
+        };
+
         self.center
             .panes()
             .into_iter()
-            .flat_map(|pane| {
-                pane.read(cx)
-                    .items()
-                    .enumerate()
-                    .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
-                    .filter_map(|(index, terminal_view)| {
-                        let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
-                        if &task_state.full_label == label {
-                            Some((index, terminal_view))
-                        } else {
-                            None
-                        }
-                    })
-                    .map(|(index, terminal_view)| (index, pane.clone(), terminal_view))
-            })
+            .cloned()
+            .flat_map(pane_terminal_views)
+            .chain(
+                workspace
+                    .read(cx)
+                    .panes()
+                    .into_iter()
+                    .cloned()
+                    .flat_map(pane_terminal_views),
+            )
+            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
             .collect()
     }
 
@@ -680,14 +699,48 @@ impl TerminalPanel {
         })
     }
 
+    pub fn add_center_terminal(
+        workspace: &mut Workspace,
+        kind: TerminalKind,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<Model<Terminal>>> {
+        if !is_enabled_in_workspace(workspace, cx) {
+            return Task::ready(Err(anyhow!(
+                "terminal not yet supported for remote projects"
+            )));
+        }
+        let window = cx.window_handle();
+        let project = workspace.project().downgrade();
+        cx.spawn(move |workspace, mut cx| async move {
+            let terminal = project
+                .update(&mut cx, |project, cx| {
+                    project.create_terminal(kind, window, cx)
+                })?
+                .await?;
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let view = cx.new_view(|cx| {
+                    TerminalView::new(
+                        terminal.clone(),
+                        workspace.weak_handle(),
+                        workspace.database_id(),
+                        cx,
+                    )
+                });
+                workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
+            })?;
+            Ok(terminal)
+        })
+    }
+
     fn add_terminal(
         &mut self,
         kind: TerminalKind,
         reveal_strategy: RevealStrategy,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Model<Terminal>>> {
-        if !self.enabled {
-            return Task::ready(Err(anyhow::anyhow!(
+        if !self.is_enabled(cx) {
+            return Task::ready(Err(anyhow!(
                 "terminal not yet supported for remote projects"
             )));
         }
@@ -786,10 +839,11 @@ impl TerminalPanel {
         cx: &mut ViewContext<'_, Self>,
     ) -> Task<Option<()>> {
         let reveal = spawn_task.reveal;
+        let reveal_target = spawn_task.reveal_target;
         let window = cx.window_handle();
         let task_workspace = self.workspace.clone();
-        cx.spawn(move |this, mut cx| async move {
-            let project = this
+        cx.spawn(move |terminal_panel, mut cx| async move {
+            let project = terminal_panel
                 .update(&mut cx, |this, cx| {
                     this.workspace
                         .update(cx, |workspace, _| workspace.project().clone())
@@ -811,32 +865,68 @@ impl TerminalPanel {
                 .ok()?;
 
             match reveal {
-                RevealStrategy::Always => {
-                    this.update(&mut cx, |this, cx| {
-                        this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
-                    })
-                    .ok()?;
-
-                    cx.spawn(|mut cx| async move {
+                RevealStrategy::Always => match reveal_target {
+                    RevealTarget::Center => {
                         task_workspace
-                            .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
-                            .ok()
-                    })
-                    .detach();
-                }
-                RevealStrategy::NoFocus => {
-                    this.update(&mut cx, |this, cx| {
-                        this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
-                    })
-                    .ok()?;
+                            .update(&mut cx, |workspace, cx| {
+                                workspace
+                                    .active_item(cx)
+                                    .context("retrieving active terminal item in the workspace")
+                                    .log_err()?
+                                    .focus_handle(cx)
+                                    .focus(cx);
+                                Some(())
+                            })
+                            .ok()??;
+                    }
+                    RevealTarget::Dock => {
+                        terminal_panel
+                            .update(&mut cx, |terminal_panel, cx| {
+                                terminal_panel.activate_terminal_view(
+                                    &task_pane,
+                                    terminal_item_index,
+                                    true,
+                                    cx,
+                                )
+                            })
+                            .ok()?;
 
-                    cx.spawn(|mut cx| async move {
+                        cx.spawn(|mut cx| async move {
+                            task_workspace
+                                .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
+                                .ok()
+                        })
+                        .detach();
+                    }
+                },
+                RevealStrategy::NoFocus => match reveal_target {
+                    RevealTarget::Center => {
                         task_workspace
-                            .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
-                            .ok()
-                    })
-                    .detach();
-                }
+                            .update(&mut cx, |workspace, cx| {
+                                workspace.active_pane().focus_handle(cx).focus(cx);
+                            })
+                            .ok()?;
+                    }
+                    RevealTarget::Dock => {
+                        terminal_panel
+                            .update(&mut cx, |terminal_panel, cx| {
+                                terminal_panel.activate_terminal_view(
+                                    &task_pane,
+                                    terminal_item_index,
+                                    false,
+                                    cx,
+                                )
+                            })
+                            .ok()?;
+
+                        cx.spawn(|mut cx| async move {
+                            task_workspace
+                                .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
+                                .ok()
+                        })
+                        .detach();
+                    }
+                },
                 RevealStrategy::Never => {}
             }
 
@@ -851,6 +941,16 @@ impl TerminalPanel {
     pub fn assistant_enabled(&self) -> bool {
         self.assistant_enabled
     }
+
+    fn is_enabled(&self, cx: &WindowContext) -> bool {
+        self.workspace.upgrade().map_or(false, |workspace| {
+            is_enabled_in_workspace(workspace.read(cx), cx)
+        })
+    }
+}
+
+fn is_enabled_in_workspace(workspace: &Workspace, cx: &WindowContext) -> bool {
+    workspace.project().read(cx).supports_terminal(cx)
 }
 
 pub fn new_terminal_pane(
@@ -1235,7 +1335,7 @@ impl Panel for TerminalPanel {
                 return;
             };
 
-            this.add_terminal(kind, RevealStrategy::Never, cx)
+            this.add_terminal(kind, RevealStrategy::Always, cx)
                 .detach_and_log_err(cx)
         })
     }
@@ -1259,7 +1359,9 @@ impl Panel for TerminalPanel {
     }
 
     fn icon(&self, cx: &WindowContext) -> Option<IconName> {
-        if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
+        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
+            && TerminalSettings::get_global(cx).button
+        {
             Some(IconName::Terminal)
         } else {
             None

crates/terminal_view/src/terminal_view.rs 🔗

@@ -14,7 +14,6 @@ use gpui::{
 use language::Bias;
 use persistence::TERMINAL_DB;
 use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
-use task::{NewCenterTask, RevealStrategy};
 use terminal::{
     alacritty_terminal::{
         index::Point,
@@ -31,7 +30,6 @@ use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
 use util::{paths::PathWithPosition, ResultExt};
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
-    notifications::NotifyResultExt,
     register_serializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
     CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
@@ -46,7 +44,7 @@ use zed_actions::InlineAssist;
 
 use std::{
     cmp,
-    ops::{ControlFlow, RangeInclusive},
+    ops::RangeInclusive,
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
@@ -81,7 +79,6 @@ pub fn init(cx: &mut AppContext) {
 
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {
         workspace.register_action(TerminalView::deploy);
-        workspace.register_action(TerminalView::deploy_center_task);
     })
     .detach();
 }
@@ -129,61 +126,6 @@ impl FocusableView for TerminalView {
 }
 
 impl TerminalView {
-    pub fn deploy_center_task(
-        workspace: &mut Workspace,
-        task: &NewCenterTask,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        let reveal_strategy: RevealStrategy = task.action.reveal;
-        let mut spawn_task = task.action.clone();
-
-        let is_local = workspace.project().read(cx).is_local();
-
-        if let ControlFlow::Break(_) =
-            TerminalPanel::fill_command(is_local, &task.action, &mut spawn_task)
-        {
-            return;
-        }
-
-        let kind = TerminalKind::Task(spawn_task);
-
-        let project = workspace.project().clone();
-        let database_id = workspace.database_id();
-        cx.spawn(|workspace, mut cx| async move {
-            let terminal = cx
-                .update(|cx| {
-                    let window = cx.window_handle();
-                    project.update(cx, |project, cx| project.create_terminal(kind, window, cx))
-                })?
-                .await?;
-
-            let terminal_view = cx.new_view(|cx| {
-                TerminalView::new(terminal.clone(), workspace.clone(), database_id, cx)
-            })?;
-
-            cx.update(|cx| {
-                let focus_item = match reveal_strategy {
-                    RevealStrategy::Always => true,
-                    RevealStrategy::Never | RevealStrategy::NoFocus => false,
-                };
-
-                workspace.update(cx, |workspace, cx| {
-                    workspace.add_item_to_active_pane(
-                        Box::new(terminal_view),
-                        None,
-                        focus_item,
-                        cx,
-                    );
-                })?;
-
-                anyhow::Ok(())
-            })??;
-
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-    }
-
     ///Create a new Terminal in the current working directory or the user's home directory
     pub fn deploy(
         workspace: &mut Workspace,
@@ -191,38 +133,8 @@ impl TerminalView {
         cx: &mut ViewContext<Workspace>,
     ) {
         let working_directory = default_working_directory(workspace, cx);
-
-        let window = cx.window_handle();
-        let project = workspace.project().downgrade();
-        cx.spawn(move |workspace, mut cx| async move {
-            let terminal = project
-                .update(&mut cx, |project, cx| {
-                    project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
-                })
-                .ok()?
-                .await;
-            let terminal = workspace
-                .update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx))
-                .ok()
-                .flatten()?;
-
-            workspace
-                .update(&mut cx, |workspace, cx| {
-                    let view = cx.new_view(|cx| {
-                        TerminalView::new(
-                            terminal,
-                            workspace.weak_handle(),
-                            workspace.database_id(),
-                            cx,
-                        )
-                    });
-                    workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
-                })
-                .ok();
-
-            Some(())
-        })
-        .detach()
+        TerminalPanel::add_center_terminal(workspace, TerminalKind::Shell(working_directory), cx)
+            .detach_and_log_err(cx);
     }
 
     pub fn new(

crates/workspace/Cargo.toml 🔗

@@ -61,7 +61,6 @@ ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 strum.workspace = true
-zed_actions.workspace = true
 
 [dev-dependencies]
 call = { workspace = true, features = ["test-support"] }

crates/workspace/src/tasks.rs 🔗

@@ -1,8 +1,7 @@
 use project::TaskSourceKind;
 use remote::ConnectionState;
-use task::{NewCenterTask, ResolvedTask, TaskContext, TaskTemplate};
+use task::{ResolvedTask, TaskContext, TaskTemplate};
 use ui::ViewContext;
-use zed_actions::TaskSpawnTarget;
 
 use crate::Workspace;
 
@@ -11,7 +10,6 @@ pub fn schedule_task(
     task_source_kind: TaskSourceKind,
     task_to_resolve: &TaskTemplate,
     task_cx: &TaskContext,
-    task_target: zed_actions::TaskSpawnTarget,
     omit_history: bool,
     cx: &mut ViewContext<'_, Workspace>,
 ) {
@@ -29,7 +27,7 @@ pub fn schedule_task(
     }
 
     if let Some(spawn_in_terminal) =
-        task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_target, task_cx)
+        task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
     {
         schedule_resolved_task(
             workspace,
@@ -48,7 +46,6 @@ pub fn schedule_resolved_task(
     omit_history: bool,
     cx: &mut ViewContext<'_, Workspace>,
 ) {
-    let target = resolved_task.target;
     if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
         if !omit_history {
             resolved_task.resolved = Some(spawn_in_terminal.clone());
@@ -63,17 +60,8 @@ pub fn schedule_resolved_task(
             });
         }
 
-        match target {
-            TaskSpawnTarget::Center => {
-                cx.dispatch_action(Box::new(NewCenterTask {
-                    action: spawn_in_terminal,
-                }));
-            }
-            TaskSpawnTarget::Dock => {
-                cx.emit(crate::Event::SpawnTask {
-                    action: Box::new(spawn_in_terminal),
-                });
-            }
-        }
+        cx.emit(crate::Event::SpawnTask {
+            action: Box::new(spawn_in_terminal),
+        });
     }
 }

crates/zed_actions/src/lib.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{actions, impl_actions};
-use serde::Deserialize;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 
 // If the zed binary doesn't use anything in this crate, it will be optimized away
 // and the actions won't initialize. So we just provide an empty initialization function
@@ -90,33 +91,39 @@ pub struct OpenRecent {
 gpui::impl_actions!(projects, [OpenRecent]);
 gpui::actions!(projects, [OpenRemote]);
 
-#[derive(PartialEq, Eq, Clone, Copy, Deserialize, Default, Debug)]
+/// Where to spawn the task in the UI.
+#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
-pub enum TaskSpawnTarget {
+pub enum RevealTarget {
+    /// In the central pane group, "main" editor area.
     Center,
+    /// In the terminal dock, "regular" terminal items' place.
     #[default]
     Dock,
 }
 
 /// Spawn a task with name or open tasks modal
-#[derive(PartialEq, Clone, Deserialize, Default)]
-pub struct Spawn {
-    #[serde(default)]
-    /// Name of the task to spawn.
-    /// If it is not set, a modal with a list of available tasks is opened instead.
-    /// Defaults to None.
-    pub task_name: Option<String>,
-    /// Which part of the UI the task should be spawned in.
-    /// Defaults to Dock.
-    #[serde(default)]
-    pub target: Option<TaskSpawnTarget>,
+#[derive(Debug, PartialEq, Clone, Deserialize)]
+#[serde(untagged)]
+pub enum Spawn {
+    /// Spawns a task by the name given.
+    ByName {
+        task_name: String,
+        #[serde(default)]
+        reveal_target: Option<RevealTarget>,
+    },
+    /// Spawns a task via modal's selection.
+    ViaModal {
+        /// Selected task's `reveal_target` property override.
+        #[serde(default)]
+        reveal_target: Option<RevealTarget>,
+    },
 }
 
 impl Spawn {
     pub fn modal() -> Self {
-        Self {
-            task_name: None,
-            target: None,
+        Self::ViaModal {
+            reveal_target: None,
         }
     }
 }

docs/src/tasks.md 🔗

@@ -17,9 +17,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
     // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
     "allow_concurrent_runs": false,
     // What to do with the terminal pane and tab, after the command was started:
-    // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
-    // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
-    // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
+    // * `always` — always show the task's pane, and focus the corresponding tab in it (default)
+    // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
+    // * `never` — do not alter focus, but still add/reuse the task's tab in its pane
     "reveal": "always",
     // What to do with the terminal pane and tab, after the command had finished:
     // * `never` — Do nothing when the command finishes (default)