Unify the tasks modal and the new session modal (#31646)

Cole Miller , Julia Ryan , Julia Ryan , Anthony Eid , and Mikayla created

Release Notes:

- Debugger Beta: added a button to the quick action bar to start a debug
session or spawn a task, depending on which of these actions was taken
most recently.
- Debugger Beta: incorporated the tasks modal into the new session modal
as an additional tab.

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Julia Ryan <p1n3appl3@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

assets/keymaps/default-linux.json                 |   7 
assets/keymaps/default-macos.json                 |   8 
crates/debugger_ui/src/debugger_panel.rs          |  31 +
crates/debugger_ui/src/debugger_ui.rs             |  51 +
crates/debugger_ui/src/new_session_modal.rs       | 431 +++++++++-------
crates/debugger_ui/src/tests/new_session_modal.rs |  13 
crates/gpui/src/key_dispatch.rs                   |   2 
crates/tasks_ui/src/modal.rs                      |  33 
crates/tasks_ui/src/tasks_ui.rs                   |  15 
crates/workspace/src/tasks.rs                     |   4 
crates/workspace/src/workspace.rs                 |  24 
crates/zed/src/zed/quick_action_bar.rs            |  41 +
12 files changed, 435 insertions(+), 225 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -1019,5 +1019,12 @@
     "bindings": {
       "enter": "menu::Confirm"
     }
+  },
+  {
+    "context": "RunModal",
+    "bindings": {
+      "ctrl-tab": "pane::ActivateNextItem",
+      "ctrl-shift-tab": "pane::ActivatePreviousItem"
+    }
   }
 ]

assets/keymaps/default-macos.json 🔗

@@ -1109,5 +1109,13 @@
     "bindings": {
       "enter": "menu::Confirm"
     }
+  },
+  {
+    "context": "RunModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-tab": "pane::ActivateNextItem",
+      "ctrl-shift-tab": "pane::ActivatePreviousItem"
+    }
   }
 ]

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
     ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
     FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
     ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
-    ToggleSessionPicker, ToggleThreadPicker, persistence,
+    ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
 };
 use anyhow::{Context as _, Result, anyhow};
 use command_palette_hooks::CommandPaletteFilter;
@@ -65,6 +65,7 @@ pub struct DebugPanel {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
+    debug_scenario_scheduled_last: bool,
     pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
     pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
     fs: Arc<dyn Fs>,
@@ -103,6 +104,7 @@ impl DebugPanel {
                 thread_picker_menu_handle,
                 session_picker_menu_handle,
                 _subscriptions: [focus_subscription],
+                debug_scenario_scheduled_last: true,
             }
         })
     }
@@ -264,6 +266,7 @@ impl DebugPanel {
                 cx,
             )
         });
+        self.debug_scenario_scheduled_last = true;
         if let Some(inventory) = self
             .project
             .read(cx)
@@ -1381,4 +1384,30 @@ impl workspace::DebuggerProvider for DebuggerProvider {
             })
         })
     }
+
+    fn spawn_task_or_modal(
+        &self,
+        workspace: &mut Workspace,
+        action: &tasks_ui::Spawn,
+        window: &mut Window,
+        cx: &mut Context<Workspace>,
+    ) {
+        spawn_task_or_modal(workspace, action, window, cx);
+    }
+
+    fn debug_scenario_scheduled(&self, cx: &mut App) {
+        self.0.update(cx, |this, _| {
+            this.debug_scenario_scheduled_last = true;
+        });
+    }
+
+    fn task_scheduled(&self, cx: &mut App) {
+        self.0.update(cx, |this, _| {
+            this.debug_scenario_scheduled_last = false;
+        })
+    }
+
+    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
+        self.0.read(cx).debug_scenario_scheduled_last
+    }
 }

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -3,11 +3,12 @@ use debugger_panel::{DebugPanel, ToggleFocus};
 use editor::Editor;
 use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
 use gpui::{App, EntityInputHandler, actions};
-use new_session_modal::NewSessionModal;
+use new_session_modal::{NewSessionModal, NewSessionMode};
 use project::debugger::{self, breakpoint_store::SourceBreakpoint};
 use session::DebugSession;
 use settings::Settings;
 use stack_trace_view::StackTraceView;
+use tasks_ui::{Spawn, TaskOverrides};
 use util::maybe;
 use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
 
@@ -62,6 +63,7 @@ pub fn init(cx: &mut App) {
 
         cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
             workspace
+                .register_action(spawn_task_or_modal)
                 .register_action(|workspace, _: &ToggleFocus, window, cx| {
                     workspace.toggle_panel_focus::<DebugPanel>(window, cx);
                 })
@@ -208,7 +210,7 @@ pub fn init(cx: &mut App) {
                     },
                 )
                 .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
-                    NewSessionModal::show(workspace, window, cx);
+                    NewSessionModal::show(workspace, window, NewSessionMode::Launch, None, cx);
                 })
                 .register_action(
                     |workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
@@ -309,3 +311,48 @@ pub fn init(cx: &mut App) {
     })
     .detach();
 }
+
+fn spawn_task_or_modal(
+    workspace: &mut Workspace,
+    action: &Spawn,
+    window: &mut ui::Window,
+    cx: &mut ui::Context<Workspace>,
+) {
+    match action {
+        Spawn::ByName {
+            task_name,
+            reveal_target,
+        } => {
+            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
+                reveal_target: Some(reveal_target),
+            });
+            let name = task_name.clone();
+            tasks_ui::spawn_tasks_filtered(
+                move |(_, task)| task.label.eq(&name),
+                overrides,
+                window,
+                cx,
+            )
+            .detach_and_log_err(cx)
+        }
+        Spawn::ByTag {
+            task_tag,
+            reveal_target,
+        } => {
+            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
+                reveal_target: Some(reveal_target),
+            });
+            let tag = task_tag.clone();
+            tasks_ui::spawn_tasks_filtered(
+                move |(_, task)| task.tags.contains(&tag),
+                overrides,
+                window,
+                cx,
+            )
+            .detach_and_log_err(cx)
+        }
+        Spawn::ViaModal { reveal_target } => {
+            NewSessionModal::show(workspace, window, NewSessionMode::Task, *reveal_target, cx);
+        }
+    }
+}

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -8,6 +8,7 @@ use std::{
     time::Duration,
     usize,
 };
+use tasks_ui::{TaskOverrides, TasksModal};
 
 use dap::{
     DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
@@ -16,12 +17,12 @@ use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
-    Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
+    Focusable, KeyContext, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
 };
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
 use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
 use settings::Settings;
-use task::{DebugScenario, LaunchRequest, ZedDebugConfig};
+use task::{DebugScenario, LaunchRequest, RevealTarget, ZedDebugConfig};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -47,10 +48,11 @@ pub(super) struct NewSessionModal {
     mode: NewSessionMode,
     launch_picker: Entity<Picker<DebugScenarioDelegate>>,
     attach_mode: Entity<AttachMode>,
-    custom_mode: Entity<CustomMode>,
+    configure_mode: Entity<ConfigureMode>,
+    task_mode: TaskMode,
     debugger: Option<DebugAdapterName>,
     save_scenario_state: Option<SaveScenarioState>,
-    _subscriptions: [Subscription; 2],
+    _subscriptions: [Subscription; 3],
 }
 
 fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
@@ -75,6 +77,8 @@ impl NewSessionModal {
     pub(super) fn show(
         workspace: &mut Workspace,
         window: &mut Window,
+        mode: NewSessionMode,
+        reveal_target: Option<RevealTarget>,
         cx: &mut Context<Workspace>,
     ) {
         let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
@@ -84,20 +88,50 @@ impl NewSessionModal {
         let languages = workspace.app_state().languages.clone();
 
         cx.spawn_in(window, async move |workspace, cx| {
+            let task_contexts = workspace
+                .update_in(cx, |workspace, window, cx| {
+                    tasks_ui::task_contexts(workspace, window, cx)
+                })?
+                .await;
+            let task_contexts = Arc::new(task_contexts);
             workspace.update_in(cx, |workspace, window, cx| {
                 let workspace_handle = workspace.weak_handle();
                 workspace.toggle_modal(window, cx, |window, cx| {
                     let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
 
                     let launch_picker = cx.new(|cx| {
-                        Picker::uniform_list(
-                            DebugScenarioDelegate::new(debug_panel.downgrade(), task_store),
-                            window,
-                            cx,
-                        )
-                        .modal(false)
+                        let mut delegate =
+                            DebugScenarioDelegate::new(debug_panel.downgrade(), task_store.clone());
+                        delegate.task_contexts_loaded(task_contexts.clone(), languages, window, cx);
+                        Picker::uniform_list(delegate, window, cx).modal(false)
                     });
 
+                    let configure_mode = ConfigureMode::new(None, window, cx);
+                    if let Some(active_cwd) = task_contexts
+                        .active_context()
+                        .and_then(|context| context.cwd.clone())
+                    {
+                        configure_mode.update(cx, |configure_mode, cx| {
+                            configure_mode.load(active_cwd, window, cx);
+                        });
+                    }
+
+                    let task_overrides = Some(TaskOverrides { reveal_target });
+
+                    let task_mode = TaskMode {
+                        task_modal: cx.new(|cx| {
+                            TasksModal::new(
+                                task_store.clone(),
+                                task_contexts,
+                                task_overrides,
+                                false,
+                                workspace_handle.clone(),
+                                window,
+                                cx,
+                            )
+                        }),
+                    };
+
                     let _subscriptions = [
                         cx.subscribe(&launch_picker, |_, _, _, cx| {
                             cx.emit(DismissEvent);
@@ -108,52 +142,18 @@ impl NewSessionModal {
                                 cx.emit(DismissEvent);
                             },
                         ),
+                        cx.subscribe(&task_mode.task_modal, |_, _, _: &DismissEvent, cx| {
+                            cx.emit(DismissEvent)
+                        }),
                     ];
 
-                    let custom_mode = CustomMode::new(None, window, cx);
-
-                    cx.spawn_in(window, {
-                        let workspace_handle = workspace_handle.clone();
-                        async move |this, cx| {
-                            let task_contexts = workspace_handle
-                                .update_in(cx, |workspace, window, cx| {
-                                    tasks_ui::task_contexts(workspace, window, cx)
-                                })?
-                                .await;
-
-                            this.update_in(cx, |this, window, cx| {
-                                if let Some(active_cwd) = task_contexts
-                                    .active_context()
-                                    .and_then(|context| context.cwd.clone())
-                                {
-                                    this.custom_mode.update(cx, |custom, cx| {
-                                        custom.load(active_cwd, window, cx);
-                                    });
-
-                                    this.debugger = None;
-                                }
-
-                                this.launch_picker.update(cx, |picker, cx| {
-                                    picker.delegate.task_contexts_loaded(
-                                        task_contexts,
-                                        languages,
-                                        window,
-                                        cx,
-                                    );
-                                    picker.refresh(window, cx);
-                                    cx.notify();
-                                });
-                            })
-                        }
-                    })
-                    .detach();
-
                     Self {
                         launch_picker,
                         attach_mode,
-                        custom_mode,
+                        configure_mode,
+                        task_mode,
                         debugger: None,
-                        mode: NewSessionMode::Launch,
+                        mode,
                         debug_panel: debug_panel.downgrade(),
                         workspace: workspace_handle,
                         save_scenario_state: None,
@@ -170,10 +170,17 @@ impl NewSessionModal {
     fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         let dap_menu = self.adapter_drop_down_menu(window, cx);
         match self.mode {
+            NewSessionMode::Task => self
+                .task_mode
+                .task_modal
+                .read(cx)
+                .picker
+                .clone()
+                .into_any_element(),
             NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
                 this.clone().render(window, cx).into_any_element()
             }),
-            NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
+            NewSessionMode::Configure => self.configure_mode.update(cx, |this, cx| {
                 this.clone().render(dap_menu, window, cx).into_any_element()
             }),
             NewSessionMode::Launch => v_flex()
@@ -185,16 +192,17 @@ impl NewSessionModal {
 
     fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
         match self.mode {
+            NewSessionMode::Task => self.task_mode.task_modal.focus_handle(cx),
             NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
-            NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
+            NewSessionMode::Configure => self.configure_mode.read(cx).program.focus_handle(cx),
             NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
         }
     }
 
     fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
         let request = match self.mode {
-            NewSessionMode::Custom => Some(DebugRequest::Launch(
-                self.custom_mode.read(cx).debug_request(cx),
+            NewSessionMode::Configure => Some(DebugRequest::Launch(
+                self.configure_mode.read(cx).debug_request(cx),
             )),
             NewSessionMode::Attach => Some(DebugRequest::Attach(
                 self.attach_mode.read(cx).debug_request(),
@@ -203,8 +211,8 @@ impl NewSessionModal {
         }?;
         let label = suggested_label(&request, debugger);
 
-        let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
-            Some(self.custom_mode.read(cx).stop_on_entry.selected())
+        let stop_on_entry = if let NewSessionMode::Configure = &self.mode {
+            Some(self.configure_mode.read(cx).stop_on_entry.selected())
         } else {
             None
         };
@@ -527,7 +535,8 @@ static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select De
 
 #[derive(Clone)]
 pub(crate) enum NewSessionMode {
-    Custom,
+    Task,
+    Configure,
     Attach,
     Launch,
 }
@@ -535,9 +544,10 @@ pub(crate) enum NewSessionMode {
 impl std::fmt::Display for NewSessionMode {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let mode = match self {
-            NewSessionMode::Launch => "Launch".to_owned(),
-            NewSessionMode::Attach => "Attach".to_owned(),
-            NewSessionMode::Custom => "Custom".to_owned(),
+            NewSessionMode::Task => "Run",
+            NewSessionMode::Launch => "Debug",
+            NewSessionMode::Attach => "Attach",
+            NewSessionMode::Configure => "Configure Debugger",
         };
 
         write!(f, "{}", mode)
@@ -597,36 +607,39 @@ impl Render for NewSessionModal {
         v_flex()
             .size_full()
             .w(rems(34.))
-            .key_context("Pane")
+            .key_context({
+                let mut key_context = KeyContext::new_with_defaults();
+                key_context.add("Pane");
+                key_context.add("RunModal");
+                key_context
+            })
             .elevation_3(cx)
             .bg(cx.theme().colors().elevated_surface_background)
             .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
                 cx.emit(DismissEvent);
             }))
+            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
+                this.mode = match this.mode {
+                    NewSessionMode::Task => NewSessionMode::Launch,
+                    NewSessionMode::Launch => NewSessionMode::Attach,
+                    NewSessionMode::Attach => NewSessionMode::Configure,
+                    NewSessionMode::Configure => NewSessionMode::Task,
+                };
+
+                this.mode_focus_handle(cx).focus(window);
+            }))
             .on_action(
                 cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
                     this.mode = match this.mode {
+                        NewSessionMode::Task => NewSessionMode::Configure,
+                        NewSessionMode::Launch => NewSessionMode::Task,
                         NewSessionMode::Attach => NewSessionMode::Launch,
-                        NewSessionMode::Launch => NewSessionMode::Attach,
-                        _ => {
-                            return;
-                        }
+                        NewSessionMode::Configure => NewSessionMode::Attach,
                     };
 
                     this.mode_focus_handle(cx).focus(window);
                 }),
             )
-            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
-                this.mode = match this.mode {
-                    NewSessionMode::Attach => NewSessionMode::Launch,
-                    NewSessionMode::Launch => NewSessionMode::Attach,
-                    _ => {
-                        return;
-                    }
-                };
-
-                this.mode_focus_handle(cx).focus(window);
-            }))
             .child(
                 h_flex()
                     .w_full()
@@ -637,37 +650,73 @@ impl Render for NewSessionModal {
                             .justify_start()
                             .w_full()
                             .child(
-                                ToggleButton::new("debugger-session-ui-picker-button", "Launch")
-                                    .size(ButtonSize::Default)
-                                    .style(ui::ButtonStyle::Subtle)
-                                    .toggle_state(matches!(self.mode, NewSessionMode::Launch))
-                                    .on_click(cx.listener(|this, _, window, cx| {
-                                        this.mode = NewSessionMode::Launch;
-                                        this.mode_focus_handle(cx).focus(window);
-                                        cx.notify();
-                                    }))
-                                    .first(),
+                                ToggleButton::new(
+                                    "debugger-session-ui-tasks-button",
+                                    NewSessionMode::Task.to_string(),
+                                )
+                                .size(ButtonSize::Default)
+                                .toggle_state(matches!(self.mode, NewSessionMode::Task))
+                                .style(ui::ButtonStyle::Subtle)
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.mode = NewSessionMode::Task;
+                                    this.mode_focus_handle(cx).focus(window);
+                                    cx.notify();
+                                }))
+                                .first(),
                             )
                             .child(
-                                ToggleButton::new("debugger-session-ui-attach-button", "Attach")
-                                    .size(ButtonSize::Default)
-                                    .toggle_state(matches!(self.mode, NewSessionMode::Attach))
-                                    .style(ui::ButtonStyle::Subtle)
-                                    .on_click(cx.listener(|this, _, window, cx| {
-                                        this.mode = NewSessionMode::Attach;
-
-                                        if let Some(debugger) = this.debugger.as_ref() {
-                                            Self::update_attach_picker(
-                                                &this.attach_mode,
-                                                &debugger,
-                                                window,
-                                                cx,
-                                            );
-                                        }
-                                        this.mode_focus_handle(cx).focus(window);
-                                        cx.notify();
-                                    }))
-                                    .last(),
+                                ToggleButton::new(
+                                    "debugger-session-ui-launch-button",
+                                    NewSessionMode::Launch.to_string(),
+                                )
+                                .size(ButtonSize::Default)
+                                .style(ui::ButtonStyle::Subtle)
+                                .toggle_state(matches!(self.mode, NewSessionMode::Launch))
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.mode = NewSessionMode::Launch;
+                                    this.mode_focus_handle(cx).focus(window);
+                                    cx.notify();
+                                }))
+                                .middle(),
+                            )
+                            .child(
+                                ToggleButton::new(
+                                    "debugger-session-ui-attach-button",
+                                    NewSessionMode::Attach.to_string(),
+                                )
+                                .size(ButtonSize::Default)
+                                .toggle_state(matches!(self.mode, NewSessionMode::Attach))
+                                .style(ui::ButtonStyle::Subtle)
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.mode = NewSessionMode::Attach;
+
+                                    if let Some(debugger) = this.debugger.as_ref() {
+                                        Self::update_attach_picker(
+                                            &this.attach_mode,
+                                            &debugger,
+                                            window,
+                                            cx,
+                                        );
+                                    }
+                                    this.mode_focus_handle(cx).focus(window);
+                                    cx.notify();
+                                }))
+                                .middle(),
+                            )
+                            .child(
+                                ToggleButton::new(
+                                    "debugger-session-ui-custom-button",
+                                    NewSessionMode::Configure.to_string(),
+                                )
+                                .size(ButtonSize::Default)
+                                .toggle_state(matches!(self.mode, NewSessionMode::Configure))
+                                .style(ui::ButtonStyle::Subtle)
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.mode = NewSessionMode::Configure;
+                                    this.mode_focus_handle(cx).focus(window);
+                                    cx.notify();
+                                }))
+                                .last(),
                             ),
                     )
                     .justify_between()
@@ -675,83 +724,83 @@ impl Render for NewSessionModal {
                     .border_b_1(),
             )
             .child(v_flex().child(self.render_mode(window, cx)))
-            .child(
-                h_flex()
+            .map(|el| {
+                let container = h_flex()
                     .justify_between()
                     .gap_2()
                     .p_2()
                     .border_color(cx.theme().colors().border_variant)
                     .border_t_1()
-                    .w_full()
-                    .child(match self.mode {
-                        NewSessionMode::Attach => {
-                            div().child(self.adapter_drop_down_menu(window, cx))
-                        }
-                        NewSessionMode::Launch => div().child(
-                            Button::new("new-session-modal-custom", "Custom").on_click({
-                                let this = cx.weak_entity();
-                                move |_, window, cx| {
-                                    this.update(cx, |this, cx| {
-                                        this.mode = NewSessionMode::Custom;
-                                        this.mode_focus_handle(cx).focus(window);
-                                    })
-                                    .ok();
-                                }
-                            }),
-                        ),
-                        NewSessionMode::Custom => h_flex()
+                    .w_full();
+                match self.mode {
+                    NewSessionMode::Configure => el.child(
+                        container
+                            .child(
+                                h_flex()
+                                    .child(
+                                        Button::new(
+                                            "new-session-modal-back",
+                                            "Save to .zed/debug.json...",
+                                        )
+                                        .on_click(cx.listener(|this, _, window, cx| {
+                                            this.save_debug_scenario(window, cx);
+                                        }))
+                                        .disabled(
+                                            self.debugger.is_none()
+                                                || self
+                                                    .configure_mode
+                                                    .read(cx)
+                                                    .program
+                                                    .read(cx)
+                                                    .is_empty(cx)
+                                                || self.save_scenario_state.is_some(),
+                                        ),
+                                    )
+                                    .child(self.render_save_state(cx)),
+                            )
                             .child(
-                                Button::new("new-session-modal-back", "Save to .zed/debug.json...")
+                                Button::new("debugger-spawn", "Start")
                                     .on_click(cx.listener(|this, _, window, cx| {
-                                        this.save_debug_scenario(window, cx);
+                                        this.start_new_session(window, cx)
                                     }))
                                     .disabled(
                                         self.debugger.is_none()
                                             || self
-                                                .custom_mode
+                                                .configure_mode
                                                 .read(cx)
                                                 .program
                                                 .read(cx)
-                                                .is_empty(cx)
-                                            || self.save_scenario_state.is_some(),
+                                                .is_empty(cx),
                                     ),
-                            )
-                            .child(self.render_save_state(cx)),
-                    })
-                    .child(
-                        Button::new("debugger-spawn", "Start")
-                            .on_click(cx.listener(|this, _, window, cx| match &this.mode {
-                                NewSessionMode::Launch => {
-                                    this.launch_picker.update(cx, |picker, cx| {
-                                        picker.delegate.confirm(true, window, cx)
-                                    })
-                                }
-                                _ => this.start_new_session(window, cx),
-                            }))
-                            .disabled(match self.mode {
-                                NewSessionMode::Launch => {
-                                    !self.launch_picker.read(cx).delegate.matches.is_empty()
-                                }
-                                NewSessionMode::Attach => {
-                                    self.debugger.is_none()
-                                        || self
-                                            .attach_mode
-                                            .read(cx)
-                                            .attach_picker
-                                            .read(cx)
-                                            .picker
-                                            .read(cx)
-                                            .delegate
-                                            .match_count()
-                                            == 0
-                                }
-                                NewSessionMode::Custom => {
-                                    self.debugger.is_none()
-                                        || self.custom_mode.read(cx).program.read(cx).is_empty(cx)
-                                }
-                            }),
+                            ),
                     ),
-            )
+                    NewSessionMode::Attach => el.child(
+                        container
+                            .child(div().child(self.adapter_drop_down_menu(window, cx)))
+                            .child(
+                                Button::new("debugger-spawn", "Start")
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        this.start_new_session(window, cx)
+                                    }))
+                                    .disabled(
+                                        self.debugger.is_none()
+                                            || self
+                                                .attach_mode
+                                                .read(cx)
+                                                .attach_picker
+                                                .read(cx)
+                                                .picker
+                                                .read(cx)
+                                                .delegate
+                                                .match_count()
+                                                == 0,
+                                    ),
+                            ),
+                    ),
+                    NewSessionMode::Launch => el,
+                    NewSessionMode::Task => el,
+                }
+            })
     }
 }
 
@@ -774,13 +823,13 @@ impl RenderOnce for AttachMode {
 }
 
 #[derive(Clone)]
-pub(super) struct CustomMode {
+pub(super) struct ConfigureMode {
     program: Entity<Editor>,
     cwd: Entity<Editor>,
     stop_on_entry: ToggleState,
 }
 
-impl CustomMode {
+impl ConfigureMode {
     pub(super) fn new(
         past_launch_config: Option<LaunchRequest>,
         window: &mut Window,
@@ -940,6 +989,11 @@ impl AttachMode {
     }
 }
 
+#[derive(Clone)]
+pub(super) struct TaskMode {
+    pub(super) task_modal: Entity<TasksModal>,
+}
+
 pub(super) struct DebugScenarioDelegate {
     task_store: Entity<TaskStore>,
     candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
@@ -995,12 +1049,12 @@ impl DebugScenarioDelegate {
 
     pub fn task_contexts_loaded(
         &mut self,
-        task_contexts: TaskContexts,
+        task_contexts: Arc<TaskContexts>,
         languages: Arc<LanguageRegistry>,
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) {
-        self.task_contexts = Some(Arc::new(task_contexts));
+        self.task_contexts = Some(task_contexts);
 
         let (recent, scenarios) = self
             .task_store
@@ -1206,7 +1260,7 @@ pub(crate) fn resolve_path(path: &mut String) {
 
 #[cfg(test)]
 impl NewSessionModal {
-    pub(crate) fn set_custom(
+    pub(crate) fn set_configure(
         &mut self,
         program: impl AsRef<str>,
         cwd: impl AsRef<str>,
@@ -1214,21 +1268,21 @@ impl NewSessionModal {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.mode = NewSessionMode::Custom;
+        self.mode = NewSessionMode::Configure;
         self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
 
-        self.custom_mode.update(cx, |custom, cx| {
-            custom.program.update(cx, |editor, cx| {
+        self.configure_mode.update(cx, |configure, cx| {
+            configure.program.update(cx, |editor, cx| {
                 editor.clear(window, cx);
                 editor.set_text(program.as_ref(), window, cx);
             });
 
-            custom.cwd.update(cx, |editor, cx| {
+            configure.cwd.update(cx, |editor, cx| {
                 editor.clear(window, cx);
                 editor.set_text(cwd.as_ref(), window, cx);
             });
 
-            custom.stop_on_entry = match stop_on_entry {
+            configure.stop_on_entry = match stop_on_entry {
                 true => ToggleState::Selected,
                 _ => ToggleState::Unselected,
             }
@@ -1239,28 +1293,3 @@ impl NewSessionModal {
         self.save_debug_scenario(window, cx);
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use paths::home_dir;
-
-    #[test]
-    fn test_normalize_paths() {
-        let sep = std::path::MAIN_SEPARATOR;
-        let home = home_dir().to_string_lossy().to_string();
-        let resolve_path = |path: &str| -> String {
-            let mut path = path.to_string();
-            super::resolve_path(&mut path);
-            path
-        };
-
-        assert_eq!(resolve_path("bin"), format!("bin"));
-        assert_eq!(resolve_path(&format!("{sep}foo")), format!("{sep}foo"));
-        assert_eq!(resolve_path(""), format!(""));
-        assert_eq!(
-            resolve_path(&format!("~{sep}blah")),
-            format!("{home}{sep}blah")
-        );
-        assert_eq!(resolve_path("~"), home);
-    }
-}

crates/debugger_ui/src/tests/new_session_modal.rs 🔗

@@ -7,6 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
 use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
 use util::path;
 
+use crate::new_session_modal::NewSessionMode;
 use crate::tests::{init_test, init_test_workspace};
 
 #[gpui::test]
@@ -170,7 +171,13 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
 
     workspace
         .update(cx, |workspace, window, cx| {
-            crate::new_session_modal::NewSessionModal::show(workspace, window, cx);
+            crate::new_session_modal::NewSessionModal::show(
+                workspace,
+                window,
+                NewSessionMode::Launch,
+                None,
+                cx,
+            );
         })
         .unwrap();
 
@@ -184,7 +191,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
         .expect("Modal should be active");
 
     modal.update_in(cx, |modal, window, cx| {
-        modal.set_custom("/project/main", "/project", false, window, cx);
+        modal.set_configure("/project/main", "/project", false, window, cx);
         modal.save_scenario(window, cx);
     });
 
@@ -213,7 +220,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
     pretty_assertions::assert_eq!(expected_content, actual_lines);
 
     modal.update_in(cx, |modal, window, cx| {
-        modal.set_custom("/project/other", "/project", true, window, cx);
+        modal.set_configure("/project/other", "/project", true, window, cx);
         modal.save_scenario(window, cx);
     });
 

crates/gpui/src/key_dispatch.rs 🔗

@@ -27,7 +27,7 @@
 ///
 /// The keybindings themselves are managed independently by calling cx.bind_keys().
 /// (Though mostly when developing Zed itself, you just need to add a new line to
-///  assets/keymaps/default.json).
+///  assets/keymaps/default-{platform}.json).
 ///
 /// ```rust
 /// cx.bind_keys([

crates/tasks_ui/src/modal.rs 🔗

@@ -23,7 +23,7 @@ use workspace::{ModalView, Workspace};
 pub use zed_actions::{Rerun, Spawn};
 
 /// A modal used to spawn new tasks.
-pub(crate) struct TasksModalDelegate {
+pub struct TasksModalDelegate {
     task_store: Entity<TaskStore>,
     candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
     task_overrides: Option<TaskOverrides>,
@@ -33,21 +33,21 @@ pub(crate) struct TasksModalDelegate {
     selected_index: usize,
     workspace: WeakEntity<Workspace>,
     prompt: String,
-    task_contexts: TaskContexts,
+    task_contexts: Arc<TaskContexts>,
     placeholder_text: Arc<str>,
 }
 
 /// Task template amendments to do before resolving the context.
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub(crate) struct TaskOverrides {
+pub struct TaskOverrides {
     /// See [`RevealTarget`].
-    pub(crate) reveal_target: Option<RevealTarget>,
+    pub reveal_target: Option<RevealTarget>,
 }
 
 impl TasksModalDelegate {
     fn new(
         task_store: Entity<TaskStore>,
-        task_contexts: TaskContexts,
+        task_contexts: Arc<TaskContexts>,
         task_overrides: Option<TaskOverrides>,
         workspace: WeakEntity<Workspace>,
     ) -> Self {
@@ -123,15 +123,16 @@ impl TasksModalDelegate {
 }
 
 pub struct TasksModal {
-    picker: Entity<Picker<TasksModalDelegate>>,
+    pub picker: Entity<Picker<TasksModalDelegate>>,
     _subscription: [Subscription; 2],
 }
 
 impl TasksModal {
-    pub(crate) fn new(
+    pub fn new(
         task_store: Entity<TaskStore>,
-        task_contexts: TaskContexts,
+        task_contexts: Arc<TaskContexts>,
         task_overrides: Option<TaskOverrides>,
+        is_modal: bool,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -142,6 +143,7 @@ impl TasksModal {
                 window,
                 cx,
             )
+            .modal(is_modal)
         });
         let _subscription = [
             cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
@@ -158,6 +160,20 @@ impl TasksModal {
             _subscription,
         }
     }
+
+    pub fn task_contexts_loaded(
+        &mut self,
+        task_contexts: Arc<TaskContexts>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.picker.update(cx, |picker, cx| {
+            picker.delegate.task_contexts = task_contexts;
+            picker.delegate.candidates = None;
+            picker.refresh(window, cx);
+            cx.notify();
+        })
+    }
 }
 
 impl Render for TasksModal {
@@ -568,6 +584,7 @@ impl PickerDelegate for TasksModalDelegate {
             Vec::new()
         }
     }
+
     fn render_footer(
         &self,
         window: &mut Window,

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -1,16 +1,15 @@
-use std::path::Path;
+use std::{path::Path, sync::Arc};
 
 use collections::HashMap;
 use editor::Editor;
 use gpui::{App, AppContext as _, Context, Entity, Task, Window};
-use modal::TaskOverrides;
 use project::{Location, TaskContexts, TaskSourceKind, Worktree};
 use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName};
 use workspace::Workspace;
 
 mod modal;
 
-pub use modal::{Rerun, ShowAttachModal, Spawn, TasksModal};
+pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal};
 
 pub fn init(cx: &mut App) {
     cx.observe_new(
@@ -95,6 +94,11 @@ fn spawn_task_or_modal(
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
+    if let Some(provider) = workspace.debugger_provider() {
+        provider.spawn_task_or_modal(workspace, action, window, cx);
+        return;
+    }
+
     match action {
         Spawn::ByName {
             task_name,
@@ -143,7 +147,7 @@ pub fn toggle_modal(
     if can_open_modal {
         let task_contexts = task_contexts(workspace, window, cx);
         cx.spawn_in(window, async move |workspace, cx| {
-            let task_contexts = task_contexts.await;
+            let task_contexts = Arc::new(task_contexts.await);
             workspace
                 .update_in(cx, |workspace, window, cx| {
                     workspace.toggle_modal(window, cx, |window, cx| {
@@ -153,6 +157,7 @@ pub fn toggle_modal(
                             reveal_target.map(|target| TaskOverrides {
                                 reveal_target: Some(target),
                             }),
+                            true,
                             workspace_handle,
                             window,
                             cx,
@@ -166,7 +171,7 @@ pub fn toggle_modal(
     }
 }
 
-fn spawn_tasks_filtered<F>(
+pub fn spawn_tasks_filtered<F>(
     mut predicate: F,
     overrides: Option<TaskOverrides>,
     window: &mut Window,

crates/workspace/src/tasks.rs 🔗

@@ -56,6 +56,10 @@ impl Workspace {
     ) {
         let spawn_in_terminal = resolved_task.resolved.clone();
         if !omit_history {
+            if let Some(debugger_provider) = self.debugger_provider.as_ref() {
+                debugger_provider.task_scheduled(cx);
+            }
+
             self.project().update(cx, |project, cx| {
                 if let Some(task_inventory) =
                     project.task_store().read(cx).task_inventory().cloned()

crates/workspace/src/workspace.rs 🔗

@@ -100,13 +100,13 @@ use task::{DebugScenario, SpawnInTerminal, TaskContext};
 use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
-use ui::prelude::*;
+use ui::{Window, prelude::*};
 use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
 use uuid::Uuid;
 pub use workspace_settings::{
     AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
 };
-use zed_actions::feedback::FileBugReport;
+use zed_actions::{Spawn, feedback::FileBugReport};
 
 use crate::notifications::NotificationId;
 use crate::persistence::{
@@ -149,6 +149,18 @@ pub trait DebuggerProvider {
         window: &mut Window,
         cx: &mut App,
     );
+
+    fn spawn_task_or_modal(
+        &self,
+        workspace: &mut Workspace,
+        action: &Spawn,
+        window: &mut Window,
+        cx: &mut Context<Workspace>,
+    );
+
+    fn task_scheduled(&self, cx: &mut App);
+    fn debug_scenario_scheduled(&self, cx: &mut App);
+    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
 }
 
 actions!(
@@ -947,7 +959,7 @@ pub struct Workspace {
     on_prompt_for_new_path: Option<PromptForNewPath>,
     on_prompt_for_open_path: Option<PromptForOpenPath>,
     terminal_provider: Option<Box<dyn TerminalProvider>>,
-    debugger_provider: Option<Box<dyn DebuggerProvider>>,
+    debugger_provider: Option<Arc<dyn DebuggerProvider>>,
     serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
     serialized_ssh_project: Option<SerializedSshProject>,
     _items_serializer: Task<Result<()>>,
@@ -1828,7 +1840,11 @@ impl Workspace {
     }
 
     pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
-        self.debugger_provider = Some(Box::new(provider));
+        self.debugger_provider = Some(Arc::new(provider));
+    }
+
+    pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
+        self.debugger_provider.clone()
     }
 
     pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {

crates/zed/src/zed/quick_action_bar.rs 🔗

@@ -133,6 +133,46 @@ impl Render for QuickActionBar {
             )
         });
 
+        let last_run_debug = self
+            .workspace
+            .read_with(cx, |workspace, cx| {
+                workspace
+                    .debugger_provider()
+                    .map(|provider| provider.debug_scenario_scheduled_last(cx))
+                    .unwrap_or_default()
+            })
+            .ok()
+            .unwrap_or_default();
+
+        let run_button = if last_run_debug {
+            QuickActionBarButton::new(
+                "debug",
+                IconName::Debug, // TODO: use debug + play icon
+                false,
+                Box::new(debugger_ui::Start),
+                focus_handle.clone(),
+                "Debug",
+                move |_, window, cx| {
+                    window.dispatch_action(Box::new(debugger_ui::Start), cx);
+                },
+            )
+        } else {
+            let action = Box::new(tasks_ui::Spawn::ViaModal {
+                reveal_target: None,
+            });
+            QuickActionBarButton::new(
+                "run",
+                IconName::Play,
+                false,
+                action.boxed_clone(),
+                focus_handle.clone(),
+                "Spawn Task",
+                move |_, window, cx| {
+                    window.dispatch_action(action.boxed_clone(), cx);
+                },
+            )
+        };
+
         let assistant_button = QuickActionBarButton::new(
             "toggle inline assistant",
             IconName::ZedAssistant,
@@ -561,6 +601,7 @@ impl Render for QuickActionBar {
                 AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
                 |bar| bar.child(assistant_button),
             )
+            .child(run_button)
             .children(code_actions_dropdown)
             .children(editor_selections_dropdown)
             .child(editor_settings_dropdown)