debugger_panel.rs

  1use crate::{
  2    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
  3    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
  4};
  5use crate::{new_session_modal::NewSessionModal, session::DebugSession};
  6use anyhow::{Result, anyhow};
  7use collections::HashMap;
  8use command_palette_hooks::CommandPaletteFilter;
  9use dap::{
 10    ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
 11    client::SessionId, debugger_settings::DebuggerSettings,
 12};
 13use futures::{SinkExt as _, channel::mpsc};
 14use gpui::{
 15    Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle,
 16    Focusable, Subscription, Task, WeakEntity, actions,
 17};
 18use project::{
 19    Project,
 20    debugger::{
 21        dap_store::{self, DapStore},
 22        session::ThreadStatus,
 23    },
 24    terminals::TerminalKind,
 25};
 26use rpc::proto::{self};
 27use settings::Settings;
 28use std::{any::TypeId, path::PathBuf};
 29use task::DebugTaskDefinition;
 30use terminal_view::terminal_panel::TerminalPanel;
 31use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 32use workspace::{
 33    Workspace,
 34    dock::{DockPosition, Panel, PanelEvent},
 35};
 36
 37pub enum DebugPanelEvent {
 38    Exited(SessionId),
 39    Terminated(SessionId),
 40    Stopped {
 41        client_id: SessionId,
 42        event: StoppedEvent,
 43        go_to_stack_frame: bool,
 44    },
 45    Thread((SessionId, ThreadEvent)),
 46    Continued((SessionId, ContinuedEvent)),
 47    Output((SessionId, OutputEvent)),
 48    Module((SessionId, ModuleEvent)),
 49    LoadedSource((SessionId, LoadedSourceEvent)),
 50    ClientShutdown(SessionId),
 51    CapabilitiesChanged(SessionId),
 52}
 53
 54actions!(debug_panel, [ToggleFocus]);
 55pub struct DebugPanel {
 56    size: Pixels,
 57    sessions: Vec<Entity<DebugSession>>,
 58    active_session: Option<Entity<DebugSession>>,
 59    /// This represents the last debug definition that was created in the new session modal
 60    pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
 61    project: WeakEntity<Project>,
 62    workspace: WeakEntity<Workspace>,
 63    focus_handle: FocusHandle,
 64    _subscriptions: Vec<Subscription>,
 65}
 66
 67impl DebugPanel {
 68    pub fn new(
 69        workspace: &Workspace,
 70        window: &mut Window,
 71        cx: &mut Context<Workspace>,
 72    ) -> Entity<Self> {
 73        cx.new(|cx| {
 74            let project = workspace.project().clone();
 75            let dap_store = project.read(cx).dap_store();
 76
 77            let _subscriptions =
 78                vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
 79
 80            let debug_panel = Self {
 81                size: px(300.),
 82                sessions: vec![],
 83                active_session: None,
 84                _subscriptions,
 85                past_debug_definition: None,
 86                focus_handle: cx.focus_handle(),
 87                project: project.downgrade(),
 88                workspace: workspace.weak_handle(),
 89            };
 90
 91            debug_panel
 92        })
 93    }
 94
 95    pub fn load(
 96        workspace: WeakEntity<Workspace>,
 97        cx: AsyncWindowContext,
 98    ) -> Task<Result<Entity<Self>>> {
 99        cx.spawn(async move |cx| {
100            workspace.update_in(cx, |workspace, window, cx| {
101                let debug_panel = DebugPanel::new(workspace, window, cx);
102
103                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
104                    workspace.project().read(cx).breakpoint_store().update(
105                        cx,
106                        |breakpoint_store, cx| {
107                            breakpoint_store.clear_breakpoints(cx);
108                        },
109                    )
110                });
111
112                cx.observe(&debug_panel, |_, debug_panel, cx| {
113                    let (has_active_session, supports_restart, support_step_back) = debug_panel
114                        .update(cx, |this, cx| {
115                            this.active_session()
116                                .map(|item| {
117                                    let running = item.read(cx).mode().as_running().cloned();
118
119                                    match running {
120                                        Some(running) => {
121                                            let caps = running.read(cx).capabilities(cx);
122                                            (
123                                                true,
124                                                caps.supports_restart_request.unwrap_or_default(),
125                                                caps.supports_step_back.unwrap_or_default(),
126                                            )
127                                        }
128                                        None => (false, false, false),
129                                    }
130                                })
131                                .unwrap_or((false, false, false))
132                        });
133
134                    let filter = CommandPaletteFilter::global_mut(cx);
135                    let debugger_action_types = [
136                        TypeId::of::<Continue>(),
137                        TypeId::of::<StepOver>(),
138                        TypeId::of::<StepInto>(),
139                        TypeId::of::<StepOut>(),
140                        TypeId::of::<Stop>(),
141                        TypeId::of::<Disconnect>(),
142                        TypeId::of::<Pause>(),
143                        TypeId::of::<ToggleIgnoreBreakpoints>(),
144                    ];
145
146                    let step_back_action_type = [TypeId::of::<StepBack>()];
147                    let restart_action_type = [TypeId::of::<Restart>()];
148
149                    if has_active_session {
150                        filter.show_action_types(debugger_action_types.iter());
151
152                        if supports_restart {
153                            filter.show_action_types(restart_action_type.iter());
154                        } else {
155                            filter.hide_action_types(&restart_action_type);
156                        }
157
158                        if support_step_back {
159                            filter.show_action_types(step_back_action_type.iter());
160                        } else {
161                            filter.hide_action_types(&step_back_action_type);
162                        }
163                    } else {
164                        // show only the `debug: start`
165                        filter.hide_action_types(&debugger_action_types);
166                        filter.hide_action_types(&step_back_action_type);
167                        filter.hide_action_types(&restart_action_type);
168                    }
169                })
170                .detach();
171
172                debug_panel
173            })
174        })
175    }
176
177    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
178        self.active_session.clone()
179    }
180
181    pub fn debug_panel_items_by_client(
182        &self,
183        client_id: &SessionId,
184        cx: &Context<Self>,
185    ) -> Vec<Entity<DebugSession>> {
186        self.sessions
187            .iter()
188            .filter(|item| item.read(cx).session_id(cx) == *client_id)
189            .map(|item| item.clone())
190            .collect()
191    }
192
193    pub fn debug_panel_item_by_client(
194        &self,
195        client_id: SessionId,
196        cx: &mut Context<Self>,
197    ) -> Option<Entity<DebugSession>> {
198        self.sessions
199            .iter()
200            .find(|item| {
201                let item = item.read(cx);
202
203                item.session_id(cx) == client_id
204            })
205            .cloned()
206    }
207
208    fn handle_dap_store_event(
209        &mut self,
210        dap_store: &Entity<DapStore>,
211        event: &dap_store::DapStoreEvent,
212        window: &mut Window,
213        cx: &mut Context<Self>,
214    ) {
215        match event {
216            dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
217                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
218                    return log::error!(
219                        "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
220                    );
221                };
222
223                let Some(project) = self.project.upgrade() else {
224                    return log::error!("Debug Panel out lived it's weak reference to Project");
225                };
226
227                if self
228                    .sessions
229                    .iter()
230                    .any(|item| item.read(cx).session_id(cx) == *session_id)
231                {
232                    // We already have an item for this session.
233                    return;
234                }
235                let session_item = DebugSession::running(
236                    project,
237                    self.workspace.clone(),
238                    session,
239                    cx.weak_entity(),
240                    window,
241                    cx,
242                );
243
244                self.sessions.push(session_item.clone());
245                self.activate_session(session_item, window, cx);
246            }
247            dap_store::DapStoreEvent::RunInTerminal {
248                title,
249                cwd,
250                command,
251                args,
252                envs,
253                sender,
254                ..
255            } => {
256                self.handle_run_in_terminal_request(
257                    title.clone(),
258                    cwd.clone(),
259                    command.clone(),
260                    args.clone(),
261                    envs.clone(),
262                    sender.clone(),
263                    window,
264                    cx,
265                )
266                .detach_and_log_err(cx);
267            }
268            _ => {}
269        }
270    }
271
272    fn handle_run_in_terminal_request(
273        &self,
274        title: Option<String>,
275        cwd: PathBuf,
276        command: Option<String>,
277        args: Vec<String>,
278        envs: HashMap<String, String>,
279        mut sender: mpsc::Sender<Result<u32>>,
280        window: &mut Window,
281        cx: &mut App,
282    ) -> Task<Result<()>> {
283        let terminal_task = self.workspace.update(cx, |workspace, cx| {
284            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
285                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
286            });
287
288            let terminal_panel = match terminal_panel {
289                Ok(panel) => panel,
290                Err(err) => return Task::ready(Err(err)),
291            };
292
293            terminal_panel.update(cx, |terminal_panel, cx| {
294                let terminal_task = terminal_panel.add_terminal(
295                    TerminalKind::Debug {
296                        command,
297                        args,
298                        envs,
299                        cwd,
300                        title,
301                    },
302                    task::RevealStrategy::Always,
303                    window,
304                    cx,
305                );
306
307                cx.spawn(async move |_, cx| {
308                    let pid_task = async move {
309                        let terminal = terminal_task.await?;
310
311                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
312                    };
313
314                    pid_task.await
315                })
316            })
317        });
318
319        cx.background_spawn(async move {
320            match terminal_task {
321                Ok(pid_task) => match pid_task.await {
322                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
323                    Ok(None) => {
324                        sender
325                            .send(Err(anyhow!(
326                                "Terminal was spawned but PID was not available"
327                            )))
328                            .await?
329                    }
330                    Err(error) => sender.send(Err(anyhow!(error))).await?,
331                },
332                Err(error) => sender.send(Err(anyhow!(error))).await?,
333            };
334
335            Ok(())
336        })
337    }
338
339    fn close_session(&mut self, entity_id: EntityId, cx: &mut Context<Self>) {
340        let Some(session) = self
341            .sessions
342            .iter()
343            .find(|other| entity_id == other.entity_id())
344        else {
345            return;
346        };
347
348        session.update(cx, |session, cx| session.shutdown(cx));
349
350        self.sessions.retain(|other| entity_id != other.entity_id());
351
352        if let Some(active_session_id) = self
353            .active_session
354            .as_ref()
355            .map(|session| session.entity_id())
356        {
357            if active_session_id == entity_id {
358                self.active_session = self.sessions.first().cloned();
359            }
360        }
361    }
362
363    fn sessions_drop_down_menu(
364        &self,
365        active_session: &Entity<DebugSession>,
366        window: &mut Window,
367        cx: &mut Context<Self>,
368    ) -> DropdownMenu {
369        let sessions = self.sessions.clone();
370        let weak = cx.weak_entity();
371        let label = active_session.read(cx).label_element(cx);
372
373        DropdownMenu::new_with_element(
374            "debugger-session-list",
375            label,
376            ContextMenu::build(window, cx, move |mut this, _, _| {
377                for session in sessions.into_iter() {
378                    let weak_session = session.downgrade();
379                    let weak_id = weak_session.entity_id();
380
381                    this = this.custom_entry(
382                        {
383                            let weak = weak.clone();
384                            move |_, cx| {
385                                weak_session
386                                    .read_with(cx, |session, cx| {
387                                        h_flex()
388                                            .w_full()
389                                            .justify_between()
390                                            .child(session.label_element(cx))
391                                            .child(
392                                                IconButton::new(
393                                                    "close-debug-session",
394                                                    IconName::Close,
395                                                )
396                                                .icon_size(IconSize::Small)
397                                                .on_click({
398                                                    let weak = weak.clone();
399                                                    move |_, _, cx| {
400                                                        weak.update(cx, |panel, cx| {
401                                                            panel.close_session(weak_id, cx);
402                                                        })
403                                                        .ok();
404                                                    }
405                                                }),
406                                            )
407                                            .into_any_element()
408                                    })
409                                    .unwrap_or_else(|_| div().into_any_element())
410                            }
411                        },
412                        {
413                            let weak = weak.clone();
414                            move |window, cx| {
415                                weak.update(cx, |panel, cx| {
416                                    panel.activate_session(session.clone(), window, cx);
417                                })
418                                .ok();
419                            }
420                        },
421                    );
422                }
423                this
424            }),
425        )
426    }
427
428    fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
429        let active_session = self.active_session.clone();
430
431        Some(
432            h_flex()
433                .border_b_1()
434                .border_color(cx.theme().colors().border)
435                .p_1()
436                .justify_between()
437                .w_full()
438                .child(
439                    h_flex().gap_2().w_full().when_some(
440                        active_session
441                            .as_ref()
442                            .and_then(|session| session.read(cx).mode().as_running()),
443                        |this, running_session| {
444                            let thread_status = running_session
445                                .read(cx)
446                                .thread_status(cx)
447                                .unwrap_or(project::debugger::session::ThreadStatus::Exited);
448                            let capabilities = running_session.read(cx).capabilities(cx);
449                            this.map(|this| {
450                                if thread_status == ThreadStatus::Running {
451                                    this.child(
452                                        IconButton::new("debug-pause", IconName::DebugPause)
453                                            .icon_size(IconSize::XSmall)
454                                            .shape(ui::IconButtonShape::Square)
455                                            .on_click(window.listener_for(
456                                                &running_session,
457                                                |this, _, _window, cx| {
458                                                    this.pause_thread(cx);
459                                                },
460                                            ))
461                                            .tooltip(move |window, cx| {
462                                                Tooltip::text("Pause program")(window, cx)
463                                            }),
464                                    )
465                                } else {
466                                    this.child(
467                                        IconButton::new("debug-continue", IconName::DebugContinue)
468                                            .icon_size(IconSize::XSmall)
469                                            .shape(ui::IconButtonShape::Square)
470                                            .on_click(window.listener_for(
471                                                &running_session,
472                                                |this, _, _window, cx| this.continue_thread(cx),
473                                            ))
474                                            .disabled(thread_status != ThreadStatus::Stopped)
475                                            .tooltip(move |window, cx| {
476                                                Tooltip::text("Continue program")(window, cx)
477                                            }),
478                                    )
479                                }
480                            })
481                            .child(
482                                IconButton::new("debug-step-over", IconName::ArrowRight)
483                                    .icon_size(IconSize::XSmall)
484                                    .shape(ui::IconButtonShape::Square)
485                                    .on_click(window.listener_for(
486                                        &running_session,
487                                        |this, _, _window, cx| {
488                                            this.step_over(cx);
489                                        },
490                                    ))
491                                    .disabled(thread_status != ThreadStatus::Stopped)
492                                    .tooltip(move |window, cx| {
493                                        Tooltip::text("Step over")(window, cx)
494                                    }),
495                            )
496                            .child(
497                                IconButton::new("debug-step-out", IconName::ArrowUpRight)
498                                    .icon_size(IconSize::XSmall)
499                                    .shape(ui::IconButtonShape::Square)
500                                    .on_click(window.listener_for(
501                                        &running_session,
502                                        |this, _, _window, cx| {
503                                            this.step_out(cx);
504                                        },
505                                    ))
506                                    .disabled(thread_status != ThreadStatus::Stopped)
507                                    .tooltip(move |window, cx| {
508                                        Tooltip::text("Step out")(window, cx)
509                                    }),
510                            )
511                            .child(
512                                IconButton::new("debug-step-into", IconName::ArrowDownRight)
513                                    .icon_size(IconSize::XSmall)
514                                    .shape(ui::IconButtonShape::Square)
515                                    .on_click(window.listener_for(
516                                        &running_session,
517                                        |this, _, _window, cx| {
518                                            this.step_in(cx);
519                                        },
520                                    ))
521                                    .disabled(thread_status != ThreadStatus::Stopped)
522                                    .tooltip(move |window, cx| {
523                                        Tooltip::text("Step in")(window, cx)
524                                    }),
525                            )
526                            .child(Divider::vertical())
527                            .child(
528                                IconButton::new(
529                                    "debug-enable-breakpoint",
530                                    IconName::DebugDisabledBreakpoint,
531                                )
532                                .icon_size(IconSize::XSmall)
533                                .shape(ui::IconButtonShape::Square)
534                                .disabled(thread_status != ThreadStatus::Stopped),
535                            )
536                            .child(
537                                IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
538                                    .icon_size(IconSize::XSmall)
539                                    .shape(ui::IconButtonShape::Square)
540                                    .disabled(thread_status != ThreadStatus::Stopped),
541                            )
542                            .child(
543                                IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
544                                    .icon_size(IconSize::XSmall)
545                                    .shape(ui::IconButtonShape::Square)
546                                    .disabled(
547                                        thread_status == ThreadStatus::Exited
548                                            || thread_status == ThreadStatus::Ended,
549                                    )
550                                    .on_click(window.listener_for(
551                                        &running_session,
552                                        |this, _, _window, cx| {
553                                            this.toggle_ignore_breakpoints(cx);
554                                        },
555                                    ))
556                                    .tooltip(move |window, cx| {
557                                        Tooltip::text("Disable all breakpoints")(window, cx)
558                                    }),
559                            )
560                            .child(Divider::vertical())
561                            .child(
562                                IconButton::new("debug-restart", IconName::DebugRestart)
563                                    .icon_size(IconSize::XSmall)
564                                    .on_click(window.listener_for(
565                                        &running_session,
566                                        |this, _, _window, cx| {
567                                            this.restart_session(cx);
568                                        },
569                                    ))
570                                    .disabled(
571                                        !capabilities.supports_restart_request.unwrap_or_default(),
572                                    )
573                                    .tooltip(move |window, cx| {
574                                        Tooltip::text("Restart")(window, cx)
575                                    }),
576                            )
577                            .child(
578                                IconButton::new("debug-stop", IconName::Power)
579                                    .icon_size(IconSize::XSmall)
580                                    .on_click(window.listener_for(
581                                        &running_session,
582                                        |this, _, _window, cx| {
583                                            this.stop_thread(cx);
584                                        },
585                                    ))
586                                    .disabled(
587                                        thread_status != ThreadStatus::Stopped
588                                            && thread_status != ThreadStatus::Running,
589                                    )
590                                    .tooltip({
591                                        let label = if capabilities
592                                            .supports_terminate_threads_request
593                                            .unwrap_or_default()
594                                        {
595                                            "Terminate Thread"
596                                        } else {
597                                            "Terminate all Threads"
598                                        };
599                                        move |window, cx| Tooltip::text(label)(window, cx)
600                                    }),
601                            )
602                        },
603                    ),
604                )
605                .child(
606                    h_flex()
607                        .gap_2()
608                        .when_some(
609                            active_session
610                                .as_ref()
611                                .and_then(|session| session.read(cx).mode().as_running())
612                                .cloned(),
613                            |this, session| {
614                                this.child(
615                                    session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
616                                )
617                                .child(Divider::vertical())
618                            },
619                        )
620                        .when_some(active_session.as_ref(), |this, session| {
621                            let context_menu = self.sessions_drop_down_menu(session, window, cx);
622                            this.child(context_menu).child(Divider::vertical())
623                        })
624                        .child(
625                            IconButton::new("debug-new-session", IconName::Plus)
626                                .icon_size(IconSize::Small)
627                                .on_click({
628                                    let workspace = self.workspace.clone();
629                                    let weak_panel = cx.weak_entity();
630                                    let past_debug_definition = self.past_debug_definition.clone();
631                                    move |_, window, cx| {
632                                        let weak_panel = weak_panel.clone();
633                                        let past_debug_definition = past_debug_definition.clone();
634
635                                        let _ = workspace.update(cx, |this, cx| {
636                                            let workspace = cx.weak_entity();
637                                            this.toggle_modal(window, cx, |window, cx| {
638                                                NewSessionModal::new(
639                                                    past_debug_definition,
640                                                    weak_panel,
641                                                    workspace,
642                                                    window,
643                                                    cx,
644                                                )
645                                            });
646                                        });
647                                    }
648                                })
649                                .tooltip(|window, cx| {
650                                    Tooltip::for_action(
651                                        "New Debug Session",
652                                        &CreateDebuggingSession,
653                                        window,
654                                        cx,
655                                    )
656                                }),
657                        ),
658                ),
659        )
660    }
661
662    fn activate_session(
663        &mut self,
664        session_item: Entity<DebugSession>,
665        window: &mut Window,
666        cx: &mut Context<Self>,
667    ) {
668        debug_assert!(self.sessions.contains(&session_item));
669        session_item.focus_handle(cx).focus(window);
670        session_item.update(cx, |this, cx| {
671            if let Some(running) = this.mode().as_running() {
672                running.update(cx, |this, cx| {
673                    this.go_to_selected_stack_frame(window, cx);
674                });
675            }
676        });
677        self.active_session = Some(session_item);
678        cx.notify();
679    }
680}
681
682impl EventEmitter<PanelEvent> for DebugPanel {}
683impl EventEmitter<DebugPanelEvent> for DebugPanel {}
684impl EventEmitter<project::Event> for DebugPanel {}
685
686impl Focusable for DebugPanel {
687    fn focus_handle(&self, _: &App) -> FocusHandle {
688        self.focus_handle.clone()
689    }
690}
691
692impl Panel for DebugPanel {
693    fn persistent_name() -> &'static str {
694        "DebugPanel"
695    }
696
697    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
698        DockPosition::Bottom
699    }
700
701    fn position_is_valid(&self, position: DockPosition) -> bool {
702        position == DockPosition::Bottom
703    }
704
705    fn set_position(
706        &mut self,
707        _position: DockPosition,
708        _window: &mut Window,
709        _cx: &mut Context<Self>,
710    ) {
711    }
712
713    fn size(&self, _window: &Window, _: &App) -> Pixels {
714        self.size
715    }
716
717    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
718        self.size = size.unwrap();
719    }
720
721    fn remote_id() -> Option<proto::PanelId> {
722        Some(proto::PanelId::DebugPanel)
723    }
724
725    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
726        Some(IconName::Debug)
727    }
728
729    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
730        if DebuggerSettings::get_global(cx).button {
731            Some("Debug Panel")
732        } else {
733            None
734        }
735    }
736
737    fn toggle_action(&self) -> Box<dyn Action> {
738        Box::new(ToggleFocus)
739    }
740
741    fn activation_priority(&self) -> u32 {
742        9
743    }
744    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
745}
746
747impl Render for DebugPanel {
748    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
749        let has_sessions = self.sessions.len() > 0;
750        debug_assert_eq!(has_sessions, self.active_session.is_some());
751
752        v_flex()
753            .size_full()
754            .key_context("DebugPanel")
755            .child(h_flex().children(self.top_controls_strip(window, cx)))
756            .track_focus(&self.focus_handle(cx))
757            .map(|this| {
758                if has_sessions {
759                    this.children(self.active_session.clone())
760                } else {
761                    this.child(
762                        v_flex()
763                            .h_full()
764                            .gap_1()
765                            .items_center()
766                            .justify_center()
767                            .child(
768                                h_flex().child(
769                                    Label::new("No Debugging Sessions")
770                                        .size(LabelSize::Small)
771                                        .color(Color::Muted),
772                                ),
773                            )
774                            .child(
775                                h_flex().flex_shrink().child(
776                                    Button::new("spawn-new-session-empty-state", "New Session")
777                                        .size(ButtonSize::Large)
778                                        .on_click(|_, window, cx| {
779                                            window.dispatch_action(
780                                                CreateDebuggingSession.boxed_clone(),
781                                                cx,
782                                            );
783                                        }),
784                                ),
785                            ),
786                    )
787                }
788            })
789            .into_any()
790    }
791}