Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:19:24.011Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

terminal_panel.rs

2533 lines · 95.7 KB · rust
1use std::{cmp, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration};
2
3use crate::{
4    TerminalView, default_working_directory,
5    persistence::{
6        SerializedItems, SerializedTerminalPanel, deserialize_terminal_panel, serialize_pane_group,
7    },
8};
9use breadcrumbs::Breadcrumbs;
10use collections::HashMap;
11use db::kvp::KeyValueStore;
12use futures::{channel::oneshot, future::join_all};
13use gpui::{
14    Action, Anchor, App, AsyncApp, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
15    Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Task, TaskExt, WeakEntity,
16    Window, actions,
17};
18use itertools::Itertools;
19use project::{Fs, Project};
20
21use settings::{Settings, TerminalDockPosition};
22use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
23use terminal::{Terminal, terminal_settings::TerminalSettings};
24use ui::{
25    ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable,
26    Tooltip, prelude::*,
27};
28use util::{ResultExt, TryFutureExt};
29use workspace::{
30    ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
31    ActivatePaneUp, ActivatePreviousPane, DraggedTab, ItemId, MoveItemToPane,
32    MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, Pane,
33    PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, SwapPaneDown,
34    SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
35    dock::{DockPosition, Panel, PanelEvent, PanelHandle},
36    item::SerializableItem,
37    move_active_item, pane,
38};
39
40use anyhow::{Result, anyhow};
41use zed_actions::assistant::InlineAssist;
42
43const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
44
45actions!(
46    terminal_panel,
47    [
48        /// Toggles the terminal panel.
49        Toggle,
50        /// Toggles focus on the terminal panel.
51        ToggleFocus
52    ]
53);
54
55pub fn init(cx: &mut App) {
56    cx.observe_new(
57        |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
58            workspace.register_action(TerminalPanel::new_terminal);
59            workspace.register_action(TerminalPanel::open_terminal);
60            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
61                if is_enabled_in_workspace(workspace, cx) {
62                    workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
63                }
64            });
65            workspace.register_action(|workspace, _: &Toggle, window, cx| {
66                if is_enabled_in_workspace(workspace, cx) {
67                    if !workspace.toggle_panel_focus::<TerminalPanel>(window, cx) {
68                        workspace.close_panel::<TerminalPanel>(window, cx);
69                    }
70                }
71            });
72        },
73    )
74    .detach();
75}
76
77pub struct TerminalPanel {
78    pub(crate) active_pane: Entity<Pane>,
79    pub(crate) center: PaneGroup,
80    fs: Arc<dyn Fs>,
81    workspace: WeakEntity<Workspace>,
82    pending_serialization: Task<Option<()>>,
83    pending_terminals_to_add: usize,
84    deferred_tasks: HashMap<TaskId, Task<()>>,
85    assistant_enabled: bool,
86    active: bool,
87}
88
89impl TerminalPanel {
90    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
91        let project = workspace.project();
92        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);
93        let center = PaneGroup::new(pane.clone());
94        let terminal_panel = Self {
95            center,
96            active_pane: pane,
97            fs: workspace.app_state().fs.clone(),
98            workspace: workspace.weak_handle(),
99            pending_serialization: Task::ready(None),
100            pending_terminals_to_add: 0,
101            deferred_tasks: HashMap::default(),
102            assistant_enabled: false,
103            active: false,
104        };
105        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
106        terminal_panel
107    }
108
109    pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
110        self.assistant_enabled = enabled;
111        for pane in self.center.panes() {
112            self.apply_tab_bar_buttons(pane, cx);
113        }
114    }
115
116    pub(crate) fn apply_tab_bar_buttons(
117        &self,
118        terminal_pane: &Entity<Pane>,
119        cx: &mut Context<Self>,
120    ) {
121        let assistant_enabled = self.assistant_enabled;
122        terminal_pane.update(cx, |pane, cx| {
123            pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
124                let split_context = pane
125                    .active_item()
126                    .and_then(|item| item.downcast::<TerminalView>())
127                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());
128                let has_focused_rename_editor = pane
129                    .active_item()
130                    .and_then(|item| item.downcast::<TerminalView>())
131                    .is_some_and(|view| view.read(cx).rename_editor_is_focused(window, cx));
132                if !pane.has_focus(window, cx)
133                    && !pane.context_menu_focused(window, cx)
134                    && !has_focused_rename_editor
135                {
136                    return (None, None);
137                }
138                let focus_handle = pane.focus_handle(cx);
139                let right_children = h_flex()
140                    .gap(DynamicSpacing::Base02.rems(cx))
141                    .child(
142                        PopoverMenu::new("terminal-tab-bar-popover-menu")
143                            .trigger_with_tooltip(
144                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
145                                Tooltip::text("New…"),
146                            )
147                            .anchor(Anchor::TopRight)
148                            .with_handle(pane.new_item_context_menu_handle.clone())
149                            .menu(move |window, cx| {
150                                let focus_handle = focus_handle.clone();
151                                let menu = ContextMenu::build(window, cx, |menu, _, _| {
152                                    menu.context(focus_handle.clone())
153                                        .action(
154                                            "New Terminal",
155                                            workspace::NewTerminal::default().boxed_clone(),
156                                        )
157                                        // We want the focus to go back to terminal panel once task modal is dismissed,
158                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
159                                        // context menu will be gone the moment we spawn the modal.
160                                        .action(
161                                            "Spawn Task",
162                                            zed_actions::Spawn::modal().boxed_clone(),
163                                        )
164                                });
165
166                                Some(menu)
167                            }),
168                    )
169                    .when(assistant_enabled, |this| {
170                        this.when_some(split_context.clone(), |this, focus_handle| {
171                            this.child(InlineAssistTabBarButton { focus_handle })
172                        })
173                    })
174                    .child(
175                        PopoverMenu::new("terminal-pane-tab-bar-split")
176                            .trigger_with_tooltip(
177                                IconButton::new("terminal-pane-split", IconName::Split)
178                                    .icon_size(IconSize::Small),
179                                Tooltip::text("Split Pane"),
180                            )
181                            .anchor(Anchor::TopRight)
182                            .with_handle(pane.split_item_context_menu_handle.clone())
183                            .menu({
184                                move |window, cx| {
185                                    ContextMenu::build(window, cx, |menu, _, _| {
186                                        menu.when_some(
187                                            split_context.clone(),
188                                            |menu, split_context| menu.context(split_context),
189                                        )
190                                        .action("Split Right", SplitRight::default().boxed_clone())
191                                        .action("Split Left", SplitLeft::default().boxed_clone())
192                                        .action("Split Up", SplitUp::default().boxed_clone())
193                                        .action("Split Down", SplitDown::default().boxed_clone())
194                                    })
195                                    .into()
196                                }
197                            }),
198                    )
199                    .child({
200                        let zoomed = pane.is_zoomed();
201                        IconButton::new("toggle_zoom", IconName::Maximize)
202                            .icon_size(IconSize::Small)
203                            .toggle_state(zoomed)
204                            .selected_icon(IconName::Minimize)
205                            .on_click(cx.listener(|pane, _, window, cx| {
206                                pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
207                            }))
208                            .tooltip(move |_window, cx| {
209                                Tooltip::for_action(
210                                    if zoomed { "Zoom Out" } else { "Zoom In" },
211                                    &ToggleZoom,
212                                    cx,
213                                )
214                            })
215                    })
216                    .into_any_element()
217                    .into();
218                (None, right_children)
219            });
220        });
221    }
222
223    fn serialization_key(workspace: &Workspace) -> Option<String> {
224        workspace
225            .database_id()
226            .map(|id| i64::from(id).to_string())
227            .or(workspace.session_id())
228            .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))
229    }
230
231    pub async fn load(
232        workspace: WeakEntity<Workspace>,
233        mut cx: AsyncWindowContext,
234    ) -> Result<Entity<Self>> {
235        let mut terminal_panel = None;
236
237        if let Some((database_id, serialization_key, kvp)) = workspace
238            .read_with(&cx, |workspace, cx| {
239                workspace
240                    .database_id()
241                    .zip(TerminalPanel::serialization_key(workspace))
242                    .map(|(id, key)| (id, key, KeyValueStore::global(cx)))
243            })
244            .ok()
245            .flatten()
246            && let Some(serialized_panel) = cx
247                .background_spawn(async move { kvp.read_kvp(&serialization_key) })
248                .await
249                .log_err()
250                .flatten()
251                .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
252                .transpose()
253                .log_err()
254                .flatten()
255            && let Ok(serialized) = workspace
256                .update_in(&mut cx, |workspace, window, cx| {
257                    deserialize_terminal_panel(
258                        workspace.weak_handle(),
259                        workspace.project().clone(),
260                        database_id,
261                        serialized_panel,
262                        window,
263                        cx,
264                    )
265                })?
266                .await
267        {
268            terminal_panel = Some(serialized);
269        }
270
271        let terminal_panel = if let Some(panel) = terminal_panel {
272            panel
273        } else {
274            workspace.update_in(&mut cx, |workspace, window, cx| {
275                cx.new(|cx| TerminalPanel::new(workspace, window, cx))
276            })?
277        };
278
279        if let Some(workspace) = workspace.upgrade() {
280            workspace.update(&mut cx, |workspace, _| {
281                workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone()))
282            });
283        }
284
285        // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
286        if let Some(workspace) = workspace.upgrade() {
287            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {
288                let alive_item_ids = terminal_panel
289                    .read(cx)
290                    .center
291                    .panes()
292                    .into_iter()
293                    .flat_map(|pane| pane.read(cx).items())
294                    .map(|item| item.item_id().as_u64() as ItemId)
295                    .collect();
296                workspace.database_id().map(|workspace_id| {
297                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)
298                })
299            })?;
300            if let Some(task) = cleanup_task {
301                task.await.log_err();
302            }
303        }
304
305        if let Some(workspace) = workspace.upgrade() {
306            let should_focus = workspace
307                .update_in(&mut cx, |workspace, window, cx| {
308                    workspace.active_item(cx).is_none()
309                        && workspace
310                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)
311                })
312                .unwrap_or(false);
313
314            if should_focus {
315                terminal_panel
316                    .update_in(&mut cx, |panel, window, cx| {
317                        panel.active_pane.update(cx, |pane, cx| {
318                            pane.focus_active_item(window, cx);
319                        });
320                    })
321                    .ok();
322            }
323        }
324        Ok(terminal_panel)
325    }
326
327    fn handle_pane_event(
328        &mut self,
329        pane: &Entity<Pane>,
330        event: &pane::Event,
331        window: &mut Window,
332        cx: &mut Context<Self>,
333    ) {
334        match event {
335            pane::Event::ActivateItem { .. } => self.serialize(cx),
336            pane::Event::RemovedItem { .. } => self.serialize(cx),
337            pane::Event::Remove { focus_on_pane } => {
338                let pane_count_before_removal = self.center.panes().len();
339                let _removal_result = self.center.remove(pane, cx);
340                if pane_count_before_removal == 1 {
341                    self.center.first_pane().update(cx, |pane, cx| {
342                        pane.set_zoomed(false, cx);
343                    });
344                    cx.emit(PanelEvent::Close);
345                } else if let Some(focus_on_pane) =
346                    focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
347                {
348                    focus_on_pane.focus_handle(cx).focus(window, cx);
349                }
350            }
351            pane::Event::ZoomIn => {
352                for pane in self.center.panes() {
353                    pane.update(cx, |pane, cx| {
354                        pane.set_zoomed(true, cx);
355                    })
356                }
357                cx.emit(PanelEvent::ZoomIn);
358                cx.notify();
359            }
360            pane::Event::ZoomOut => {
361                for pane in self.center.panes() {
362                    pane.update(cx, |pane, cx| {
363                        pane.set_zoomed(false, cx);
364                    })
365                }
366                cx.emit(PanelEvent::ZoomOut);
367                cx.notify();
368            }
369            pane::Event::AddItem { item } => {
370                if let Some(workspace) = self.workspace.upgrade() {
371                    workspace.update(cx, |workspace, cx| {
372                        item.added_to_pane(workspace, pane.clone(), window, cx)
373                    })
374                }
375                self.serialize(cx);
376            }
377            &pane::Event::Split { direction, mode } => {
378                match mode {
379                    SplitMode::ClonePane | SplitMode::EmptyPane => {
380                        let clone = matches!(mode, SplitMode::ClonePane);
381                        let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
382                        let pane = pane.clone();
383                        cx.spawn_in(window, async move |panel, cx| {
384                            let Some(new_pane) = new_pane.await else {
385                                return;
386                            };
387                            panel
388                                .update_in(cx, |panel, window, cx| {
389                                    panel.center.split(&pane, &new_pane, direction, cx);
390                                    window.focus(&new_pane.focus_handle(cx), cx);
391                                })
392                                .ok();
393                        })
394                        .detach();
395                    }
396                    SplitMode::MovePane => {
397                        let Some(item) =
398                            pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
399                        else {
400                            return;
401                        };
402                        let Ok(project) = self
403                            .workspace
404                            .update(cx, |workspace, _| workspace.project().clone())
405                        else {
406                            return;
407                        };
408                        let new_pane =
409                            new_terminal_pane(self.workspace.clone(), project, false, window, cx);
410                        new_pane.update(cx, |pane, cx| {
411                            pane.add_item(item, true, true, None, window, cx);
412                        });
413                        self.center.split(&pane, &new_pane, direction, cx);
414                        window.focus(&new_pane.focus_handle(cx), cx);
415                    }
416                };
417            }
418            pane::Event::Focus => {
419                self.active_pane = pane.clone();
420            }
421            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {
422                self.serialize(cx);
423            }
424
425            _ => {}
426        }
427    }
428
429    fn new_pane_with_active_terminal(
430        &mut self,
431        clone: bool,
432        window: &mut Window,
433        cx: &mut Context<Self>,
434    ) -> Task<Option<Entity<Pane>>> {
435        let Some(workspace) = self.workspace.upgrade() else {
436            return Task::ready(None);
437        };
438        let workspace = workspace.read(cx);
439        let database_id = workspace.database_id();
440        let weak_workspace = self.workspace.clone();
441        let project = workspace.project().clone();
442        let active_pane = &self.active_pane;
443        let terminal_view = if clone {
444            active_pane
445                .read(cx)
446                .active_item()
447                .and_then(|item| item.downcast::<TerminalView>())
448        } else {
449            None
450        };
451        let working_directory = if clone {
452            terminal_view
453                .as_ref()
454                .and_then(|terminal_view| {
455                    terminal_view
456                        .read(cx)
457                        .terminal()
458                        .read(cx)
459                        .working_directory()
460                })
461                .or_else(|| default_working_directory(workspace, cx))
462        } else {
463            default_working_directory(workspace, cx)
464        };
465
466        let is_zoomed = if clone {
467            active_pane.read(cx).is_zoomed()
468        } else {
469            false
470        };
471        cx.spawn_in(window, async move |panel, cx| {
472            let terminal = project
473                .update(cx, |project, cx| match terminal_view {
474                    Some(view) => project.clone_terminal(
475                        &view.read(cx).terminal.clone(),
476                        cx,
477                        working_directory,
478                    ),
479                    None => project.create_terminal_shell(working_directory, cx),
480                })
481                .await
482                .log_err()?;
483
484            panel
485                .update_in(cx, move |terminal_panel, window, cx| {
486                    let terminal_view = Box::new(cx.new(|cx| {
487                        TerminalView::new(
488                            terminal.clone(),
489                            weak_workspace.clone(),
490                            database_id,
491                            project.downgrade(),
492                            window,
493                            cx,
494                        )
495                    }));
496                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
497                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
498                    pane.update(cx, |pane, cx| {
499                        pane.add_item(terminal_view, true, true, None, window, cx);
500                    });
501                    Some(pane)
502                })
503                .ok()
504                .flatten()
505        })
506    }
507
508    pub fn open_terminal(
509        workspace: &mut Workspace,
510        action: &workspace::OpenTerminal,
511        window: &mut Window,
512        cx: &mut Context<Workspace>,
513    ) {
514        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
515            return;
516        };
517
518        terminal_panel
519            .update(cx, |panel, cx| {
520                if action.local {
521                    panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
522                } else {
523                    panel.add_terminal_shell(
524                        Some(action.working_directory.clone()),
525                        RevealStrategy::Always,
526                        window,
527                        cx,
528                    )
529                }
530            })
531            .detach_and_log_err(cx);
532    }
533
534    pub fn spawn_task(
535        &mut self,
536        task: &SpawnInTerminal,
537        window: &mut Window,
538        cx: &mut Context<Self>,
539    ) -> Task<Result<WeakEntity<Terminal>>> {
540        let Some(workspace) = self.workspace.upgrade() else {
541            return Task::ready(Err(anyhow!("failed to read workspace")));
542        };
543
544        let project = workspace.read(cx).project().read(cx);
545
546        if project.is_via_collab() {
547            return Task::ready(Err(anyhow!("cannot spawn tasks as a guest")));
548        }
549
550        let remote_client = project.remote_client();
551        let is_windows = project.path_style(cx).is_windows();
552        let remote_shell = remote_client
553            .as_ref()
554            .and_then(|remote_client| remote_client.read(cx).shell());
555
556        let shell = if let Some(remote_shell) = remote_shell
557            && task.shell == Shell::System
558        {
559            Shell::Program(remote_shell)
560        } else {
561            task.shell.clone()
562        };
563
564        let task = prepare_task_for_spawn(task, &shell, is_windows);
565
566        if task.allow_concurrent_runs && task.use_new_terminal {
567            return self.spawn_in_new_terminal(task, window, cx);
568        }
569
570        let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx);
571        let Some(existing) = terminals_for_task.pop() else {
572            return self.spawn_in_new_terminal(task, window, cx);
573        };
574
575        let (existing_item_index, task_pane, existing_terminal) = existing;
576        if task.allow_concurrent_runs {
577            return self.replace_terminal(
578                task,
579                task_pane,
580                existing_item_index,
581                existing_terminal,
582                window,
583                cx,
584            );
585        }
586
587        let (tx, rx) = oneshot::channel();
588
589        self.deferred_tasks.insert(
590            task.id.clone(),
591            cx.spawn_in(window, async move |terminal_panel, cx| {
592                wait_for_terminals_tasks(terminals_for_task, cx).await;
593                let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
594                    if task.use_new_terminal {
595                        terminal_panel.spawn_in_new_terminal(task, window, cx)
596                    } else {
597                        terminal_panel.replace_terminal(
598                            task,
599                            task_pane,
600                            existing_item_index,
601                            existing_terminal,
602                            window,
603                            cx,
604                        )
605                    }
606                });
607                if let Ok(task) = task {
608                    tx.send(task.await).ok();
609                }
610            }),
611        );
612
613        cx.spawn(async move |_, _| rx.await?)
614    }
615
616    fn spawn_in_new_terminal(
617        &mut self,
618        spawn_task: SpawnInTerminal,
619        window: &mut Window,
620        cx: &mut Context<Self>,
621    ) -> Task<Result<WeakEntity<Terminal>>> {
622        let reveal = spawn_task.reveal;
623        let reveal_target = spawn_task.reveal_target;
624        match reveal_target {
625            RevealTarget::Center => self
626                .workspace
627                .update(cx, |workspace, cx| {
628                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
629                        project.create_terminal_task(spawn_task, cx)
630                    })
631                })
632                .unwrap_or_else(|e| Task::ready(Err(e))),
633            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
634        }
635    }
636
637    /// Create a new Terminal in the current working directory or the user's home directory
638    fn new_terminal(
639        workspace: &mut Workspace,
640        action: &workspace::NewTerminal,
641        window: &mut Window,
642        cx: &mut Context<Workspace>,
643    ) {
644        let center_pane = workspace.active_pane();
645        let center_pane_has_focus = center_pane.focus_handle(cx).contains_focused(window, cx);
646        let active_center_item_is_terminal = center_pane
647            .read(cx)
648            .active_item()
649            .is_some_and(|item| item.downcast::<TerminalView>().is_some());
650
651        if center_pane_has_focus && active_center_item_is_terminal {
652            let working_directory = default_working_directory(workspace, cx);
653            let local = action.local;
654            Self::add_center_terminal(workspace, window, cx, move |project, cx| {
655                if local {
656                    project.create_local_terminal(cx)
657                } else {
658                    project.create_terminal_shell(working_directory, cx)
659                }
660            })
661            .detach_and_log_err(cx);
662            return;
663        }
664
665        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
666            return;
667        };
668
669        terminal_panel
670            .update(cx, |this, cx| {
671                if action.local {
672                    this.add_local_terminal_shell(RevealStrategy::Always, window, cx)
673                } else {
674                    this.add_terminal_shell(
675                        default_working_directory(workspace, cx),
676                        RevealStrategy::Always,
677                        window,
678                        cx,
679                    )
680                }
681            })
682            .detach_and_log_err(cx);
683    }
684
685    fn terminals_for_task(
686        &self,
687        label: &str,
688        cx: &mut App,
689    ) -> Vec<(usize, Entity<Pane>, Entity<TerminalView>)> {
690        let Some(workspace) = self.workspace.upgrade() else {
691            return Vec::new();
692        };
693
694        let pane_terminal_views = |pane: Entity<Pane>| {
695            pane.read(cx)
696                .items()
697                .enumerate()
698                .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
699                .filter_map(|(index, terminal_view)| {
700                    let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
701                    if &task_state.spawned_task.full_label == label {
702                        Some((index, terminal_view))
703                    } else {
704                        None
705                    }
706                })
707                .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
708        };
709
710        self.center
711            .panes()
712            .into_iter()
713            .cloned()
714            .flat_map(pane_terminal_views)
715            .chain(
716                workspace
717                    .read(cx)
718                    .panes()
719                    .iter()
720                    .cloned()
721                    .flat_map(pane_terminal_views),
722            )
723            .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
724            .collect()
725    }
726
727    fn activate_terminal_view(
728        &self,
729        pane: &Entity<Pane>,
730        item_index: usize,
731        focus: bool,
732        window: &mut Window,
733        cx: &mut App,
734    ) {
735        pane.update(cx, |pane, cx| {
736            pane.activate_item(item_index, true, focus, window, cx)
737        })
738    }
739
740    pub fn add_center_terminal(
741        workspace: &mut Workspace,
742        window: &mut Window,
743        cx: &mut Context<Workspace>,
744        create_terminal: impl FnOnce(
745            &mut Project,
746            &mut Context<Project>,
747        ) -> Task<Result<Entity<Terminal>>>
748        + 'static,
749    ) -> Task<Result<WeakEntity<Terminal>>> {
750        if !is_enabled_in_workspace(workspace, cx) {
751            return Task::ready(Err(anyhow!(
752                "terminal not yet supported for remote projects"
753            )));
754        }
755        let project = workspace.project().downgrade();
756        cx.spawn_in(window, async move |workspace, cx| {
757            let terminal = project.update(cx, create_terminal)?.await?;
758
759            workspace.update_in(cx, |workspace, window, cx| {
760                let terminal_view = cx.new(|cx| {
761                    TerminalView::new(
762                        terminal.clone(),
763                        workspace.weak_handle(),
764                        workspace.database_id(),
765                        workspace.project().downgrade(),
766                        window,
767                        cx,
768                    )
769                });
770                // Don't steal focus from an open modal (e.g. the command palette):
771                // a background terminal can finish starting up after the user has
772                // moved on, and focusing it would dismiss whatever they opened.
773                let focus_item = !workspace.has_active_modal(window, cx);
774                workspace.add_item_to_active_pane(
775                    Box::new(terminal_view),
776                    None,
777                    focus_item,
778                    window,
779                    cx,
780                );
781            })?;
782            Ok(terminal.downgrade())
783        })
784    }
785
786    pub fn add_terminal_task(
787        &mut self,
788        task: SpawnInTerminal,
789        reveal_strategy: RevealStrategy,
790        window: &mut Window,
791        cx: &mut Context<Self>,
792    ) -> Task<Result<WeakEntity<Terminal>>> {
793        let workspace = self.workspace.clone();
794        cx.spawn_in(window, async move |terminal_panel, cx| {
795            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
796                anyhow::bail!("terminal not yet supported for remote projects");
797            }
798            let pane = terminal_panel.update(cx, |terminal_panel, _| {
799                terminal_panel.pending_terminals_to_add += 1;
800                terminal_panel.active_pane.clone()
801            })?;
802            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
803            let terminal = project
804                .update(cx, |project, cx| project.create_terminal_task(task, cx))
805                .await?;
806            let result = workspace.update_in(cx, |workspace, window, cx| {
807                let terminal_view = Box::new(cx.new(|cx| {
808                    TerminalView::new(
809                        terminal.clone(),
810                        workspace.weak_handle(),
811                        workspace.database_id(),
812                        workspace.project().downgrade(),
813                        window,
814                        cx,
815                    )
816                }));
817
818                match reveal_strategy {
819                    RevealStrategy::Always => {
820                        workspace.focus_panel::<Self>(window, cx);
821                    }
822                    RevealStrategy::NoFocus => {
823                        workspace.open_panel::<Self>(window, cx);
824                    }
825                    RevealStrategy::Never => {}
826                }
827
828                pane.update(cx, |pane, cx| {
829                    let focus = matches!(reveal_strategy, RevealStrategy::Always);
830                    pane.add_item(terminal_view, true, focus, None, window, cx);
831                });
832
833                Ok(terminal.downgrade())
834            })?;
835            terminal_panel.update(cx, |terminal_panel, cx| {
836                terminal_panel.pending_terminals_to_add =
837                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
838                terminal_panel.serialize(cx)
839            })?;
840            result
841        })
842    }
843
844    fn add_terminal_shell(
845        &mut self,
846        cwd: Option<PathBuf>,
847        reveal_strategy: RevealStrategy,
848        window: &mut Window,
849        cx: &mut Context<Self>,
850    ) -> Task<Result<WeakEntity<Terminal>>> {
851        self.add_terminal_shell_internal(false, cwd, reveal_strategy, window, cx)
852    }
853
854    fn add_local_terminal_shell(
855        &mut self,
856        reveal_strategy: RevealStrategy,
857        window: &mut Window,
858        cx: &mut Context<Self>,
859    ) -> Task<Result<WeakEntity<Terminal>>> {
860        self.add_terminal_shell_internal(true, None, reveal_strategy, window, cx)
861    }
862
863    fn add_terminal_shell_internal(
864        &mut self,
865        force_local: bool,
866        cwd: Option<PathBuf>,
867        reveal_strategy: RevealStrategy,
868        window: &mut Window,
869        cx: &mut Context<Self>,
870    ) -> Task<Result<WeakEntity<Terminal>>> {
871        let workspace = self.workspace.clone();
872
873        cx.spawn_in(window, async move |terminal_panel, cx| {
874            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
875                anyhow::bail!("terminal not yet supported for collaborative projects");
876            }
877            let pane = terminal_panel.update(cx, |terminal_panel, _| {
878                terminal_panel.pending_terminals_to_add += 1;
879                terminal_panel.active_pane.clone()
880            })?;
881            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
882            let terminal = if force_local {
883                project
884                    .update(cx, |project, cx| project.create_local_terminal(cx))
885                    .await
886            } else {
887                project
888                    .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))
889                    .await
890            };
891
892            match terminal {
893                Ok(terminal) => {
894                    let result = workspace.update_in(cx, |workspace, window, cx| {
895                        let terminal_view = Box::new(cx.new(|cx| {
896                            TerminalView::new(
897                                terminal.clone(),
898                                workspace.weak_handle(),
899                                workspace.database_id(),
900                                workspace.project().downgrade(),
901                                window,
902                                cx,
903                            )
904                        }));
905
906                        match reveal_strategy {
907                            RevealStrategy::Always => {
908                                workspace.focus_panel::<Self>(window, cx);
909                            }
910                            RevealStrategy::NoFocus => {
911                                workspace.open_panel::<Self>(window, cx);
912                            }
913                            RevealStrategy::Never => {}
914                        }
915
916                        pane.update(cx, |pane, cx| {
917                            let focus = matches!(reveal_strategy, RevealStrategy::Always);
918                            pane.add_item(terminal_view, true, focus, None, window, cx);
919                        });
920
921                        Ok(terminal.downgrade())
922                    })?;
923                    terminal_panel.update(cx, |terminal_panel, cx| {
924                        terminal_panel.pending_terminals_to_add =
925                            terminal_panel.pending_terminals_to_add.saturating_sub(1);
926                        terminal_panel.serialize(cx)
927                    })?;
928                    result
929                }
930                Err(error) => {
931                    pane.update_in(cx, |pane, window, cx| {
932                        let focus = pane.has_focus(window, cx);
933                        let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal {
934                            error: error.to_string(),
935                            focus_handle: cx.focus_handle(),
936                        });
937                        pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx);
938                    })?;
939                    Err(error)
940                }
941            }
942        })
943    }
944
945    fn serialize(&mut self, cx: &mut Context<Self>) {
946        let Some(serialization_key) = self
947            .workspace
948            .read_with(cx, |workspace, _| {
949                TerminalPanel::serialization_key(workspace)
950            })
951            .ok()
952            .flatten()
953        else {
954            return;
955        };
956        let kvp = KeyValueStore::global(cx);
957        self.pending_serialization = cx.spawn(async move |terminal_panel, cx| {
958            cx.background_executor()
959                .timer(Duration::from_millis(50))
960                .await;
961            let terminal_panel = terminal_panel.upgrade()?;
962            let items = terminal_panel.update(cx, |terminal_panel, cx| {
963                SerializedItems::WithSplits(serialize_pane_group(
964                    &terminal_panel.center,
965                    &terminal_panel.active_pane,
966                    cx,
967                ))
968            });
969            cx.background_spawn(
970                async move {
971                    kvp.write_kvp(
972                        serialization_key,
973                        serde_json::to_string(&SerializedTerminalPanel {
974                            items,
975                            active_item_id: None,
976                        })?,
977                    )
978                    .await?;
979                    anyhow::Ok(())
980                }
981                .log_err(),
982            )
983            .await;
984            Some(())
985        });
986    }
987
988    fn replace_terminal(
989        &self,
990        spawn_task: SpawnInTerminal,
991        task_pane: Entity<Pane>,
992        terminal_item_index: usize,
993        terminal_to_replace: Entity<TerminalView>,
994        window: &mut Window,
995        cx: &mut Context<Self>,
996    ) -> Task<Result<WeakEntity<Terminal>>> {
997        let reveal = spawn_task.reveal;
998        let task_workspace = self.workspace.clone();
999        cx.spawn_in(window, async move |terminal_panel, cx| {
1000            let project = terminal_panel.update(cx, |this, cx| {
1001                this.workspace
1002                    .update(cx, |workspace, _| workspace.project().clone())
1003            })??;
1004            let new_terminal = project
1005                .update(cx, |project, cx| {
1006                    project.create_terminal_task(spawn_task, cx)
1007                })
1008                .await?;
1009            terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
1010                terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
1011            })?;
1012
1013            let reveal_target = terminal_panel.update(cx, |panel, _| {
1014                if panel.center.panes().iter().any(|p| **p == task_pane) {
1015                    RevealTarget::Dock
1016                } else {
1017                    RevealTarget::Center
1018                }
1019            })?;
1020
1021            match reveal {
1022                RevealStrategy::Always => match reveal_target {
1023                    RevealTarget::Center => {
1024                        task_workspace.update_in(cx, |workspace, window, cx| {
1025                            let did_activate = workspace.activate_item(
1026                                &terminal_to_replace,
1027                                true,
1028                                true,
1029                                window,
1030                                cx,
1031                            );
1032
1033                            anyhow::ensure!(did_activate, "Failed to retrieve terminal pane");
1034
1035                            anyhow::Ok(())
1036                        })??;
1037                    }
1038                    RevealTarget::Dock => {
1039                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1040                            terminal_panel.activate_terminal_view(
1041                                &task_pane,
1042                                terminal_item_index,
1043                                true,
1044                                window,
1045                                cx,
1046                            )
1047                        })?;
1048
1049                        cx.spawn(async move |cx| {
1050                            task_workspace
1051                                .update_in(cx, |workspace, window, cx| {
1052                                    workspace.focus_panel::<Self>(window, cx)
1053                                })
1054                                .ok()
1055                        })
1056                        .detach();
1057                    }
1058                },
1059                RevealStrategy::NoFocus => match reveal_target {
1060                    RevealTarget::Center => {
1061                        task_workspace.update_in(cx, |workspace, window, cx| {
1062                            workspace.active_pane().focus_handle(cx).focus(window, cx);
1063                        })?;
1064                    }
1065                    RevealTarget::Dock => {
1066                        terminal_panel.update_in(cx, |terminal_panel, window, cx| {
1067                            terminal_panel.activate_terminal_view(
1068                                &task_pane,
1069                                terminal_item_index,
1070                                false,
1071                                window,
1072                                cx,
1073                            )
1074                        })?;
1075
1076                        cx.spawn(async move |cx| {
1077                            task_workspace
1078                                .update_in(cx, |workspace, window, cx| {
1079                                    workspace.open_panel::<Self>(window, cx)
1080                                })
1081                                .ok()
1082                        })
1083                        .detach();
1084                    }
1085                },
1086                RevealStrategy::Never => {}
1087            }
1088
1089            Ok(new_terminal.downgrade())
1090        })
1091    }
1092
1093    fn has_no_terminals(&self, cx: &App) -> bool {
1094        self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
1095    }
1096
1097    pub fn assistant_enabled(&self) -> bool {
1098        self.assistant_enabled
1099    }
1100
1101    /// Returns all panes in the terminal panel.
1102    pub fn panes(&self) -> Vec<&Entity<Pane>> {
1103        self.center.panes()
1104    }
1105
1106    /// Returns all non-empty terminal selections from all terminal views in all panes.
1107    pub fn terminal_selections(&self, cx: &App) -> Vec<String> {
1108        self.center
1109            .panes()
1110            .iter()
1111            .flat_map(|pane| {
1112                pane.read(cx).items().filter_map(|item| {
1113                    let terminal_view = item.downcast::<crate::TerminalView>()?;
1114                    terminal_view
1115                        .read(cx)
1116                        .terminal()
1117                        .read(cx)
1118                        .last_content
1119                        .selection_text
1120                        .clone()
1121                        .filter(|text| !text.is_empty())
1122                })
1123            })
1124            .collect()
1125    }
1126
1127    fn is_enabled(&self, cx: &App) -> bool {
1128        self.workspace
1129            .upgrade()
1130            .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx))
1131    }
1132
1133    fn activate_pane_in_direction(
1134        &mut self,
1135        direction: SplitDirection,
1136        window: &mut Window,
1137        cx: &mut Context<Self>,
1138    ) {
1139        if let Some(pane) = self
1140            .center
1141            .find_pane_in_direction(&self.active_pane, direction, cx)
1142        {
1143            window.focus(&pane.focus_handle(cx), cx);
1144        } else {
1145            self.workspace
1146                .update(cx, |workspace, cx| {
1147                    workspace.activate_pane_in_direction(direction, window, cx)
1148                })
1149                .ok();
1150        }
1151    }
1152
1153    fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1154        if let Some(to) = self
1155            .center
1156            .find_pane_in_direction(&self.active_pane, direction, cx)
1157            .cloned()
1158        {
1159            self.center.swap(&self.active_pane, &to, cx);
1160            cx.notify();
1161        }
1162    }
1163
1164    fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
1165        if self
1166            .center
1167            .move_to_border(&self.active_pane, direction, cx)
1168            .unwrap()
1169        {
1170            cx.notify();
1171        }
1172    }
1173}
1174
1175/// Prepares a `SpawnInTerminal` by computing the command, args, and command_label
1176/// based on the shell configuration. This is a pure function that can be tested
1177/// without spawning actual terminals.
1178pub fn prepare_task_for_spawn(
1179    task: &SpawnInTerminal,
1180    shell: &Shell,
1181    is_windows: bool,
1182) -> SpawnInTerminal {
1183    let builder = ShellBuilder::new(shell, is_windows);
1184    let command_label = builder.command_label(task.command.as_deref().unwrap_or(""));
1185    let (command, args) = builder.build_no_quote(task.command.clone(), &task.args);
1186
1187    SpawnInTerminal {
1188        command_label,
1189        command: Some(command),
1190        args,
1191        ..task.clone()
1192    }
1193}
1194
1195fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool {
1196    workspace.project().read(cx).supports_terminal(cx)
1197}
1198
1199pub fn new_terminal_pane(
1200    workspace: WeakEntity<Workspace>,
1201    project: Entity<Project>,
1202    zoomed: bool,
1203    window: &mut Window,
1204    cx: &mut Context<TerminalPanel>,
1205) -> Entity<Pane> {
1206    let terminal_panel = cx.entity();
1207    let pane = cx.new(|cx| {
1208        let mut pane = Pane::new(
1209            workspace.clone(),
1210            project.clone(),
1211            Default::default(),
1212            None,
1213            workspace::NewTerminal::default().boxed_clone(),
1214            false,
1215            window,
1216            cx,
1217        );
1218        pane.set_zoomed(zoomed, cx);
1219        pane.set_can_navigate(false, cx);
1220        pane.display_nav_history_buttons(None);
1221        pane.set_should_display_tab_bar(|_, _| true);
1222        pane.set_zoom_out_on_close(false);
1223
1224        let split_closure_terminal_panel = terminal_panel.downgrade();
1225        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
1226            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
1227                let is_current_pane = tab.pane == cx.entity();
1228                let Some(can_drag_away) = split_closure_terminal_panel
1229                    .read_with(cx, |terminal_panel, _| {
1230                        let current_panes = terminal_panel.center.panes();
1231                        !current_panes.contains(&&tab.pane)
1232                            || current_panes.len() > 1
1233                            || (!is_current_pane || pane.items_len() > 1)
1234                    })
1235                    .ok()
1236                else {
1237                    return false;
1238                };
1239                if can_drag_away {
1240                    let item = if is_current_pane {
1241                        pane.item_for_index(tab.ix)
1242                    } else {
1243                        tab.pane.read(cx).item_for_index(tab.ix)
1244                    };
1245                    if let Some(item) = item {
1246                        return item.downcast::<TerminalView>().is_some();
1247                    }
1248                }
1249            }
1250            false
1251        })));
1252
1253        let toolbar = pane.toolbar().clone();
1254        if let Some(callbacks) = cx.try_global::<workspace::PaneSearchBarCallbacks>() {
1255            let languages = Some(project.read(cx).languages().clone());
1256            (callbacks.setup_search_bar)(languages, &toolbar, window, cx);
1257        }
1258        let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1259        toolbar.update(cx, |toolbar, cx| {
1260            toolbar.add_item(breadcrumbs, window, cx);
1261        });
1262
1263        pane
1264    });
1265
1266    cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event)
1267        .detach();
1268    cx.observe(&pane, |_, _, cx| cx.notify()).detach();
1269
1270    pane
1271}
1272
1273async fn wait_for_terminals_tasks(
1274    terminals_for_task: Vec<(usize, Entity<Pane>, Entity<TerminalView>)>,
1275    cx: &mut AsyncApp,
1276) {
1277    let pending_tasks = terminals_for_task.iter().map(|(_, _, terminal)| {
1278        terminal.update(cx, |terminal_view, cx| {
1279            terminal_view
1280                .terminal()
1281                .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1282        })
1283    });
1284    join_all(pending_tasks).await;
1285}
1286
1287struct FailedToSpawnTerminal {
1288    error: String,
1289    focus_handle: FocusHandle,
1290}
1291
1292impl Focusable for FailedToSpawnTerminal {
1293    fn focus_handle(&self, _: &App) -> FocusHandle {
1294        self.focus_handle.clone()
1295    }
1296}
1297
1298impl Render for FailedToSpawnTerminal {
1299    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1300        let popover_menu = PopoverMenu::new("settings-popover")
1301            .trigger(
1302                IconButton::new("icon-button-popover", IconName::ChevronDown)
1303                    .icon_size(IconSize::XSmall),
1304            )
1305            .menu(move |window, cx| {
1306                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1307                    context_menu
1308                        .action("Open Settings", zed_actions::OpenSettings.boxed_clone())
1309                        .action(
1310                            "Edit settings.json",
1311                            zed_actions::OpenSettingsFile.boxed_clone(),
1312                        )
1313                }))
1314            })
1315            .anchor(Anchor::TopRight)
1316            .offset(gpui::Point {
1317                x: px(0.0),
1318                y: px(2.0),
1319            });
1320
1321        v_flex()
1322            .track_focus(&self.focus_handle)
1323            .size_full()
1324            .p_4()
1325            .items_center()
1326            .justify_center()
1327            .bg(cx.theme().colors().editor_background)
1328            .child(
1329                v_flex()
1330                    .max_w_112()
1331                    .items_center()
1332                    .justify_center()
1333                    .text_center()
1334                    .child(Label::new("Failed to spawn terminal"))
1335                    .child(
1336                        Label::new(self.error.to_string())
1337                            .size(LabelSize::Small)
1338                            .color(Color::Muted)
1339                            .mb_4(),
1340                    )
1341                    .child(SplitButton::new(
1342                        ButtonLike::new("open-settings-ui")
1343                            .child(Label::new("Edit Settings").size(LabelSize::Small))
1344                            .on_click(|_, window, cx| {
1345                                window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1346                            }),
1347                        popover_menu.into_any_element(),
1348                    )),
1349            )
1350    }
1351}
1352
1353impl EventEmitter<()> for FailedToSpawnTerminal {}
1354
1355impl workspace::Item for FailedToSpawnTerminal {
1356    type Event = ();
1357
1358    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1359        SharedString::new_static("Failed to spawn terminal")
1360    }
1361}
1362
1363impl EventEmitter<PanelEvent> for TerminalPanel {}
1364
1365impl Render for TerminalPanel {
1366    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1367        let registrar = cx
1368            .try_global::<workspace::PaneSearchBarCallbacks>()
1369            .map(|callbacks| {
1370                (callbacks.wrap_div_with_search_actions)(div(), self.active_pane.clone())
1371            })
1372            .unwrap_or_else(div);
1373        self.workspace
1374            .update(cx, |workspace, cx| {
1375                registrar.size_full().child(self.center.render(
1376                    workspace.zoomed_item(),
1377                    None,
1378                    &workspace::PaneRenderContext {
1379                        follower_states: &HashMap::default(),
1380                        active_call: workspace.active_call(),
1381                        active_pane: &self.active_pane,
1382                        app_state: workspace.app_state(),
1383                        project: workspace.project(),
1384                        workspace: &workspace.weak_handle(),
1385                    },
1386                    window,
1387                    cx,
1388                ))
1389            })
1390            .ok()
1391            .map(|div| {
1392                div.on_action({
1393                    cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| {
1394                        terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx);
1395                    })
1396                })
1397                .on_action({
1398                    cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| {
1399                        terminal_panel.activate_pane_in_direction(
1400                            SplitDirection::Right,
1401                            window,
1402                            cx,
1403                        );
1404                    })
1405                })
1406                .on_action({
1407                    cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| {
1408                        terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx);
1409                    })
1410                })
1411                .on_action({
1412                    cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| {
1413                        terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx);
1414                    })
1415                })
1416                .on_action(
1417                    cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| {
1418                        let panes = terminal_panel.center.panes();
1419                        if let Some(ix) = panes
1420                            .iter()
1421                            .position(|pane| **pane == terminal_panel.active_pane)
1422                        {
1423                            let next_ix = (ix + 1) % panes.len();
1424                            window.focus(&panes[next_ix].focus_handle(cx), cx);
1425                        }
1426                    }),
1427                )
1428                .on_action(cx.listener(
1429                    |terminal_panel, _action: &ActivatePreviousPane, window, cx| {
1430                        let panes = terminal_panel.center.panes();
1431                        if let Some(ix) = panes
1432                            .iter()
1433                            .position(|pane| **pane == terminal_panel.active_pane)
1434                        {
1435                            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1436                            window.focus(&panes[prev_ix].focus_handle(cx), cx);
1437                        }
1438                    },
1439                ))
1440                .on_action(
1441                    cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
1442                        let panes = terminal_panel.center.panes();
1443                        if let Some(&pane) = panes.get(action.0) {
1444                            window.focus(&pane.read(cx).focus_handle(cx), cx);
1445                        } else {
1446                            let future =
1447                                terminal_panel.new_pane_with_active_terminal(true, window, cx);
1448                            cx.spawn_in(window, async move |terminal_panel, cx| {
1449                                if let Some(new_pane) = future.await {
1450                                    _ = terminal_panel.update_in(
1451                                        cx,
1452                                        |terminal_panel, window, cx| {
1453                                            terminal_panel.center.split(
1454                                                &terminal_panel.active_pane,
1455                                                &new_pane,
1456                                                SplitDirection::Right,
1457                                                cx,
1458                                            );
1459                                            let new_pane = new_pane.read(cx);
1460                                            window.focus(&new_pane.focus_handle(cx), cx);
1461                                        },
1462                                    );
1463                                }
1464                            })
1465                            .detach();
1466                        }
1467                    }),
1468                )
1469                .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| {
1470                    terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx);
1471                }))
1472                .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| {
1473                    terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx);
1474                }))
1475                .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| {
1476                    terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx);
1477                }))
1478                .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| {
1479                    terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx);
1480                }))
1481                .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| {
1482                    terminal_panel.move_pane_to_border(SplitDirection::Left, cx);
1483                }))
1484                .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| {
1485                    terminal_panel.move_pane_to_border(SplitDirection::Right, cx);
1486                }))
1487                .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| {
1488                    terminal_panel.move_pane_to_border(SplitDirection::Up, cx);
1489                }))
1490                .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| {
1491                    terminal_panel.move_pane_to_border(SplitDirection::Down, cx);
1492                }))
1493                .on_action(
1494                    cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| {
1495                        let Some(&target_pane) =
1496                            terminal_panel.center.panes().get(action.destination)
1497                        else {
1498                            return;
1499                        };
1500                        move_active_item(
1501                            &terminal_panel.active_pane,
1502                            target_pane,
1503                            action.focus,
1504                            true,
1505                            window,
1506                            cx,
1507                        );
1508                    }),
1509                )
1510                .on_action(cx.listener(
1511                    |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| {
1512                        let source_pane = &terminal_panel.active_pane;
1513                        if let Some(destination_pane) = terminal_panel
1514                            .center
1515                            .find_pane_in_direction(source_pane, action.direction, cx)
1516                        {
1517                            move_active_item(
1518                                source_pane,
1519                                destination_pane,
1520                                action.focus,
1521                                true,
1522                                window,
1523                                cx,
1524                            );
1525                        };
1526                    },
1527                ))
1528            })
1529            .unwrap_or_else(|| div())
1530    }
1531}
1532
1533impl Focusable for TerminalPanel {
1534    fn focus_handle(&self, cx: &App) -> FocusHandle {
1535        self.active_pane.focus_handle(cx)
1536    }
1537}
1538
1539impl Panel for TerminalPanel {
1540    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1541        TerminalSettings::get_global(cx).dock.into()
1542    }
1543
1544    fn position_is_valid(&self, _: DockPosition) -> bool {
1545        true
1546    }
1547
1548    fn set_position(
1549        &mut self,
1550        position: DockPosition,
1551        _window: &mut Window,
1552        cx: &mut Context<Self>,
1553    ) {
1554        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1555            let dock = match position {
1556                DockPosition::Left => TerminalDockPosition::Left,
1557                DockPosition::Bottom => TerminalDockPosition::Bottom,
1558                DockPosition::Right => TerminalDockPosition::Right,
1559            };
1560            settings.terminal.get_or_insert_default().dock = Some(dock);
1561        });
1562    }
1563
1564    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
1565        let settings = TerminalSettings::get_global(cx);
1566        match self.position(window, cx) {
1567            DockPosition::Left | DockPosition::Right => settings.default_width,
1568            DockPosition::Bottom => settings.default_height,
1569        }
1570    }
1571
1572    fn supports_flexible_size(&self) -> bool {
1573        true
1574    }
1575
1576    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
1577        TerminalSettings::get_global(cx).flexible
1578    }
1579
1580    fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
1581        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1582            settings.terminal.get_or_insert_default().flexible = Some(flexible);
1583        });
1584    }
1585
1586    fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
1587        self.active_pane.read(cx).is_zoomed()
1588    }
1589
1590    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1591        for pane in self.center.panes() {
1592            pane.update(cx, |pane, cx| {
1593                pane.set_zoomed(zoomed, cx);
1594            })
1595        }
1596        cx.notify();
1597    }
1598
1599    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1600        let old_active = self.active;
1601        self.active = active;
1602        if !active || old_active == active || !self.has_no_terminals(cx) {
1603            return;
1604        }
1605        cx.defer_in(window, |this, window, cx| {
1606            let Ok(kind) = this
1607                .workspace
1608                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
1609            else {
1610                return;
1611            };
1612
1613            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
1614                .detach_and_log_err(cx)
1615        })
1616    }
1617
1618    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
1619        if !TerminalSettings::get_global(cx).show_count_badge {
1620            return None;
1621        }
1622        let count = self
1623            .center
1624            .panes()
1625            .into_iter()
1626            .map(|pane| pane.read(cx).items_len())
1627            .sum::<usize>();
1628        if count == 0 {
1629            None
1630        } else {
1631            Some(count.to_string())
1632        }
1633    }
1634
1635    fn persistent_name() -> &'static str {
1636        "TerminalPanel"
1637    }
1638
1639    fn panel_key() -> &'static str {
1640        TERMINAL_PANEL_KEY
1641    }
1642
1643    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1644        if (self.is_enabled(cx) || !self.has_no_terminals(cx))
1645            && TerminalSettings::get_global(cx).button
1646        {
1647            Some(IconName::TerminalAlt)
1648        } else {
1649            None
1650        }
1651    }
1652
1653    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1654        Some("Terminal Panel")
1655    }
1656
1657    fn toggle_action(&self) -> Box<dyn gpui::Action> {
1658        Box::new(Toggle)
1659    }
1660
1661    fn pane(&self) -> Option<Entity<Pane>> {
1662        Some(self.active_pane.clone())
1663    }
1664
1665    fn activation_priority(&self) -> u32 {
1666        2
1667    }
1668
1669    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
1670        Some(workspace::HideStatusItem::new(|settings| {
1671            settings.terminal.get_or_insert_default().button = Some(false);
1672        }))
1673    }
1674}
1675
1676struct TerminalProvider(Entity<TerminalPanel>);
1677
1678impl workspace::TerminalProvider for TerminalProvider {
1679    fn spawn(
1680        &self,
1681        task: SpawnInTerminal,
1682        window: &mut Window,
1683        cx: &mut App,
1684    ) -> Task<Option<Result<ExitStatus>>> {
1685        let terminal_panel = self.0.clone();
1686        window.spawn(cx, async move |cx| {
1687            let terminal = terminal_panel
1688                .update_in(cx, |terminal_panel, window, cx| {
1689                    terminal_panel.spawn_task(&task, window, cx)
1690                })
1691                .ok()?
1692                .await;
1693            match terminal {
1694                Ok(terminal) => {
1695                    let exit_status = terminal
1696                        .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1697                        .ok()?
1698                        .await?;
1699                    Some(Ok(exit_status))
1700                }
1701                Err(e) => Some(Err(e)),
1702            }
1703        })
1704    }
1705}
1706
1707#[derive(IntoElement)]
1708struct InlineAssistTabBarButton {
1709    focus_handle: FocusHandle,
1710}
1711
1712impl RenderOnce for InlineAssistTabBarButton {
1713    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
1714        let focus_handle = self.focus_handle;
1715        IconButton::new("terminal_inline_assistant", IconName::OmegaAssistant)
1716            .icon_size(IconSize::Small)
1717            .on_click({
1718                let focus_handle = focus_handle.clone();
1719                move |_, window, cx| {
1720                    focus_handle.dispatch_action(&InlineAssist::default(), window, cx);
1721                }
1722            })
1723            .tooltip(move |_window, cx| {
1724                Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
1725            })
1726    }
1727}
1728
1729#[cfg(test)]
1730mod tests {
1731    use std::num::NonZero;
1732
1733    use super::*;
1734    use gpui::{Modifiers, TestAppContext, UpdateGlobal as _, VisualTestContext};
1735    use pretty_assertions::assert_eq;
1736    use project::FakeFs;
1737    use settings::SettingsStore;
1738    use workspace::MultiWorkspace;
1739
1740    #[test]
1741    fn test_prepare_empty_task() {
1742        let input = SpawnInTerminal::default();
1743        let shell = Shell::System;
1744
1745        let result = prepare_task_for_spawn(&input, &shell, false);
1746
1747        let expected_shell = util::get_system_shell();
1748        assert_eq!(result.env, HashMap::default());
1749        assert_eq!(result.cwd, None);
1750        assert_eq!(result.shell, Shell::System);
1751        assert_eq!(
1752            result.command,
1753            Some(expected_shell.clone()),
1754            "Empty tasks should spawn a -i shell"
1755        );
1756        assert_eq!(result.args, Vec::<String>::new());
1757        assert_eq!(
1758            result.command_label, expected_shell,
1759            "We show the shell launch for empty commands"
1760        );
1761    }
1762
1763    #[gpui::test]
1764    async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) {
1765        cx.executor().allow_parking();
1766        init_test(cx);
1767
1768        let fs = FakeFs::new(cx.executor());
1769        let project = Project::test(fs, [], cx).await;
1770        let window_handle =
1771            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1772
1773        let terminal_panel = window_handle
1774            .update(cx, |multi_workspace, window, cx| {
1775                multi_workspace.workspace().update(cx, |workspace, cx| {
1776                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1777                })
1778            })
1779            .unwrap();
1780
1781        set_max_tabs(cx, Some(3));
1782
1783        for _ in 0..5 {
1784            let task = window_handle
1785                .update(cx, |_, window, cx| {
1786                    terminal_panel.update(cx, |panel, cx| {
1787                        panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1788                    })
1789                })
1790                .unwrap();
1791            task.await.unwrap();
1792        }
1793
1794        cx.run_until_parked();
1795
1796        let item_count =
1797            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
1798
1799        assert_eq!(
1800            item_count, 5,
1801            "Terminal panel should bypass max_tabs limit and have all 5 terminals"
1802        );
1803    }
1804
1805    #[cfg(unix)]
1806    #[test]
1807    fn test_prepare_script_like_task() {
1808        let user_command = r#"REPO_URL=$(git remote get-url origin | sed -e \"s/^git@\\(.*\\):\\(.*\\)\\.git$/https:\\/\\/\\1\\/\\2/\"); COMMIT_SHA=$(git log -1 --format=\"%H\" -- \"${ZED_RELATIVE_FILE}\"); echo \"${REPO_URL}/blob/${COMMIT_SHA}/${ZED_RELATIVE_FILE}#L${ZED_ROW}-$(echo $(($(wc -l <<< \"$ZED_SELECTED_TEXT\") + $ZED_ROW - 1)))\" | xclip -selection clipboard"#.to_string();
1809        let expected_cwd = PathBuf::from("/some/work");
1810
1811        let input = SpawnInTerminal {
1812            command: Some(user_command.clone()),
1813            cwd: Some(expected_cwd.clone()),
1814            ..SpawnInTerminal::default()
1815        };
1816        let shell = Shell::System;
1817
1818        let result = prepare_task_for_spawn(&input, &shell, false);
1819
1820        let system_shell = util::get_system_shell();
1821        assert_eq!(result.env, HashMap::default());
1822        assert_eq!(result.cwd, Some(expected_cwd));
1823        assert_eq!(result.shell, Shell::System);
1824        assert_eq!(result.command, Some(system_shell.clone()));
1825        assert_eq!(
1826            result.args,
1827            vec!["-i".to_string(), "-c".to_string(), user_command.clone()],
1828            "User command should have been moved into the arguments, as we're spawning a new -i shell",
1829        );
1830        assert_eq!(
1831            result.command_label,
1832            format!(
1833                "{system_shell} {interactive}-c '{user_command}'",
1834                interactive = if cfg!(windows) { "" } else { "-i " }
1835            ),
1836            "We want to show to the user the entire command spawned"
1837        );
1838    }
1839
1840    #[gpui::test]
1841    async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) {
1842        cx.executor().allow_parking();
1843        init_test(cx);
1844
1845        cx.update(|cx| {
1846            SettingsStore::update_global(cx, |store, cx| {
1847                store.update_user_settings(cx, |settings| {
1848                    settings.terminal.get_or_insert_default().project.shell =
1849                        Some(settings::Shell::Program("__nonexistent_shell__".to_owned()));
1850                });
1851            });
1852        });
1853
1854        let fs = FakeFs::new(cx.executor());
1855        let project = Project::test(fs, [], cx).await;
1856        let window_handle =
1857            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1858
1859        let terminal_panel = window_handle
1860            .update(cx, |multi_workspace, window, cx| {
1861                multi_workspace.workspace().update(cx, |workspace, cx| {
1862                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1863                })
1864            })
1865            .unwrap();
1866
1867        window_handle
1868            .update(cx, |_, window, cx| {
1869                terminal_panel.update(cx, |terminal_panel, cx| {
1870                    terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
1871                })
1872            })
1873            .unwrap()
1874            .await
1875            .unwrap_err();
1876
1877        window_handle
1878            .update(cx, |_, _, cx| {
1879                terminal_panel.update(cx, |terminal_panel, cx| {
1880                    assert!(
1881                        terminal_panel
1882                            .active_pane
1883                            .read(cx)
1884                            .items()
1885                            .any(|item| item.downcast::<FailedToSpawnTerminal>().is_some()),
1886                        "should spawn `FailedToSpawnTerminal` pane"
1887                    );
1888                })
1889            })
1890            .unwrap();
1891    }
1892
1893    #[gpui::test]
1894    async fn test_local_terminal_in_local_project(cx: &mut TestAppContext) {
1895        cx.executor().allow_parking();
1896        init_test(cx);
1897
1898        let fs = FakeFs::new(cx.executor());
1899        let project = Project::test(fs, [], cx).await;
1900        let window_handle =
1901            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1902
1903        let terminal_panel = window_handle
1904            .update(cx, |multi_workspace, window, cx| {
1905                multi_workspace.workspace().update(cx, |workspace, cx| {
1906                    cx.new(|cx| TerminalPanel::new(workspace, window, cx))
1907                })
1908            })
1909            .unwrap();
1910
1911        let result = window_handle
1912            .update(cx, |_, window, cx| {
1913                terminal_panel.update(cx, |terminal_panel, cx| {
1914                    terminal_panel.add_local_terminal_shell(RevealStrategy::Always, window, cx)
1915                })
1916            })
1917            .unwrap()
1918            .await;
1919
1920        assert!(
1921            result.is_ok(),
1922            "local terminal should successfully create in local project"
1923        );
1924    }
1925
1926    struct FocusOnlyModal {
1927        focus_handle: gpui::FocusHandle,
1928    }
1929    impl gpui::EventEmitter<gpui::DismissEvent> for FocusOnlyModal {}
1930    impl gpui::Focusable for FocusOnlyModal {
1931        fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
1932            self.focus_handle.clone()
1933        }
1934    }
1935    impl Render for FocusOnlyModal {
1936        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1937            gpui::div().track_focus(&self.focus_handle)
1938        }
1939    }
1940    impl workspace::ModalView for FocusOnlyModal {}
1941
1942    async fn open_center_display_terminal(
1943        workspace: &Entity<Workspace>,
1944        cx: &mut VisualTestContext,
1945    ) {
1946        workspace
1947            .update_in(cx, |workspace, window, cx| {
1948                TerminalPanel::add_center_terminal(workspace, window, cx, |_, cx| {
1949                    let terminal = cx.new(|cx| {
1950                        terminal::TerminalBuilder::new_display_only(
1951                            terminal::terminal_settings::CursorShape::default(),
1952                            terminal::terminal_settings::AlternateScroll::On,
1953                            None,
1954                            0,
1955                            cx.background_executor(),
1956                            util::paths::PathStyle::local(),
1957                        )
1958                        .subscribe(cx)
1959                    });
1960                    gpui::Task::ready(Ok(terminal))
1961                })
1962            })
1963            .await
1964            .unwrap();
1965        cx.run_until_parked();
1966    }
1967
1968    #[gpui::test]
1969    async fn test_center_terminal_keeps_focus_on_active_modal(cx: &mut TestAppContext) {
1970        cx.executor().allow_parking();
1971        init_test(cx);
1972
1973        let fs = FakeFs::new(cx.executor());
1974        let project = Project::test(fs, [], cx).await;
1975        let window_handle =
1976            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
1977        let workspace = window_handle
1978            .update(cx, |multi_workspace, _, _| {
1979                multi_workspace.workspace().clone()
1980            })
1981            .unwrap();
1982        let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
1983
1984        let modal_focus_handle = workspace.update_in(cx, |workspace, window, cx| {
1985            let focus_handle = cx.focus_handle();
1986            workspace.toggle_modal(window, cx, {
1987                let focus_handle = focus_handle.clone();
1988                move |_, _| FocusOnlyModal { focus_handle }
1989            });
1990            focus_handle
1991        });
1992
1993        workspace.update_in(cx, |workspace, window, cx| {
1994            assert!(workspace.has_active_modal(window, cx));
1995            assert!(
1996                modal_focus_handle.is_focused(window),
1997                "the modal should hold focus before the terminal is created"
1998            );
1999        });
2000
2001        open_center_display_terminal(&workspace, cx).await;
2002
2003        workspace.update_in(cx, |_, window, _| {
2004            assert!(
2005                modal_focus_handle.is_focused(window),
2006                "a background center terminal must not steal focus from an active modal"
2007            );
2008        });
2009    }
2010
2011    #[gpui::test]
2012    async fn test_center_terminal_takes_focus_without_modal(cx: &mut TestAppContext) {
2013        cx.executor().allow_parking();
2014        init_test(cx);
2015
2016        let fs = FakeFs::new(cx.executor());
2017        let project = Project::test(fs, [], cx).await;
2018        let window_handle =
2019            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
2020        let workspace = window_handle
2021            .update(cx, |multi_workspace, _, _| {
2022                multi_workspace.workspace().clone()
2023            })
2024            .unwrap();
2025        let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
2026
2027        open_center_display_terminal(&workspace, cx).await;
2028
2029        workspace.update_in(cx, |workspace, window, cx| {
2030            assert!(!workspace.has_active_modal(window, cx));
2031            let terminal_view = workspace
2032                .active_pane()
2033                .read(cx)
2034                .active_item()
2035                .and_then(|item| item.downcast::<TerminalView>())
2036                .expect("the new center terminal should be the active item");
2037            assert!(
2038                terminal_view.focus_handle(cx).contains_focused(window, cx),
2039                "with no modal open, a new center terminal should take focus"
2040            );
2041        });
2042    }
2043
2044    #[gpui::test]
2045    async fn test_inline_assist_tooltip_shows_keybinding_of_active_terminal(
2046        cx: &mut TestAppContext,
2047    ) {
2048        cx.executor().allow_parking();
2049        init_test(cx);
2050
2051        cx.update(|cx| {
2052            cx.bind_keys([gpui::KeyBinding::new(
2053                "ctrl-enter",
2054                InlineAssist::default(),
2055                Some("Terminal"),
2056            )])
2057        });
2058
2059        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2060        let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
2061
2062        terminal_panel.update(cx, |panel, cx| panel.set_assistant_enabled(true, cx));
2063        terminal_panel
2064            .update_in(cx, |panel, window, cx| {
2065                panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2066            })
2067            .await
2068            .unwrap();
2069        cx.run_until_parked();
2070
2071        let button_bounds = cx
2072            .debug_bounds("ICON-ZedAssistant")
2073            .expect("inline assist button should be rendered in the terminal tab bar");
2074        cx.simulate_mouse_move(button_bounds.center(), None, Modifiers::default());
2075
2076        cx.executor().advance_clock(Duration::from_millis(600));
2077        cx.run_until_parked();
2078
2079        assert!(
2080            cx.debug_bounds("KEY_BINDING-enter").is_some(),
2081            "tooltip should show the InlineAssist keybinding resolved in the terminal's context"
2082        );
2083    }
2084
2085    async fn init_workspace_with_panel(
2086        cx: &mut TestAppContext,
2087    ) -> (gpui::WindowHandle<MultiWorkspace>, Entity<TerminalPanel>) {
2088        let fs = FakeFs::new(cx.executor());
2089        let project = Project::test(fs, [], cx).await;
2090        let window_handle =
2091            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
2092
2093        let terminal_panel = window_handle
2094            .update(cx, |multi_workspace, window, cx| {
2095                multi_workspace.workspace().update(cx, |workspace, cx| {
2096                    let panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx));
2097                    workspace.add_panel(panel.clone(), window, cx);
2098                    panel
2099                })
2100            })
2101            .expect("Failed to initialize workspace with terminal panel");
2102
2103        (window_handle, terminal_panel)
2104    }
2105
2106    #[gpui::test]
2107    async fn test_new_terminal_opens_in_panel_by_default(cx: &mut TestAppContext) {
2108        cx.executor().allow_parking();
2109        init_test(cx);
2110
2111        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2112
2113        let panel_items_before =
2114            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2115        let center_items_before = window_handle
2116            .read_with(cx, |multi_workspace, cx| {
2117                multi_workspace
2118                    .workspace()
2119                    .read(cx)
2120                    .active_pane()
2121                    .read(cx)
2122                    .items_len()
2123            })
2124            .expect("Failed to read center pane items");
2125
2126        window_handle
2127            .update(cx, |multi_workspace, window, cx| {
2128                multi_workspace.workspace().update(cx, |workspace, cx| {
2129                    TerminalPanel::new_terminal(
2130                        workspace,
2131                        &workspace::NewTerminal::default(),
2132                        window,
2133                        cx,
2134                    );
2135                })
2136            })
2137            .expect("Failed to dispatch new_terminal");
2138
2139        cx.run_until_parked();
2140
2141        let panel_items_after =
2142            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2143        let center_items_after = window_handle
2144            .read_with(cx, |multi_workspace, cx| {
2145                multi_workspace
2146                    .workspace()
2147                    .read(cx)
2148                    .active_pane()
2149                    .read(cx)
2150                    .items_len()
2151            })
2152            .expect("Failed to read center pane items");
2153
2154        assert_eq!(
2155            panel_items_after,
2156            panel_items_before + 1,
2157            "Terminal should be added to the panel when no center terminal is focused"
2158        );
2159        assert_eq!(
2160            center_items_after, center_items_before,
2161            "Center pane should not gain a new terminal"
2162        );
2163    }
2164
2165    #[gpui::test]
2166    async fn test_new_terminal_opens_in_center_when_center_terminal_focused(
2167        cx: &mut TestAppContext,
2168    ) {
2169        cx.executor().allow_parking();
2170        init_test(cx);
2171
2172        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2173
2174        window_handle
2175            .update(cx, |multi_workspace, window, cx| {
2176                multi_workspace.workspace().update(cx, |workspace, cx| {
2177                    TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2178                        project.create_terminal_shell(None, cx)
2179                    })
2180                })
2181            })
2182            .expect("Failed to update workspace")
2183            .await
2184            .expect("Failed to create center terminal");
2185        cx.run_until_parked();
2186
2187        let center_items_before = window_handle
2188            .read_with(cx, |multi_workspace, cx| {
2189                multi_workspace
2190                    .workspace()
2191                    .read(cx)
2192                    .active_pane()
2193                    .read(cx)
2194                    .items_len()
2195            })
2196            .expect("Failed to read center pane items");
2197        assert_eq!(center_items_before, 1, "Center pane should have 1 terminal");
2198
2199        window_handle
2200            .update(cx, |multi_workspace, window, cx| {
2201                multi_workspace.workspace().update(cx, |workspace, cx| {
2202                    let active_item = workspace
2203                        .active_pane()
2204                        .read(cx)
2205                        .active_item()
2206                        .expect("Center pane should have an active item");
2207                    let terminal_view = active_item
2208                        .downcast::<TerminalView>()
2209                        .expect("Active center item should be a TerminalView");
2210                    window.focus(&terminal_view.focus_handle(cx), cx);
2211                })
2212            })
2213            .expect("Failed to focus terminal view");
2214        cx.run_until_parked();
2215
2216        let panel_items_before =
2217            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2218
2219        window_handle
2220            .update(cx, |multi_workspace, window, cx| {
2221                multi_workspace.workspace().update(cx, |workspace, cx| {
2222                    TerminalPanel::new_terminal(
2223                        workspace,
2224                        &workspace::NewTerminal::default(),
2225                        window,
2226                        cx,
2227                    );
2228                })
2229            })
2230            .expect("Failed to dispatch new_terminal");
2231        cx.run_until_parked();
2232
2233        let center_items_after = window_handle
2234            .read_with(cx, |multi_workspace, cx| {
2235                multi_workspace
2236                    .workspace()
2237                    .read(cx)
2238                    .active_pane()
2239                    .read(cx)
2240                    .items_len()
2241            })
2242            .expect("Failed to read center pane items");
2243        let panel_items_after =
2244            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2245
2246        assert_eq!(
2247            center_items_after,
2248            center_items_before + 1,
2249            "New terminal should be added to the center pane"
2250        );
2251        assert_eq!(
2252            panel_items_after, panel_items_before,
2253            "Terminal panel should not gain a new terminal"
2254        );
2255    }
2256
2257    #[gpui::test]
2258    async fn test_new_terminal_opens_in_panel_when_panel_focused(cx: &mut TestAppContext) {
2259        cx.executor().allow_parking();
2260        init_test(cx);
2261
2262        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2263
2264        window_handle
2265            .update(cx, |_, window, cx| {
2266                terminal_panel.update(cx, |panel, cx| {
2267                    panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2268                })
2269            })
2270            .expect("Failed to update workspace")
2271            .await
2272            .expect("Failed to create panel terminal");
2273        cx.run_until_parked();
2274
2275        window_handle
2276            .update(cx, |_, window, cx| {
2277                window.focus(&terminal_panel.read(cx).focus_handle(cx), cx);
2278            })
2279            .expect("Failed to focus terminal panel");
2280        cx.run_until_parked();
2281
2282        let panel_items_before =
2283            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2284
2285        let center_items_before = window_handle
2286            .read_with(cx, |multi_workspace, cx| {
2287                multi_workspace
2288                    .workspace()
2289                    .read(cx)
2290                    .active_pane()
2291                    .read(cx)
2292                    .items_len()
2293            })
2294            .expect("Failed to read center pane items");
2295
2296        window_handle
2297            .update(cx, |multi_workspace, window, cx| {
2298                multi_workspace.workspace().update(cx, |workspace, cx| {
2299                    TerminalPanel::new_terminal(
2300                        workspace,
2301                        &workspace::NewTerminal::default(),
2302                        window,
2303                        cx,
2304                    );
2305                })
2306            })
2307            .expect("Failed to dispatch new_terminal");
2308        cx.run_until_parked();
2309
2310        let panel_items_after =
2311            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2312        let center_items_after = window_handle
2313            .read_with(cx, |multi_workspace, cx| {
2314                multi_workspace
2315                    .workspace()
2316                    .read(cx)
2317                    .active_pane()
2318                    .read(cx)
2319                    .items_len()
2320            })
2321            .expect("Failed to read center pane items");
2322
2323        assert_eq!(
2324            panel_items_after,
2325            panel_items_before + 1,
2326            "New terminal should be added to the panel when panel is focused"
2327        );
2328        assert_eq!(
2329            center_items_after, center_items_before,
2330            "Center pane should not gain a new terminal"
2331        );
2332    }
2333
2334    #[gpui::test]
2335    async fn test_new_local_terminal_opens_in_center_when_center_terminal_focused(
2336        cx: &mut TestAppContext,
2337    ) {
2338        cx.executor().allow_parking();
2339        init_test(cx);
2340
2341        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2342
2343        window_handle
2344            .update(cx, |multi_workspace, window, cx| {
2345                multi_workspace.workspace().update(cx, |workspace, cx| {
2346                    TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2347                        project.create_terminal_shell(None, cx)
2348                    })
2349                })
2350            })
2351            .expect("Failed to update workspace")
2352            .await
2353            .expect("Failed to create center terminal");
2354        cx.run_until_parked();
2355
2356        window_handle
2357            .update(cx, |multi_workspace, window, cx| {
2358                multi_workspace.workspace().update(cx, |workspace, cx| {
2359                    let active_item = workspace
2360                        .active_pane()
2361                        .read(cx)
2362                        .active_item()
2363                        .expect("Center pane should have an active item");
2364                    let terminal_view = active_item
2365                        .downcast::<TerminalView>()
2366                        .expect("Active center item should be a TerminalView");
2367                    window.focus(&terminal_view.focus_handle(cx), cx);
2368                })
2369            })
2370            .expect("Failed to focus terminal view");
2371        cx.run_until_parked();
2372
2373        let center_items_before = window_handle
2374            .read_with(cx, |multi_workspace, cx| {
2375                multi_workspace
2376                    .workspace()
2377                    .read(cx)
2378                    .active_pane()
2379                    .read(cx)
2380                    .items_len()
2381            })
2382            .expect("Failed to read center pane items");
2383        let panel_items_before =
2384            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2385
2386        window_handle
2387            .update(cx, |multi_workspace, window, cx| {
2388                multi_workspace.workspace().update(cx, |workspace, cx| {
2389                    TerminalPanel::new_terminal(
2390                        workspace,
2391                        &workspace::NewTerminal { local: true },
2392                        window,
2393                        cx,
2394                    );
2395                })
2396            })
2397            .expect("Failed to dispatch new_terminal with local=true");
2398        cx.run_until_parked();
2399
2400        let center_items_after = window_handle
2401            .read_with(cx, |multi_workspace, cx| {
2402                multi_workspace
2403                    .workspace()
2404                    .read(cx)
2405                    .active_pane()
2406                    .read(cx)
2407                    .items_len()
2408            })
2409            .expect("Failed to read center pane items");
2410        let panel_items_after =
2411            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2412
2413        assert_eq!(
2414            center_items_after,
2415            center_items_before + 1,
2416            "New local terminal should be added to the center pane"
2417        );
2418        assert_eq!(
2419            panel_items_after, panel_items_before,
2420            "Terminal panel should not gain a new terminal"
2421        );
2422    }
2423
2424    #[gpui::test]
2425    async fn test_new_terminal_opens_in_panel_when_panel_focused_and_center_has_terminal(
2426        cx: &mut TestAppContext,
2427    ) {
2428        cx.executor().allow_parking();
2429        init_test(cx);
2430
2431        let (window_handle, terminal_panel) = init_workspace_with_panel(cx).await;
2432
2433        window_handle
2434            .update(cx, |multi_workspace, window, cx| {
2435                multi_workspace.workspace().update(cx, |workspace, cx| {
2436                    TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
2437                        project.create_terminal_shell(None, cx)
2438                    })
2439                })
2440            })
2441            .expect("Failed to update workspace")
2442            .await
2443            .expect("Failed to create center terminal");
2444        cx.run_until_parked();
2445
2446        window_handle
2447            .update(cx, |_, window, cx| {
2448                terminal_panel.update(cx, |panel, cx| {
2449                    panel.add_terminal_shell(None, RevealStrategy::Always, window, cx)
2450                })
2451            })
2452            .expect("Failed to update workspace")
2453            .await
2454            .expect("Failed to create panel terminal");
2455        cx.run_until_parked();
2456
2457        window_handle
2458            .update(cx, |_, window, cx| {
2459                window.focus(&terminal_panel.read(cx).focus_handle(cx), cx);
2460            })
2461            .expect("Failed to focus terminal panel");
2462        cx.run_until_parked();
2463
2464        let panel_items_before =
2465            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2466        let center_items_before = window_handle
2467            .read_with(cx, |multi_workspace, cx| {
2468                multi_workspace
2469                    .workspace()
2470                    .read(cx)
2471                    .active_pane()
2472                    .read(cx)
2473                    .items_len()
2474            })
2475            .expect("Failed to read center pane items");
2476
2477        window_handle
2478            .update(cx, |multi_workspace, window, cx| {
2479                multi_workspace.workspace().update(cx, |workspace, cx| {
2480                    TerminalPanel::new_terminal(
2481                        workspace,
2482                        &workspace::NewTerminal::default(),
2483                        window,
2484                        cx,
2485                    );
2486                })
2487            })
2488            .expect("Failed to dispatch new_terminal");
2489        cx.run_until_parked();
2490
2491        let panel_items_after =
2492            terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len());
2493        let center_items_after = window_handle
2494            .read_with(cx, |multi_workspace, cx| {
2495                multi_workspace
2496                    .workspace()
2497                    .read(cx)
2498                    .active_pane()
2499                    .read(cx)
2500                    .items_len()
2501            })
2502            .expect("Failed to read center pane items");
2503
2504        assert_eq!(
2505            panel_items_after,
2506            panel_items_before + 1,
2507            "New terminal should go to panel when panel is focused, even if center has a terminal"
2508        );
2509        assert_eq!(
2510            center_items_after, center_items_before,
2511            "Center pane should not gain a new terminal when panel is focused"
2512        );
2513    }
2514
2515    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
2516        cx.update_global(|store: &mut SettingsStore, cx| {
2517            store.update_user_settings(cx, |settings| {
2518                settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
2519            });
2520        });
2521    }
2522
2523    pub fn init_test(cx: &mut TestAppContext) {
2524        cx.update(|cx| {
2525            let store = SettingsStore::test(cx);
2526            cx.set_global(store);
2527            theme_settings::init(theme::LoadThemes::JustBase, cx);
2528            editor::init(cx);
2529            crate::init(cx);
2530        });
2531    }
2532}
2533
Served at tenant.openagents/omega Member data and write actions are omitted.