terminal_panel.rs

   1use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
   2
   3use crate::{
   4    default_working_directory,
   5    persistence::{
   6        deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel,
   7    },
   8    TerminalView,
   9};
  10use breadcrumbs::Breadcrumbs;
  11use collections::HashMap;
  12use db::kvp::KEY_VALUE_STORE;
  13use futures::future::join_all;
  14use gpui::{
  15    actions, Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity,
  16    EventEmitter, ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels,
  17    Render, Styled, Task, WeakEntity, Window,
  18};
  19use itertools::Itertools;
  20use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
  21use search::{buffer_search::DivRegistrar, BufferSearchBar};
  22use settings::Settings;
  23use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};
  24use terminal::{
  25    terminal_settings::{TerminalDockPosition, TerminalSettings},
  26    Terminal,
  27};
  28use ui::{
  29    prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable,
  30    Tooltip,
  31};
  32use util::{ResultExt, TryFutureExt};
  33use workspace::{
  34    dock::{DockPosition, Panel, PanelEvent, PanelHandle},
  35    item::SerializableItem,
  36    move_active_item, move_item, pane,
  37    ui::IconName,
  38    ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane,
  39    DraggedSelection, DraggedTab, ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal,
  40    Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp,
  41    SwapPaneInDirection, ToggleZoom, Workspace,
  42};
  43
  44use anyhow::{anyhow, Context as _, Result};
  45use zed_actions::assistant::InlineAssist;
  46
  47const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
  48
  49actions!(terminal_panel, [ToggleFocus]);
  50
  51pub fn init(cx: &mut App) {
  52    cx.observe_new(
  53        |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
  54            workspace.register_action(TerminalPanel::new_terminal);
  55            workspace.register_action(TerminalPanel::open_terminal);
  56            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
  57                if is_enabled_in_workspace(workspace, cx) {
  58                    workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
  59                }
  60            });
  61        },
  62    )
  63    .detach();
  64}
  65
  66pub struct TerminalPanel {
  67    pub(crate) active_pane: Entity<Pane>,
  68    pub(crate) center: PaneGroup,
  69    fs: Arc<dyn Fs>,
  70    workspace: WeakEntity<Workspace>,
  71    pub(crate) width: Option<Pixels>,
  72    pub(crate) height: Option<Pixels>,
  73    pending_serialization: Task<Option<()>>,
  74    pending_terminals_to_add: usize,
  75    deferred_tasks: HashMap<TaskId, Task<()>>,
  76    assistant_enabled: bool,
  77    assistant_tab_bar_button: Option<AnyView>,
  78    active: bool,
  79}
  80
  81impl TerminalPanel {
  82    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
  83        let project = workspace.project();
  84        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
  85        let center = PaneGroup::new(pane.clone());
  86        let terminal_panel = Self {
  87            center,
  88            active_pane: pane,
  89            fs: workspace.app_state().fs.clone(),
  90            workspace: workspace.weak_handle(),
  91            pending_serialization: Task::ready(None),
  92            width: None,
  93            height: None,
  94            pending_terminals_to_add: 0,
  95            deferred_tasks: HashMap::default(),
  96            assistant_enabled: false,
  97            assistant_tab_bar_button: None,
  98            active: false,
  99        };
 100        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
 101        terminal_panel
 102    }
 103
 104    pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
 105        self.assistant_enabled = enabled;
 106        if enabled {
 107            let focus_handle = self
 108                .active_pane
 109                .read(cx)
 110                .active_item()
 111                .map(|item| item.item_focus_handle(cx))
 112                .unwrap_or(self.focus_handle(cx));
 113            self.assistant_tab_bar_button = Some(
 114                cx.new(move |_| InlineAssistTabBarButton { focus_handle })
 115                    .into(),
 116            );
 117        } else {
 118            self.assistant_tab_bar_button = None;
 119        }
 120        for pane in self.center.panes() {
 121            self.apply_tab_bar_buttons(pane, cx);
 122        }
 123    }
 124
 125    fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {
 126        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
 127        terminal_pane.update(cx, |pane, cx| {
 128            pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
 129                let split_context = pane
 130                    .active_item()
 131                    .and_then(|item| item.downcast::<TerminalView>())
 132                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
 133                if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
 134                    return (None, None);
 135                }
 136                let focus_handle = pane.focus_handle(cx);
 137                let right_children = h_flex()
 138                    .gap(DynamicSpacing::Base02.rems(cx))
 139                    .child(
 140                        PopoverMenu::new("terminal-tab-bar-popover-menu")
 141                            .trigger(
 142                                IconButton::new("plus", IconName::Plus)
 143                                    .icon_size(IconSize::Small)
 144                                    .tooltip(Tooltip::text("New…")),
 145                            )
 146                            .anchor(Corner::TopRight)
 147                            .with_handle(pane.new_item_context_menu_handle.clone())
 148                            .menu(move |window, cx| {
 149                                let focus_handle = focus_handle.clone();
 150                                let menu = ContextMenu::build(window, cx, |menu, _, _| {
 151                                    menu.context(focus_handle.clone())
 152                                        .action(
 153                                            "New Terminal",
 154                                            workspace::NewTerminal.boxed_clone(),
 155                                        )
 156                                        // We want the focus to go back to terminal panel once task modal is dismissed,
 157                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
 158                                        // context menu will be gone the moment we spawn the modal.
 159                                        .action(
 160                                            "Spawn task",
 161                                            zed_actions::Spawn::modal().boxed_clone(),
 162                                        )
 163                                });
 164
 165                                Some(menu)
 166                            }),
 167                    )
 168                    .children(assistant_tab_bar_button.clone())
 169                    .child(
 170                        PopoverMenu::new("terminal-pane-tab-bar-split")
 171                            .trigger(
 172                                IconButton::new("terminal-pane-split", IconName::Split)
 173                                    .icon_size(IconSize::Small)
 174                                    .tooltip(Tooltip::text("Split Pane")),
 175                            )
 176                            .anchor(Corner::TopRight)
 177                            .with_handle(pane.split_item_context_menu_handle.clone())
 178                            .menu({
 179                                let split_context = split_context.clone();
 180                                move |window, cx| {
 181                                    ContextMenu::build(window, cx, |menu, _, _| {
 182                                        menu.when_some(
 183                                            split_context.clone(),
 184                                            |menu, split_context| menu.context(split_context),
 185                                        )
 186                                        .action("Split Right", SplitRight.boxed_clone())
 187                                        .action("Split Left", SplitLeft.boxed_clone())
 188                                        .action("Split Up", SplitUp.boxed_clone())
 189                                        .action("Split Down", SplitDown.boxed_clone())
 190                                    })
 191                                    .into()
 192                                }
 193                            }),
 194                    )
 195                    .child({
 196                        let zoomed = pane.is_zoomed();
 197                        IconButton::new("toggle_zoom", IconName::Maximize)
 198                            .icon_size(IconSize::Small)
 199                            .toggle_state(zoomed)
 200                            .selected_icon(IconName::Minimize)
 201                            .on_click(cx.listener(|pane, _, window, cx| {
 202                                pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
 203                            }))
 204                            .tooltip(move |window, cx| {
 205                                Tooltip::for_action(
 206                                    if zoomed { "Zoom Out" } else { "Zoom In" },
 207                                    &ToggleZoom,
 208                                    window,
 209                                    cx,
 210                                )
 211                            })
 212                    })
 213                    .into_any_element()
 214                    .into();
 215                (None, right_children)
 216            });
 217        });
 218    }
 219
 220    pub async fn load(
 221        workspace: WeakEntity<Workspace>,
 222        mut cx: AsyncWindowContext,
 223    ) -> Result<Entity<Self>> {
 224        let serialized_panel = cx
 225            .background_executor()
 226            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
 227            .await
 228            .log_err()
 229            .flatten()
 230            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
 231            .transpose()
 232            .log_err()
 233            .flatten();
 234
 235        let terminal_panel = workspace
 236            .update_in(&mut cx, |workspace, window, cx| {
 237                match serialized_panel.zip(workspace.database_id()) {
 238                    Some((serialized_panel, database_id)) => deserialize_terminal_panel(
 239                        workspace.weak_handle(),
 240                        workspace.project().clone(),
 241                        database_id,
 242                        serialized_panel,
 243                        window,
 244                        cx,
 245                    ),
 246                    None => Task::ready(Ok(cx.new(|cx| TerminalPanel::new(workspace, window, cx)))),
 247                }
 248            })?
 249            .await?;
 250
 251        if let Some(workspace) = workspace.upgrade() {
 252            terminal_panel
 253                .update_in(&mut cx, |_, window, cx| {
 254                    cx.subscribe_in(&workspace, window, |terminal_panel, _, e, window, cx| {
 255                        if let workspace::Event::SpawnTask {
 256                            action: spawn_in_terminal,
 257                        } = e
 258                        {
 259                            terminal_panel.spawn_task(spawn_in_terminal, window, cx);
 260                        };
 261                    })
 262                    .detach();
 263                })
 264                .ok();
 265        }
 266
 267        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
 268        if let Some(workspace) = workspace.upgrade() {
 269            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
 270                let alive_item_ids = terminal_panel
 271                    .read(cx)
 272                    .center
 273                    .panes()
 274                    .into_iter()
 275                    .flat_map(|pane| pane.read(cx).items())
 276                    .map(|item| item.item_id().as_u64() as ItemId)
 277                    .collect();
 278                workspace.database_id().map(|workspace_id| {
 279                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
 280                })
 281            })?;
 282            if let Some(task) = cleanup_task {
 283                task.await.log_err();
 284            }
 285        }
 286
 287        if let Some(workspace) = workspace.upgrade() {
 288            let should_focus = workspace
 289                .update_in(&mut cx, |workspace, window, cx| {
 290                    workspace.active_item(cx).is_none()
 291                        && workspace
 292                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
 293                })
 294                .unwrap_or(false);
 295
 296            if should_focus {
 297                terminal_panel
 298                    .update_in(&mut cx, |panel, window, cx| {
 299                        panel.active_pane.update(cx, |pane, cx| {
 300                            pane.focus_active_item(window, cx);
 301                        });
 302                    })
 303                    .ok();
 304            }
 305        }
 306
 307        Ok(terminal_panel)
 308    }
 309
 310    fn handle_pane_event(
 311        &mut self,
 312        pane: &Entity<Pane>,
 313        event: &pane::Event,
 314        window: &mut Window,
 315        cx: &mut Context<Self>,
 316    ) {
 317        match event {
 318            pane::Event::ActivateItem { .. } => self.serialize(cx),
 319            pane::Event::RemovedItem { .. } => self.serialize(cx),
 320            pane::Event::Remove { focus_on_pane } => {
 321                let pane_count_before_removal = self.center.panes().len();
 322                let _removal_result = self.center.remove(&pane);
 323                if pane_count_before_removal == 1 {
 324                    self.center.first_pane().update(cx, |pane, cx| {
 325                        pane.set_zoomed(false, cx);
 326                    });
 327                    cx.emit(PanelEvent::Close);
 328                } else {
 329                    if let Some(focus_on_pane) =
 330                        focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
 331                    {
 332                        focus_on_pane.focus_handle(cx).focus(window);
 333                    }
 334                }
 335            }
 336            pane::Event::ZoomIn => {
 337                for pane in self.center.panes() {
 338                    pane.update(cx, |pane, cx| {
 339                        pane.set_zoomed(true, cx);
 340                    })
 341                }
 342                cx.emit(PanelEvent::ZoomIn);
 343                cx.notify();
 344            }
 345            pane::Event::ZoomOut => {
 346                for pane in self.center.panes() {
 347                    pane.update(cx, |pane, cx| {
 348                        pane.set_zoomed(false, cx);
 349                    })
 350                }
 351                cx.emit(PanelEvent::ZoomOut);
 352                cx.notify();
 353            }
 354            pane::Event::AddItem { item } => {
 355                if let Some(workspace) = self.workspace.upgrade() {
 356                    workspace.update(cx, |workspace, cx| {
 357                        item.added_to_pane(workspace, pane.clone(), window, cx)
 358                    })
 359                }
 360                self.serialize(cx);
 361            }
 362            pane::Event::Split(direction) => {
 363                let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else {
 364                    return;
 365                };
 366                let pane = pane.clone();
 367                let direction = *direction;
 368                self.center.split(&pane, &new_pane, direction).log_err();
 369                window.focus(&new_pane.focus_handle(cx));
 370            }
 371            pane::Event::Focus => {
 372                self.active_pane = pane.clone();
 373            }
 374
 375            _ => {}
 376        }
 377    }
 378
 379    fn new_pane_with_cloned_active_terminal(
 380        &mut self,
 381        window: &mut Window,
 382        cx: &mut Context<Self>,
 383    ) -> Option<Entity<Pane>> {
 384        let workspace = self.workspace.upgrade()?;
 385        let workspace = workspace.read(cx);
 386        let database_id = workspace.database_id();
 387        let weak_workspace = self.workspace.clone();
 388        let project = workspace.project().clone();
 389        let (working_directory, python_venv_directory) = self
 390            .active_pane
 391            .read(cx)
 392            .active_item()
 393            .and_then(|item| item.downcast::<TerminalView>())
 394            .map(|terminal_view| {
 395                let terminal = terminal_view.read(cx).terminal().read(cx);
 396                (
 397                    terminal
 398                        .working_directory()
 399                        .or_else(|| default_working_directory(workspace, cx)),
 400                    terminal.python_venv_directory.clone(),
 401                )
 402            })
 403            .unwrap_or((None, None));
 404        let kind = TerminalKind::Shell(working_directory);
 405        let window_handle = window.window_handle();
 406        let terminal = project
 407            .update(cx, |project, cx| {
 408                project.create_terminal_with_venv(kind, python_venv_directory, window_handle, cx)
 409            })
 410            .ok()?;
 411
 412        let terminal_view = Box::new(cx.new(|cx| {
 413            TerminalView::new(
 414                terminal.clone(),
 415                weak_workspace.clone(),
 416                database_id,
 417                project.downgrade(),
 418                window,
 419                cx,
 420            )
 421        }));
 422        let pane = new_terminal_pane(
 423            weak_workspace,
 424            project,
 425            self.active_pane.read(cx).is_zoomed(),
 426            window,
 427            cx,
 428        );
 429        self.apply_tab_bar_buttons(&pane, cx);
 430        pane.update(cx, |pane, cx| {
 431            pane.add_item(terminal_view, true, true, None, window, cx);
 432        });
 433
 434        Some(pane)
 435    }
 436
 437    pub fn open_terminal(
 438        workspace: &mut Workspace,
 439        action: &workspace::OpenTerminal,
 440        window: &mut Window,
 441        cx: &mut Context<Workspace>,
 442    ) {
 443        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 444            return;
 445        };
 446
 447        terminal_panel
 448            .update(cx, |panel, cx| {
 449                panel.add_terminal(
 450                    TerminalKind::Shell(Some(action.working_directory.clone())),
 451                    RevealStrategy::Always,
 452                    window,
 453                    cx,
 454                )
 455            })
 456            .detach_and_log_err(cx);
 457    }
 458
 459    fn spawn_task(&mut self, task: &SpawnInTerminal, window: &mut Window, cx: &mut Context<Self>) {
 460        let Ok(is_local) = self
 461            .workspace
 462            .update(cx, |workspace, cx| workspace.project().read(cx).is_local())
 463        else {
 464            return;
 465        };
 466
 467        let builder = ShellBuilder::new(is_local, &task.shell);
 468        let command_label = builder.command_label(&task.command_label);
 469        let (command, args) = builder.build(task.command.clone(), &task.args);
 470
 471        let task = SpawnInTerminal {
 472            command_label,
 473            command,
 474            args,
 475            ..task.clone()
 476        };
 477
 478        if task.allow_concurrent_runs && task.use_new_terminal {
 479            self.spawn_in_new_terminal(task, window, cx)
 480                .detach_and_log_err(cx);
 481            return;
 482        }
 483
 484        let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
 485        let Some(existing) = terminals_for_task.pop() else {
 486            self.spawn_in_new_terminal(task, window, cx)
 487                .detach_and_log_err(cx);
 488            return;
 489        };
 490
 491        let (existing_item_index, task_pane, existing_terminal) = existing;
 492        if task.allow_concurrent_runs {
 493            self.replace_terminal(
 494                task,
 495                task_pane,
 496                existing_item_index,
 497                existing_terminal,
 498                window,
 499                cx,
 500            )
 501            .detach();
 502            return;
 503        }
 504
 505        self.deferred_tasks.insert(
 506            task.id.clone(),
 507            cx.spawn_in(window, |terminal_panel, mut cx| async move {
 508                wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
 509                let task = terminal_panel.update_in(&mut cx, |terminal_panel, window, cx| {
 510                    if task.use_new_terminal {
 511                        terminal_panel
 512                            .spawn_in_new_terminal(task, window, cx)
 513                            .detach_and_log_err(cx);
 514                        None
 515                    } else {
 516                        Some(terminal_panel.replace_terminal(
 517                            task,
 518                            task_pane,
 519                            existing_item_index,
 520                            existing_terminal,
 521                            window,
 522                            cx,
 523                        ))
 524                    }
 525                });
 526                if let Ok(Some(task)) = task {
 527                    task.await;
 528                }
 529            }),
 530        );
 531    }
 532
 533    pub fn spawn_in_new_terminal(
 534        &mut self,
 535        spawn_task: SpawnInTerminal,
 536        window: &mut Window,
 537        cx: &mut Context<Self>,
 538    ) -> Task<Result<Entity<Terminal>>> {
 539        let reveal = spawn_task.reveal;
 540        let reveal_target = spawn_task.reveal_target;
 541        let kind = TerminalKind::Task(spawn_task);
 542        match reveal_target {
 543            RevealTarget::Center => self
 544                .workspace
 545                .update(cx, |workspace, cx| {
 546                    Self::add_center_terminal(workspace, kind, window, cx)
 547                })
 548                .unwrap_or_else(|e| Task::ready(Err(e))),
 549            RevealTarget::Dock => self.add_terminal(kind, reveal, window, cx),
 550        }
 551    }
 552
 553    /// Create a new Terminal in the current working directory or the user's home directory
 554    fn new_terminal(
 555        workspace: &mut Workspace,
 556        _: &workspace::NewTerminal,
 557        window: &mut Window,
 558        cx: &mut Context<Workspace>,
 559    ) {
 560        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
 561            return;
 562        };
 563
 564        let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
 565
 566        terminal_panel
 567            .update(cx, |this, cx| {
 568                this.add_terminal(kind, RevealStrategy::Always, window, cx)
 569            })
 570            .detach_and_log_err(cx);
 571    }
 572
 573    fn terminals_for_task(
 574        &self,
 575        label: &str,
 576        cx: &mut App,
 577    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
 578        let Some(workspace) = self.workspace.upgrade() else {
 579            return Vec::new();
 580        };
 581
 582        let pane_terminal_views = |pane: Entity<Pane>| {
 583            pane.read(cx)
 584                .items()
 585                .enumerate()
 586                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
 587                .filter_map(|(index, terminal_view)| {
 588                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
 589                    if &task_state.full_label == label {
 590                        Some((index, terminal_view))
 591                    } else {
 592                        None
 593                    }
 594                })
 595                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
 596        };
 597
 598        self.center
 599            .panes()
 600            .into_iter()
 601            .cloned()
 602            .flat_map(pane_terminal_views)
 603            .chain(
 604                workspace
 605                    .read(cx)
 606                    .panes()
 607                    .into_iter()
 608                    .cloned()
 609                    .flat_map(pane_terminal_views),
 610            )
 611            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
 612            .collect()
 613    }
 614
 615    fn activate_terminal_view(
 616        &self,
 617        pane: &Entity<Pane>,
 618        item_index: usize,
 619        focus: bool,
 620        window: &mut Window,
 621        cx: &mut App,
 622    ) {
 623        pane.update(cx, |pane, cx| {
 624            pane.activate_item(item_index, true, focus, window, cx)
 625        })
 626    }
 627
 628    pub fn add_center_terminal(
 629        workspace: &mut Workspace,
 630        kind: TerminalKind,
 631        window: &mut Window,
 632        cx: &mut Context<Workspace>,
 633    ) -> Task<Result<Entity<Terminal>>> {
 634        if !is_enabled_in_workspace(workspace, cx) {
 635            return Task::ready(Err(anyhow!(
 636                "terminal not yet supported for remote projects"
 637            )));
 638        }
 639        let window_handle = window.window_handle();
 640        let project = workspace.project().downgrade();
 641        cx.spawn_in(window, move |workspace, mut cx| async move {
 642            let terminal = project
 643                .update(&mut cx, |project, cx| {
 644                    project.create_terminal(kind, window_handle, cx)
 645                })?
 646                .await?;
 647
 648            workspace.update_in(&mut cx, |workspace, window, cx| {
 649                let terminal_view = cx.new(|cx| {
 650                    TerminalView::new(
 651                        terminal.clone(),
 652                        workspace.weak_handle(),
 653                        workspace.database_id(),
 654                        workspace.project().downgrade(),
 655                        window,
 656                        cx,
 657                    )
 658                });
 659                workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx);
 660            })?;
 661            Ok(terminal)
 662        })
 663    }
 664
 665    fn add_terminal(
 666        &mut self,
 667        kind: TerminalKind,
 668        reveal_strategy: RevealStrategy,
 669        window: &mut Window,
 670        cx: &mut Context<Self>,
 671    ) -> Task<Result<Entity<Terminal>>> {
 672        let workspace = self.workspace.clone();
 673        cx.spawn_in(window, |terminal_panel, mut cx| async move {
 674            if workspace.update(&mut cx, |workspace, cx| {
 675                !is_enabled_in_workspace(workspace, cx)
 676            })? {
 677                anyhow::bail!("terminal not yet supported for remote projects");
 678            }
 679            let pane = terminal_panel.update(&mut cx, |terminal_panel, _| {
 680                terminal_panel.pending_terminals_to_add += 1;
 681                terminal_panel.active_pane.clone()
 682            })?;
 683            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
 684            let window_handle = cx.window_handle();
 685            let terminal = project
 686                .update(&mut cx, |project, cx| {
 687                    project.create_terminal(kind, window_handle, cx)
 688                })?
 689                .await?;
 690            let result = workspace.update_in(&mut cx, |workspace, window, cx| {
 691                let terminal_view = Box::new(cx.new(|cx| {
 692                    TerminalView::new(
 693                        terminal.clone(),
 694                        workspace.weak_handle(),
 695                        workspace.database_id(),
 696                        workspace.project().downgrade(),
 697                        window,
 698                        cx,
 699                    )
 700                }));
 701
 702                match reveal_strategy {
 703                    RevealStrategy::Always => {
 704                        workspace.focus_panel::<Self>(window, cx);
 705                    }
 706                    RevealStrategy::NoFocus => {
 707                        workspace.open_panel::<Self>(window, cx);
 708                    }
 709                    RevealStrategy::Never => {}
 710                }
 711
 712                pane.update(cx, |pane, cx| {
 713                    let focus = pane.has_focus(window, cx)
 714                        || matches!(reveal_strategy, RevealStrategy::Always);
 715                    pane.add_item(terminal_view, true, focus, None, window, cx);
 716                });
 717
 718                Ok(terminal)
 719            })?;
 720            terminal_panel.update(&mut cx, |this, cx| {
 721                this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
 722                this.serialize(cx)
 723            })?;
 724            result
 725        })
 726    }
 727
 728    fn serialize(&mut self, cx: &mut Context<Self>) {
 729        let height = self.height;
 730        let width = self.width;
 731        self.pending_serialization = cx.spawn(|terminal_panel, mut cx| async move {
 732            cx.background_executor()
 733                .timer(Duration::from_millis(50))
 734                .await;
 735            let terminal_panel = terminal_panel.upgrade()?;
 736            let items = terminal_panel
 737                .update(&mut cx, |terminal_panel, cx| {
 738                    SerializedItems::WithSplits(serialize_pane_group(
 739                        &terminal_panel.center,
 740                        &terminal_panel.active_pane,
 741                        cx,
 742                    ))
 743                })
 744                .ok()?;
 745            cx.background_executor()
 746                .spawn(
 747                    async move {
 748                        KEY_VALUE_STORE
 749                            .write_kvp(
 750                                TERMINAL_PANEL_KEY.into(),
 751                                serde_json::to_string(&SerializedTerminalPanel {
 752                                    items,
 753                                    active_item_id: None,
 754                                    height,
 755                                    width,
 756                                })?,
 757                            )
 758                            .await?;
 759                        anyhow::Ok(())
 760                    }
 761                    .log_err(),
 762                )
 763                .await;
 764            Some(())
 765        });
 766    }
 767
 768    fn replace_terminal(
 769        &self,
 770        spawn_task: SpawnInTerminal,
 771        task_pane: Entity<Pane>,
 772        terminal_item_index: usize,
 773        terminal_to_replace: Entity<TerminalView>,
 774        window: &mut Window,
 775        cx: &mut Context<Self>,
 776    ) -> Task<Option<()>> {
 777        let reveal = spawn_task.reveal;
 778        let reveal_target = spawn_task.reveal_target;
 779        let window_handle = window.window_handle();
 780        let task_workspace = self.workspace.clone();
 781        cx.spawn_in(window, move |terminal_panel, mut cx| async move {
 782            let project = terminal_panel
 783                .update(&mut cx, |this, cx| {
 784                    this.workspace
 785                        .update(cx, |workspace, _| workspace.project().clone())
 786                        .ok()
 787                })
 788                .ok()
 789                .flatten()?;
 790            let new_terminal = project
 791                .update(&mut cx, |project, cx| {
 792                    project.create_terminal(TerminalKind::Task(spawn_task), window_handle, cx)
 793                })
 794                .ok()?
 795                .await
 796                .log_err()?;
 797            terminal_to_replace
 798                .update_in(&mut cx, |terminal_to_replace, window, cx| {
 799                    terminal_to_replace.set_terminal(new_terminal, window, cx);
 800                })
 801                .ok()?;
 802
 803            match reveal {
 804                RevealStrategy::Always => match reveal_target {
 805                    RevealTarget::Center => {
 806                        task_workspace
 807                            .update_in(&mut cx, |workspace, window, cx| {
 808                                workspace
 809                                    .active_item(cx)
 810                                    .context("retrieving active terminal item in the workspace")
 811                                    .log_err()?
 812                                    .item_focus_handle(cx)
 813                                    .focus(window);
 814                                Some(())
 815                            })
 816                            .ok()??;
 817                    }
 818                    RevealTarget::Dock => {
 819                        terminal_panel
 820                            .update_in(&mut cx, |terminal_panel, window, cx| {
 821                                terminal_panel.activate_terminal_view(
 822                                    &task_pane,
 823                                    terminal_item_index,
 824                                    true,
 825                                    window,
 826                                    cx,
 827                                )
 828                            })
 829                            .ok()?;
 830
 831                        cx.spawn(|mut cx| async move {
 832                            task_workspace
 833                                .update_in(&mut cx, |workspace, window, cx| {
 834                                    workspace.focus_panel::<Self>(window, cx)
 835                                })
 836                                .ok()
 837                        })
 838                        .detach();
 839                    }
 840                },
 841                RevealStrategy::NoFocus => match reveal_target {
 842                    RevealTarget::Center => {
 843                        task_workspace
 844                            .update_in(&mut cx, |workspace, window, cx| {
 845                                workspace.active_pane().focus_handle(cx).focus(window);
 846                            })
 847                            .ok()?;
 848                    }
 849                    RevealTarget::Dock => {
 850                        terminal_panel
 851                            .update_in(&mut cx, |terminal_panel, window, cx| {
 852                                terminal_panel.activate_terminal_view(
 853                                    &task_pane,
 854                                    terminal_item_index,
 855                                    false,
 856                                    window,
 857                                    cx,
 858                                )
 859                            })
 860                            .ok()?;
 861
 862                        cx.spawn(|mut cx| async move {
 863                            task_workspace
 864                                .update_in(&mut cx, |workspace, window, cx| {
 865                                    workspace.open_panel::<Self>(window, cx)
 866                                })
 867                                .ok()
 868                        })
 869                        .detach();
 870                    }
 871                },
 872                RevealStrategy::Never => {}
 873            }
 874
 875            Some(())
 876        })
 877    }
 878
 879    fn has_no_terminals(&self, cx: &App) -> bool {
 880        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
 881    }
 882
 883    pub fn assistant_enabled(&self) -> bool {
 884        self.assistant_enabled
 885    }
 886
 887    fn is_enabled(&self, cx: &App) -> bool {
 888        self.workspace.upgrade().map_or(false, |workspace| {
 889            is_enabled_in_workspace(workspace.read(cx), cx)
 890        })
 891    }
 892}
 893
 894fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
 895    workspace.project().read(cx).supports_terminal(cx)
 896}
 897
 898pub fn new_terminal_pane(
 899    workspace: WeakEntity<Workspace>,
 900    project: Entity<Project>,
 901    zoomed: bool,
 902    window: &mut Window,
 903    cx: &mut Context<TerminalPanel>,
 904) -> Entity<Pane> {
 905    let is_local = project.read(cx).is_local();
 906    let terminal_panel = cx.entity().clone();
 907    let pane = cx.new(|cx| {
 908        let mut pane = Pane::new(
 909            workspace.clone(),
 910            project.clone(),
 911            Default::default(),
 912            None,
 913            NewTerminal.boxed_clone(),
 914            window,
 915            cx,
 916        );
 917        pane.set_zoomed(zoomed, cx);
 918        pane.set_can_navigate(false, cx);
 919        pane.display_nav_history_buttons(None);
 920        pane.set_should_display_tab_bar(|_, _| true);
 921        pane.set_zoom_out_on_close(false);
 922
 923        let split_closure_terminal_panel = terminal_panel.downgrade();
 924        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
 925            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
 926                let is_current_pane = tab.pane == cx.entity();
 927                let Some(can_drag_away) = split_closure_terminal_panel
 928                    .update(cx, |terminal_panel, _| {
 929                        let current_panes = terminal_panel.center.panes();
 930                        !current_panes.contains(&&tab.pane)
 931                            || current_panes.len() > 1
 932                            || (!is_current_pane || pane.items_len() > 1)
 933                    })
 934                    .ok()
 935                else {
 936                    return false;
 937                };
 938                if can_drag_away {
 939                    let item = if is_current_pane {
 940                        pane.item_for_index(tab.ix)
 941                    } else {
 942                        tab.pane.read(cx).item_for_index(tab.ix)
 943                    };
 944                    if let Some(item) = item {
 945                        return item.downcast::<TerminalView>().is_some();
 946                    }
 947                }
 948            }
 949            false
 950        })));
 951
 952        let buffer_search_bar = cx.new(|cx| search::BufferSearchBar::new(window, cx));
 953        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
 954        pane.toolbar().update(cx, |toolbar, cx| {
 955            toolbar.add_item(buffer_search_bar, window, cx);
 956            toolbar.add_item(breadcrumbs, window, cx);
 957        });
 958
 959        let drop_closure_project = project.downgrade();
 960        let drop_closure_terminal_panel = terminal_panel.downgrade();
 961        pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
 962            let Some(project) = drop_closure_project.upgrade() else {
 963                return ControlFlow::Break(());
 964            };
 965            if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
 966                let this_pane = cx.entity().clone();
 967                let item = if tab.pane == this_pane {
 968                    pane.item_for_index(tab.ix)
 969                } else {
 970                    tab.pane.read(cx).item_for_index(tab.ix)
 971                };
 972                if let Some(item) = item {
 973                    if item.downcast::<TerminalView>().is_some() {
 974                        let source = tab.pane.clone();
 975                        let item_id_to_move = item.item_id();
 976
 977                        let Ok(new_split_pane) = pane
 978                            .drag_split_direction()
 979                            .map(|split_direction| {
 980                                drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
 981                                    let is_zoomed = if terminal_panel.active_pane == this_pane {
 982                                        pane.is_zoomed()
 983                                    } else {
 984                                        terminal_panel.active_pane.read(cx).is_zoomed()
 985                                    };
 986                                    let new_pane = new_terminal_pane(
 987                                        workspace.clone(),
 988                                        project.clone(),
 989                                        is_zoomed,
 990                                        window,
 991                                        cx,
 992                                    );
 993                                    terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
 994                                    terminal_panel.center.split(
 995                                        &this_pane,
 996                                        &new_pane,
 997                                        split_direction,
 998                                    )?;
 999                                    anyhow::Ok(new_pane)
1000                                })
1001                            })
1002                            .transpose()
1003                        else {
1004                            return ControlFlow::Break(());
1005                        };
1006
1007                        match new_split_pane.transpose() {
1008                            // Source pane may be the one currently updated, so defer the move.
1009                            Ok(Some(new_pane)) => cx
1010                                .spawn_in(window, |_, mut cx| async move {
1011                                    cx.update(|window, cx| {
1012                                        move_item(
1013                                            &source,
1014                                            &new_pane,
1015                                            item_id_to_move,
1016                                            new_pane.read(cx).active_item_index(),
1017                                            window,
1018                                            cx,
1019                                        );
1020                                    })
1021                                    .ok();
1022                                })
1023                                .detach(),
1024                            // If we drop into existing pane or current pane,
1025                            // regular pane drop handler will take care of it,
1026                            // using the right tab index for the operation.
1027                            Ok(None) => return ControlFlow::Continue(()),
1028                            err @ Err(_) => {
1029                                err.log_err();
1030                                return ControlFlow::Break(());
1031                            }
1032                        };
1033                    } else if let Some(project_path) = item.project_path(cx) {
1034                        if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
1035                        {
1036                            add_paths_to_terminal(pane, &[entry_path], window, cx);
1037                        }
1038                    }
1039                }
1040            } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
1041                let project = project.read(cx);
1042                let paths_to_add = selection
1043                    .items()
1044                    .map(|selected_entry| selected_entry.entry_id)
1045                    .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
1046                    .filter_map(|project_path| project.absolute_path(&project_path, cx))
1047                    .collect::<Vec<_>>();
1048                if !paths_to_add.is_empty() {
1049                    add_paths_to_terminal(pane, &paths_to_add, window, cx);
1050                }
1051            } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
1052                if let Some(entry_path) = project
1053                    .read(cx)
1054                    .path_for_entry(entry_id, cx)
1055                    .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
1056                {
1057                    add_paths_to_terminal(pane, &[entry_path], window, cx);
1058                }
1059            } else if is_local {
1060                if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
1061                    add_paths_to_terminal(pane, paths.paths(), window, cx);
1062                }
1063            }
1064
1065            ControlFlow::Break(())
1066        });
1067
1068        pane
1069    });
1070
1071    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1072        .detach();
1073    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1074
1075    pane
1076}
1077
1078async fn wait_for_terminals_tasks(
1079    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1080    cx: &mut AsyncApp,
1081) {
1082    let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| {
1083        terminal
1084            .update(cx, |terminal_view, cx| {
1085                terminal_view
1086                    .terminal()
1087                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1088            })
1089            .ok()
1090    });
1091    let _: Vec<()> = join_all(pending_tasks).await;
1092}
1093
1094fn add_paths_to_terminal(
1095    pane: &mut Pane,
1096    paths: &[PathBuf],
1097    window: &mut Window,
1098    cx: &mut Context<Pane>,
1099) {
1100    if let Some(terminal_view) = pane
1101        .active_item()
1102        .and_then(|item| item.downcast::<TerminalView>())
1103    {
1104        window.focus(&terminal_view.focus_handle(cx));
1105        let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
1106        new_text.push(' ');
1107        terminal_view.update(cx, |terminal_view, cx| {
1108            terminal_view.terminal().update(cx, |terminal, _| {
1109                terminal.paste(&new_text);
1110            });
1111        });
1112    }
1113}
1114
1115impl EventEmitter<PanelEvent> for TerminalPanel {}
1116
1117impl Render for TerminalPanel {
1118    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1119        let mut registrar = DivRegistrar::new(
1120            |panel, _, cx| {
1121                panel
1122                    .active_pane
1123                    .read(cx)
1124                    .toolbar()
1125                    .read(cx)
1126                    .item_of_type::<BufferSearchBar>()
1127            },
1128            cx,
1129        );
1130        BufferSearchBar::register(&mut registrar);
1131        let registrar = registrar.into_div();
1132        self.workspace
1133            .update(cx, |workspace, cx| {
1134                registrar.size_full().child(self.center.render(
1135                    workspace.project(),
1136                    &HashMap::default(),
1137                    None,
1138                    &self.active_pane,
1139                    workspace.zoomed_item(),
1140                    workspace.app_state(),
1141                    window,
1142                    cx,
1143                ))
1144            })
1145            .ok()
1146            .map(|div| {
1147                div.on_action({
1148                    cx.listener(
1149                        |terminal_panel, action: &ActivatePaneInDirection, window, cx| {
1150                            if let Some(pane) = terminal_panel.center.find_pane_in_direction(
1151                                &terminal_panel.active_pane,
1152                                action.0,
1153                                cx,
1154                            ) {
1155                                window.focus(&pane.focus_handle(cx));
1156                            } else {
1157                                terminal_panel
1158                                    .workspace
1159                                    .update(cx, |workspace, cx| {
1160                                        workspace.activate_pane_in_direction(action.0, window, cx)
1161                                    })
1162                                    .ok();
1163                            }
1164                        },
1165                    )
1166                })
1167                .on_action(
1168                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1169                        let panes = terminal_panel.center.panes();
1170                        if let Some(ix) = panes
1171                            .iter()
1172                            .position(|pane| **pane == terminal_panel.active_pane)
1173                        {
1174                            let next_ix = (ix + 1) % panes.len();
1175                            window.focus(&panes[next_ix].focus_handle(cx));
1176                        }
1177                    }),
1178                )
1179                .on_action(cx.listener(
1180                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1181                        let panes = terminal_panel.center.panes();
1182                        if let Some(ix) = panes
1183                            .iter()
1184                            .position(|pane| **pane == terminal_panel.active_pane)
1185                        {
1186                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1187                            window.focus(&panes[prev_ix].focus_handle(cx));
1188                        }
1189                    },
1190                ))
1191                .on_action(
1192                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1193                        let panes = terminal_panel.center.panes();
1194                        if let Some(&pane) = panes.get(action.0) {
1195                            window.focus(&pane.read(cx).focus_handle(cx));
1196                        } else {
1197                            if let Some(new_pane) =
1198                                terminal_panel.new_pane_with_cloned_active_terminal(window, cx)
1199                            {
1200                                terminal_panel
1201                                    .center
1202                                    .split(
1203                                        &terminal_panel.active_pane,
1204                                        &new_pane,
1205                                        SplitDirection::Right,
1206                                    )
1207                                    .log_err();
1208                                window.focus(&new_pane.focus_handle(cx));
1209                            }
1210                        }
1211                    }),
1212                )
1213                .on_action(
1214                    cx.listener(|terminal_panel, action: &SwapPaneInDirection, _, cx| {
1215                        if let Some(to) = terminal_panel
1216                            .center
1217                            .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx)
1218                            .cloned()
1219                        {
1220                            terminal_panel.center.swap(&terminal_panel.active_pane, &to);
1221                            cx.notify();
1222                        }
1223                    }),
1224                )
1225                .on_action(
1226                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1227                        let Some(&target_pane) =
1228                            terminal_panel.center.panes().get(action.destination)
1229                        else {
1230                            return;
1231                        };
1232                        move_active_item(
1233                            &terminal_panel.active_pane,
1234                            target_pane,
1235                            action.focus,
1236                            true,
1237                            window,
1238                            cx,
1239                        );
1240                    }),
1241                )
1242                .on_action(cx.listener(
1243                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1244                        let source_pane = &terminal_panel.active_pane;
1245                        if let Some(destination_pane) = terminal_panel
1246                            .center
1247                            .find_pane_in_direction(source_pane, action.direction, cx)
1248                        {
1249                            move_active_item(
1250                                source_pane,
1251                                destination_pane,
1252                                action.focus,
1253                                true,
1254                                window,
1255                                cx,
1256                            );
1257                        };
1258                    },
1259                ))
1260            })
1261            .unwrap_or_else(|| div())
1262    }
1263}
1264
1265impl Focusable for TerminalPanel {
1266    fn focus_handle(&self, cx: &App) -> FocusHandle {
1267        self.active_pane.focus_handle(cx)
1268    }
1269}
1270
1271impl Panel for TerminalPanel {
1272    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1273        match TerminalSettings::get_global(cx).dock {
1274            TerminalDockPosition::Left => DockPosition::Left,
1275            TerminalDockPosition::Bottom => DockPosition::Bottom,
1276            TerminalDockPosition::Right => DockPosition::Right,
1277        }
1278    }
1279
1280    fn position_is_valid(&self, _: DockPosition) -> bool {
1281        true
1282    }
1283
1284    fn set_position(
1285        &mut self,
1286        position: DockPosition,
1287        _window: &mut Window,
1288        cx: &mut Context<Self>,
1289    ) {
1290        settings::update_settings_file::<TerminalSettings>(
1291            self.fs.clone(),
1292            cx,
1293            move |settings, _| {
1294                let dock = match position {
1295                    DockPosition::Left => TerminalDockPosition::Left,
1296                    DockPosition::Bottom => TerminalDockPosition::Bottom,
1297                    DockPosition::Right => TerminalDockPosition::Right,
1298                };
1299                settings.dock = Some(dock);
1300            },
1301        );
1302    }
1303
1304    fn size(&self, window: &Window, cx: &App) -> Pixels {
1305        let settings = TerminalSettings::get_global(cx);
1306        match self.position(window, cx) {
1307            DockPosition::Left | DockPosition::Right => {
1308                self.width.unwrap_or(settings.default_width)
1309            }
1310            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1311        }
1312    }
1313
1314    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1315        match self.position(window, cx) {
1316            DockPosition::Left | DockPosition::Right => self.width = size,
1317            DockPosition::Bottom => self.height = size,
1318        }
1319        self.serialize(cx);
1320        cx.notify();
1321    }
1322
1323    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1324        self.active_pane.read(cx).is_zoomed()
1325    }
1326
1327    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1328        for pane in self.center.panes() {
1329            pane.update(cx, |pane, cx| {
1330                pane.set_zoomed(zoomed, cx);
1331            })
1332        }
1333        cx.notify();
1334    }
1335
1336    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1337        let old_active = self.active;
1338        self.active = active;
1339        if !active || old_active == active || !self.has_no_terminals(cx) {
1340            return;
1341        }
1342        cx.defer_in(window, |this, window, cx| {
1343            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
1344                TerminalKind::Shell(default_working_directory(workspace, cx))
1345            }) else {
1346                return;
1347            };
1348
1349            this.add_terminal(kind, RevealStrategy::Always, window, cx)
1350                .detach_and_log_err(cx)
1351        })
1352    }
1353
1354    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1355        let count = self
1356            .center
1357            .panes()
1358            .into_iter()
1359            .map(|pane| pane.read(cx).items_len())
1360            .sum::<usize>();
1361        if count == 0 {
1362            None
1363        } else {
1364            Some(count.to_string())
1365        }
1366    }
1367
1368    fn persistent_name() -> &'static str {
1369        "TerminalPanel"
1370    }
1371
1372    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1373        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1374            && TerminalSettings::get_global(cx).button
1375        {
1376            Some(IconName::Terminal)
1377        } else {
1378            None
1379        }
1380    }
1381
1382    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1383        Some("Terminal Panel")
1384    }
1385
1386    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1387        Box::new(ToggleFocus)
1388    }
1389
1390    fn pane(&self) -> Option<Entity<Pane>> {
1391        Some(self.active_pane.clone())
1392    }
1393
1394    fn activation_priority(&self) -> u32 {
1395        1
1396    }
1397}
1398
1399struct InlineAssistTabBarButton {
1400    focus_handle: FocusHandle,
1401}
1402
1403impl Render for InlineAssistTabBarButton {
1404    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1405        let focus_handle = self.focus_handle.clone();
1406        IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
1407            .icon_size(IconSize::Small)
1408            .on_click(cx.listener(|_, _, window, cx| {
1409                window.dispatch_action(InlineAssist::default().boxed_clone(), cx);
1410            }))
1411            .tooltip(move |window, cx| {
1412                Tooltip::for_action_in(
1413                    "Inline Assist",
1414                    &InlineAssist::default(),
1415                    &focus_handle,
1416                    window,
1417                    cx,
1418                )
1419            })
1420    }
1421}