Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:59:11.308Z 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

multi_workspace.rs

2298 lines · 84.7 KB · rust
1use anyhow::Result;
2use fs::Fs;
3
4use gpui::{
5    AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
6    ManagedView, MouseButton, Pixels, Render, Subscription, Task, TaskExt, WeakEntity, Window,
7    WindowId, actions, deferred, px,
8};
9pub use project::ProjectGroupKey;
10use project::{DisableAiSettings, Project};
11use remote::RemoteConnectionOptions;
12use settings::Settings;
13pub use settings::SidebarSide;
14use std::cell::Cell;
15use std::future::Future;
16use std::path::PathBuf;
17use std::rc::Rc;
18use ui::prelude::*;
19use util::ResultExt;
20use util::path_list::PathList;
21use zed_actions::agents_sidebar::ToggleThreadSwitcher;
22
23use agent_settings::AgentSettings;
24use settings::SidebarDockPosition;
25use ui::{ContextMenu, right_click_menu};
26
27const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
28
29use crate::open_remote_project_with_existing_connection;
30use crate::{
31    CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode,
32    Panel, Workspace, WorkspaceId, client_side_decorations,
33    persistence::model::MultiWorkspaceState,
34};
35
36actions!(
37    multi_workspace,
38    [
39        /// Toggles the workspace switcher sidebar.
40        ToggleWorkspaceSidebar,
41        /// Closes the workspace sidebar.
42        CloseWorkspaceSidebar,
43        /// Moves focus to or from the workspace sidebar without closing it.
44        FocusWorkspaceSidebar,
45        /// Activates the next project in the sidebar.
46        NextProject,
47        /// Activates the previous project in the sidebar.
48        PreviousProject,
49        /// Activates the next thread in sidebar order.
50        NextThread,
51        /// Activates the previous thread in sidebar order.
52        PreviousThread,
53        /// Creates a new thread in the current workspace.
54        NewThread,
55        /// Moves the active project to a new window.
56        MoveProjectToNewWindow,
57    ]
58);
59
60#[derive(Default)]
61pub struct SidebarRenderState {
62    pub open: bool,
63    pub side: SidebarSide,
64}
65
66pub fn sidebar_side_context_menu(
67    id: impl Into<ElementId>,
68    cx: &App,
69) -> ui::RightClickMenu<ContextMenu> {
70    let current_position = AgentSettings::get_global(cx).sidebar_side;
71    right_click_menu(id).menu(move |window, cx| {
72        let fs = <dyn fs::Fs>::global(cx);
73        ContextMenu::build(window, cx, move |mut menu, _, _cx| {
74            let positions: [(SidebarDockPosition, &str); 2] = [
75                (SidebarDockPosition::Left, "Left"),
76                (SidebarDockPosition::Right, "Right"),
77            ];
78            for (position, label) in positions {
79                let fs = fs.clone();
80                menu = menu.toggleable_entry(
81                    label,
82                    position == current_position,
83                    IconPosition::Start,
84                    None,
85                    move |_window, cx| {
86                        let side = match position {
87                            SidebarDockPosition::Left => "left",
88                            SidebarDockPosition::Right => "right",
89                        };
90                        telemetry::event!("Sidebar Side Changed", side = side);
91                        settings::update_settings_file(fs.clone(), cx, move |settings, _cx| {
92                            settings
93                                .agent
94                                .get_or_insert_default()
95                                .set_sidebar_side(position);
96                        });
97                    },
98                );
99            }
100            menu
101        })
102    })
103}
104
105pub enum MultiWorkspaceEvent {
106    ActiveWorkspaceChanged {
107        source_workspace: Option<WeakEntity<Workspace>>,
108    },
109    WorkspaceAdded(Entity<Workspace>),
110    WorkspaceRemoved(EntityId),
111    ProjectGroupsChanged,
112}
113
114pub enum SidebarEvent {
115    SerializeNeeded,
116}
117
118pub trait Sidebar: Focusable + Render + EventEmitter<SidebarEvent> + Sized {
119    fn width(&self, cx: &App) -> Pixels;
120    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
121    fn has_notifications(&self, cx: &App) -> bool;
122    fn side(&self, _cx: &App) -> SidebarSide;
123
124    fn is_threads_list_view_active(&self) -> bool {
125        true
126    }
127    /// Makes focus reset back to the search editor upon toggling the sidebar from outside
128    fn prepare_for_focus(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
129    /// Opens or cycles the thread switcher popup.
130    fn toggle_thread_switcher(
131        &mut self,
132        _select_last: bool,
133        _window: &mut Window,
134        _cx: &mut Context<Self>,
135    ) {
136    }
137
138    /// Activates the next or previous project.
139    fn cycle_project(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
140
141    /// Activates the next or previous thread in sidebar order.
142    fn cycle_thread(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
143
144    /// Return an opaque JSON blob of sidebar-specific state to persist.
145    fn serialized_state(&self, _cx: &App) -> Option<String> {
146        None
147    }
148
149    /// Restore sidebar state from a previously-serialized blob.
150    fn restore_serialized_state(
151        &mut self,
152        _state: &str,
153        _window: &mut Window,
154        _cx: &mut Context<Self>,
155    ) {
156    }
157}
158
159pub trait SidebarHandle: 'static + Send + Sync {
160    fn width(&self, cx: &App) -> Pixels;
161    fn set_width(&self, width: Option<Pixels>, cx: &mut App);
162    fn focus_handle(&self, cx: &App) -> FocusHandle;
163    fn focus(&self, window: &mut Window, cx: &mut App);
164    fn prepare_for_focus(&self, window: &mut Window, cx: &mut App);
165    fn has_notifications(&self, cx: &App) -> bool;
166    fn to_any(&self) -> AnyView;
167    fn entity_id(&self) -> EntityId;
168    fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App);
169    fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App);
170    fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App);
171
172    fn is_threads_list_view_active(&self, cx: &App) -> bool;
173
174    fn side(&self, cx: &App) -> SidebarSide;
175    fn serialized_state(&self, cx: &App) -> Option<String>;
176    fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App);
177}
178
179#[derive(Clone)]
180pub struct DraggedSidebar;
181
182impl Render for DraggedSidebar {
183    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
184        gpui::Empty
185    }
186}
187
188impl<T: Sidebar> SidebarHandle for Entity<T> {
189    fn width(&self, cx: &App) -> Pixels {
190        self.read(cx).width(cx)
191    }
192
193    fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
194        self.update(cx, |this, cx| this.set_width(width, cx))
195    }
196
197    fn focus_handle(&self, cx: &App) -> FocusHandle {
198        self.read(cx).focus_handle(cx)
199    }
200
201    fn focus(&self, window: &mut Window, cx: &mut App) {
202        let handle = self.read(cx).focus_handle(cx);
203        window.focus(&handle, cx);
204    }
205
206    fn prepare_for_focus(&self, window: &mut Window, cx: &mut App) {
207        self.update(cx, |this, cx| this.prepare_for_focus(window, cx));
208    }
209
210    fn has_notifications(&self, cx: &App) -> bool {
211        self.read(cx).has_notifications(cx)
212    }
213
214    fn to_any(&self) -> AnyView {
215        self.clone().into()
216    }
217
218    fn entity_id(&self) -> EntityId {
219        Entity::entity_id(self)
220    }
221
222    fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App) {
223        let entity = self.clone();
224        window.defer(cx, move |window, cx| {
225            entity.update(cx, |this, cx| {
226                this.toggle_thread_switcher(select_last, window, cx);
227            });
228        });
229    }
230
231    fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App) {
232        let entity = self.clone();
233        window.defer(cx, move |window, cx| {
234            entity.update(cx, |this, cx| {
235                this.cycle_project(forward, window, cx);
236            });
237        });
238    }
239
240    fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App) {
241        let entity = self.clone();
242        window.defer(cx, move |window, cx| {
243            entity.update(cx, |this, cx| {
244                this.cycle_thread(forward, window, cx);
245            });
246        });
247    }
248
249    fn is_threads_list_view_active(&self, cx: &App) -> bool {
250        self.read(cx).is_threads_list_view_active()
251    }
252
253    fn side(&self, cx: &App) -> SidebarSide {
254        self.read(cx).side(cx)
255    }
256
257    fn serialized_state(&self, cx: &App) -> Option<String> {
258        self.read(cx).serialized_state(cx)
259    }
260
261    fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App) {
262        self.update(cx, |this, cx| {
263            this.restore_serialized_state(state, window, cx)
264        })
265    }
266}
267
268#[derive(Clone)]
269pub struct ProjectGroup {
270    pub key: ProjectGroupKey,
271    pub workspaces: Vec<Entity<Workspace>>,
272    pub expanded: bool,
273}
274
275pub struct SerializedProjectGroupState {
276    pub key: ProjectGroupKey,
277    pub expanded: bool,
278}
279
280#[derive(Clone)]
281pub struct ProjectGroupState {
282    pub key: ProjectGroupKey,
283    pub expanded: bool,
284    pub last_active_workspace: Option<WeakEntity<Workspace>>,
285}
286
287pub struct MultiWorkspace {
288    window_id: WindowId,
289    retained_workspaces: Vec<Entity<Workspace>>,
290    project_groups: Vec<ProjectGroupState>,
291    active_workspace: Entity<Workspace>,
292    /// Source of truth for which workspace is presented in this window, shared
293    /// with each member `Workspace` so they can tell whether they own the
294    /// platform window's title and edited indicator. This only exists to prevent
295    /// Workspaces from having to read their parent MultiWorkspace to check
296    /// chrome ownership, as that might cause a double lease. Kept in sync with
297    /// `active_workspace`.
298    active_workspace_id: Rc<Cell<EntityId>>,
299    sidebar: Option<Box<dyn SidebarHandle>>,
300    sidebar_open: bool,
301    sidebar_overlay: Option<AnyView>,
302    pending_removal_tasks: Vec<Task<()>>,
303    _serialize_task: Option<Task<()>>,
304    _subscriptions: Vec<Subscription>,
305    previous_focus_handle: Option<FocusHandle>,
306}
307
308impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
309
310impl MultiWorkspace {
311    pub fn sidebar_side(&self, cx: &App) -> SidebarSide {
312        self.sidebar
313            .as_ref()
314            .map_or(SidebarSide::Left, |s| s.side(cx))
315    }
316
317    pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
318        SidebarRenderState {
319            open: self.sidebar_open() && self.multi_workspace_enabled(cx),
320            side: self.sidebar_side(cx),
321        }
322    }
323
324    pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
325        let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
326            if let Some(task) = this._serialize_task.take() {
327                task.detach();
328            }
329            for task in std::mem::take(&mut this.pending_removal_tasks) {
330                task.detach();
331            }
332        });
333        let quit_subscription = cx.on_app_quit(Self::app_will_quit);
334        let settings_subscription = cx.observe_global_in::<settings::SettingsStore>(window, {
335            let mut previous_multi_workspace_enabled = !DisableAiSettings::get_global(cx)
336                .disable_ai
337                && AgentSettings::get_global(cx).enabled;
338            move |this, window, cx| {
339                let multi_workspace_enabled = this.multi_workspace_enabled(cx);
340                if previous_multi_workspace_enabled && !multi_workspace_enabled {
341                    this.collapse_to_single_workspace(window, cx);
342                }
343                previous_multi_workspace_enabled = multi_workspace_enabled;
344            }
345        });
346        Self::subscribe_to_workspace(&workspace, window, cx);
347        let weak_self = cx.weak_entity();
348        let active_workspace_id = Rc::new(Cell::new(workspace.entity_id()));
349        workspace.update(cx, |workspace, cx| {
350            workspace.set_multi_workspace(weak_self, active_workspace_id.clone(), cx);
351        });
352        Self {
353            window_id: window.window_handle().window_id(),
354            retained_workspaces: Vec::new(),
355            project_groups: Vec::new(),
356            active_workspace: workspace,
357            active_workspace_id,
358            sidebar: None,
359            sidebar_open: false,
360            sidebar_overlay: None,
361            pending_removal_tasks: Vec::new(),
362            _serialize_task: None,
363            _subscriptions: vec![
364                release_subscription,
365                quit_subscription,
366                settings_subscription,
367            ],
368            previous_focus_handle: None,
369        }
370    }
371
372    pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
373        self._subscriptions
374            .push(cx.observe(&sidebar, |_this, _, cx| {
375                cx.notify();
376            }));
377        self._subscriptions
378            .push(cx.subscribe(&sidebar, |this, _, event, cx| match event {
379                SidebarEvent::SerializeNeeded => {
380                    this.serialize(cx);
381                }
382            }));
383        self.sidebar = Some(Box::new(sidebar));
384    }
385
386    pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
387        self.sidebar.as_deref()
388    }
389
390    pub fn set_sidebar_overlay(&mut self, overlay: Option<AnyView>, cx: &mut Context<Self>) {
391        self.sidebar_overlay = overlay;
392        cx.notify();
393    }
394
395    pub fn sidebar_open(&self) -> bool {
396        self.sidebar_open
397    }
398
399    pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
400        self.sidebar
401            .as_ref()
402            .map_or(false, |s| s.has_notifications(cx))
403    }
404
405    pub fn is_threads_list_view_active(&self, cx: &App) -> bool {
406        self.sidebar
407            .as_ref()
408            .map_or(false, |s| s.is_threads_list_view_active(cx))
409    }
410
411    pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
412        !DisableAiSettings::get_global(cx).disable_ai && AgentSettings::get_global(cx).enabled
413    }
414
415    pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
416        if !self.multi_workspace_enabled(cx) {
417            return;
418        }
419
420        if self.sidebar_open() {
421            self.close_sidebar(window, cx);
422        } else {
423            self.previous_focus_handle = window.focused(cx);
424            self.open_sidebar(cx);
425            if let Some(sidebar) = &self.sidebar {
426                sidebar.prepare_for_focus(window, cx);
427                sidebar.focus(window, cx);
428            }
429        }
430    }
431
432    pub fn close_sidebar_action(&mut self, window: &mut Window, cx: &mut Context<Self>) {
433        if !self.multi_workspace_enabled(cx) {
434            return;
435        }
436
437        if self.sidebar_open() {
438            self.close_sidebar(window, cx);
439        }
440    }
441
442    pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
443        if !self.multi_workspace_enabled(cx) {
444            return;
445        }
446
447        if self.sidebar_open() {
448            let sidebar_is_focused = self
449                .sidebar
450                .as_ref()
451                .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
452
453            if sidebar_is_focused {
454                self.restore_previous_focus(false, window, cx);
455            } else {
456                self.previous_focus_handle = window.focused(cx);
457                if let Some(sidebar) = &self.sidebar {
458                    sidebar.prepare_for_focus(window, cx);
459                    sidebar.focus(window, cx);
460                }
461            }
462        } else {
463            self.previous_focus_handle = window.focused(cx);
464            self.open_sidebar(cx);
465            if let Some(sidebar) = &self.sidebar {
466                sidebar.prepare_for_focus(window, cx);
467                sidebar.focus(window, cx);
468            }
469        }
470    }
471
472    pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
473        let side = match self.sidebar_side(cx) {
474            SidebarSide::Left => "left",
475            SidebarSide::Right => "right",
476        };
477        telemetry::event!("Sidebar Toggled", action = "open", side = side);
478        self.apply_open_sidebar(cx);
479    }
480
481    /// Restores the sidebar to open state from persisted session data without
482    /// firing a telemetry event, since this is not a user-initiated action.
483    pub(crate) fn restore_open_sidebar(&mut self, cx: &mut Context<Self>) {
484        self.apply_open_sidebar(cx);
485    }
486
487    fn apply_open_sidebar(&mut self, cx: &mut Context<Self>) {
488        self.sidebar_open = true;
489        self.retain_active_workspace(cx);
490        let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
491        for workspace in self.retained_workspaces.clone() {
492            workspace.update(cx, |workspace, _cx| {
493                workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
494            });
495        }
496        self.serialize(cx);
497        cx.notify();
498    }
499
500    pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
501        let side = match self.sidebar_side(cx) {
502            SidebarSide::Left => "left",
503            SidebarSide::Right => "right",
504        };
505        telemetry::event!("Sidebar Toggled", action = "close", side = side);
506        self.sidebar_open = false;
507        for workspace in self.retained_workspaces.clone() {
508            workspace.update(cx, |workspace, _cx| {
509                workspace.set_sidebar_focus_handle(None);
510            });
511        }
512        let sidebar_has_focus = self
513            .sidebar
514            .as_ref()
515            .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
516        if sidebar_has_focus {
517            self.restore_previous_focus(true, window, cx);
518        } else {
519            self.previous_focus_handle.take();
520        }
521        self.serialize(cx);
522        cx.notify();
523    }
524
525    fn restore_previous_focus(&mut self, clear: bool, window: &mut Window, cx: &mut Context<Self>) {
526        let focus_handle = if clear {
527            self.previous_focus_handle.take()
528        } else {
529            self.previous_focus_handle.clone()
530        };
531
532        if let Some(previous_focus) = focus_handle {
533            previous_focus.focus(window, cx);
534        } else {
535            let pane = self.workspace().read(cx).active_pane().clone();
536            window.focus(&pane.read(cx).focus_handle(cx), cx);
537        }
538    }
539
540    pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
541        cx.spawn_in(window, async move |this, cx| {
542            let workspaces = this.update(cx, |multi_workspace, _cx| {
543                multi_workspace.workspaces().cloned().collect::<Vec<_>>()
544            })?;
545
546            for workspace in workspaces {
547                let should_continue = workspace
548                    .update_in(cx, |workspace, window, cx| {
549                        workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
550                    })?
551                    .await?;
552                if !should_continue {
553                    return anyhow::Ok(());
554                }
555            }
556
557            cx.update(|window, _cx| {
558                window.remove_window();
559            })?;
560
561            anyhow::Ok(())
562        })
563        .detach_and_log_err(cx);
564    }
565
566    fn subscribe_to_workspace(
567        workspace: &Entity<Workspace>,
568        window: &Window,
569        cx: &mut Context<Self>,
570    ) {
571        let project = workspace.read(cx).project().clone();
572        cx.subscribe_in(&project, window, {
573            let workspace = workspace.downgrade();
574            move |this, _project, event, _window, cx| match event {
575                project::Event::WorktreePathsChanged { old_worktree_paths } => {
576                    if let Some(workspace) = workspace.upgrade() {
577                        let host = workspace
578                            .read(cx)
579                            .project()
580                            .read(cx)
581                            .remote_connection_options(cx);
582                        let old_key =
583                            ProjectGroupKey::from_worktree_paths(old_worktree_paths, host);
584                        this.handle_project_group_key_change(&workspace, &old_key, cx);
585                    }
586                }
587                _ => {}
588            }
589        })
590        .detach();
591
592        cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
593            if let WorkspaceEvent::Activate = event {
594                this.activate(workspace.clone(), None, window, cx);
595            }
596        })
597        .detach();
598    }
599
600    fn handle_project_group_key_change(
601        &mut self,
602        workspace: &Entity<Workspace>,
603        old_key: &ProjectGroupKey,
604        cx: &mut Context<Self>,
605    ) {
606        if !self.is_workspace_retained(workspace) {
607            return;
608        }
609
610        let new_key = workspace.read(cx).project_group_key(cx);
611        if new_key.path_list().paths().is_empty() {
612            return;
613        }
614
615        // The Project already emitted WorktreePathsChanged which the
616        // sidebar handles for thread migration.
617        self.rekey_project_group(old_key, &new_key, cx);
618        self.serialize(cx);
619        cx.notify();
620    }
621
622    pub fn is_workspace_retained(&self, workspace: &Entity<Workspace>) -> bool {
623        self.retained_workspaces
624            .iter()
625            .any(|retained| retained == workspace)
626    }
627
628    pub fn active_workspace_is_retained(&self) -> bool {
629        self.is_workspace_retained(&self.active_workspace)
630    }
631
632    pub fn retained_workspaces(&self) -> &[Entity<Workspace>] {
633        &self.retained_workspaces
634    }
635
636    /// Ensures a project group exists for `key`, creating one if needed.
637    fn ensure_project_group_state(&mut self, key: ProjectGroupKey) {
638        if key.path_list().paths().is_empty() {
639            return;
640        }
641
642        if self.project_groups.iter().any(|group| group.key == key) {
643            return;
644        }
645
646        self.project_groups.insert(
647            0,
648            ProjectGroupState {
649                key,
650                expanded: true,
651                last_active_workspace: None,
652            },
653        );
654    }
655
656    /// Transitions a project group from `old_key` to `new_key`.
657    ///
658    /// On collision (both keys have groups), the active workspace's
659    /// Re-keys a project group from `old_key` to `new_key`, handling
660    /// collisions. When two groups collide, the active workspace's
661    /// group always wins. Otherwise the old key's state is preserved
662    /// — it represents the group the user or system just acted on.
663    /// The losing group is removed, and the winner is re-keyed in
664    /// place to preserve sidebar order.
665    fn rekey_project_group(
666        &mut self,
667        old_key: &ProjectGroupKey,
668        new_key: &ProjectGroupKey,
669        cx: &App,
670    ) {
671        if old_key == new_key {
672            return;
673        }
674
675        if new_key.path_list().paths().is_empty() {
676            return;
677        }
678
679        let old_key_exists = self.project_groups.iter().any(|g| g.key == *old_key);
680        let new_key_exists = self.project_groups.iter().any(|g| g.key == *new_key);
681
682        if !old_key_exists {
683            self.ensure_project_group_state(new_key.clone());
684            return;
685        }
686
687        if new_key_exists {
688            let active_key = self.active_workspace.read(cx).project_group_key(cx);
689            if active_key == *new_key {
690                self.project_groups.retain(|g| g.key != *old_key);
691            } else {
692                self.project_groups.retain(|g| g.key != *new_key);
693                if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == *old_key) {
694                    group.key = new_key.clone();
695                }
696            }
697        } else {
698            if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == *old_key) {
699                group.key = new_key.clone();
700            }
701        }
702
703        // If another retained workspace still has the old key (e.g. a
704        // linked worktree workspace), re-create the old group so it
705        // remains reachable in the sidebar.
706        let other_workspace_needs_old_key = self
707            .retained_workspaces
708            .iter()
709            .any(|ws| ws.read(cx).project_group_key(cx) == *old_key);
710        if other_workspace_needs_old_key {
711            self.ensure_project_group_state(old_key.clone());
712        }
713    }
714
715    pub(crate) fn retain_workspace(
716        &mut self,
717        workspace: Entity<Workspace>,
718        key: ProjectGroupKey,
719        cx: &mut Context<Self>,
720    ) {
721        self.ensure_project_group_state(key);
722        if self.is_workspace_retained(&workspace) {
723            return;
724        }
725
726        self.retained_workspaces.push(workspace.clone());
727        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
728    }
729
730    pub(crate) fn activate_provisional_workspace(
731        &mut self,
732        workspace: Entity<Workspace>,
733        provisional_key: ProjectGroupKey,
734        window: &mut Window,
735        cx: &mut Context<Self>,
736    ) {
737        if workspace != self.active_workspace {
738            self.register_workspace(&workspace, window, cx);
739        }
740
741        self.ensure_project_group_state(provisional_key);
742        if !self.is_workspace_retained(&workspace) {
743            self.retained_workspaces.push(workspace.clone());
744        }
745
746        self.activate(workspace.clone(), None, window, cx);
747        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
748    }
749
750    fn register_workspace(
751        &mut self,
752        workspace: &Entity<Workspace>,
753        window: &Window,
754        cx: &mut Context<Self>,
755    ) {
756        Self::subscribe_to_workspace(workspace, window, cx);
757        let weak_self = cx.weak_entity();
758        let active_workspace_id = self.active_workspace_id.clone();
759        workspace.update(cx, |workspace, cx| {
760            workspace.set_multi_workspace(weak_self, active_workspace_id, cx);
761        });
762
763        let entity = cx.entity();
764        cx.defer({
765            let workspace = workspace.clone();
766            move |cx| {
767                entity.update(cx, |this, cx| {
768                    this.sync_sidebar_to_workspace(&workspace, cx);
769                })
770            }
771        });
772    }
773
774    pub fn project_group_key_for_workspace(
775        &self,
776        workspace: &Entity<Workspace>,
777        cx: &App,
778    ) -> ProjectGroupKey {
779        workspace.read(cx).project_group_key(cx)
780    }
781
782    pub fn restore_project_groups(
783        &mut self,
784        groups: Vec<SerializedProjectGroupState>,
785        _cx: &mut Context<Self>,
786    ) {
787        let mut restored: Vec<ProjectGroupState> = Vec::new();
788        for SerializedProjectGroupState { key, expanded } in groups {
789            if key.path_list().paths().is_empty() {
790                continue;
791            }
792            if restored.iter().any(|group| group.key == key) {
793                continue;
794            }
795            restored.push(ProjectGroupState {
796                key,
797                expanded,
798                last_active_workspace: None,
799            });
800        }
801        for existing in std::mem::take(&mut self.project_groups) {
802            if !restored.iter().any(|group| group.key == existing.key) {
803                restored.push(existing);
804            }
805        }
806        self.project_groups = restored;
807    }
808
809    pub fn project_group_keys(&self) -> Vec<ProjectGroupKey> {
810        self.project_groups
811            .iter()
812            .map(|group| group.key.clone())
813            .collect()
814    }
815
816    fn derived_project_groups(&self, cx: &App) -> Vec<ProjectGroup> {
817        self.project_groups
818            .iter()
819            .map(|group| ProjectGroup {
820                key: group.key.clone(),
821                workspaces: self
822                    .retained_workspaces
823                    .iter()
824                    .filter(|workspace| workspace.read(cx).project_group_key(cx) == group.key)
825                    .cloned()
826                    .collect(),
827                expanded: group.expanded,
828            })
829            .collect()
830    }
831
832    pub fn project_groups(&self, cx: &App) -> Vec<ProjectGroup> {
833        self.derived_project_groups(cx)
834    }
835
836    pub fn last_active_workspace_for_group(
837        &self,
838        key: &ProjectGroupKey,
839        cx: &App,
840    ) -> Option<Entity<Workspace>> {
841        let group = self.project_groups.iter().find(|g| g.key == *key)?;
842        let weak = group.last_active_workspace.as_ref()?;
843        let workspace = weak.upgrade()?;
844        (workspace.read(cx).project_group_key(cx) == *key).then_some(workspace)
845    }
846
847    pub fn group_state_by_key(&self, key: &ProjectGroupKey) -> Option<&ProjectGroupState> {
848        self.project_groups.iter().find(|group| group.key == *key)
849    }
850
851    pub fn group_state_by_key_mut(
852        &mut self,
853        key: &ProjectGroupKey,
854    ) -> Option<&mut ProjectGroupState> {
855        self.project_groups
856            .iter_mut()
857            .find(|group| group.key == *key)
858    }
859
860    pub fn set_all_groups_expanded(&mut self, expanded: bool) {
861        for group in &mut self.project_groups {
862            group.expanded = expanded;
863        }
864    }
865
866    pub fn move_project_group_up(&mut self, key: &ProjectGroupKey, cx: &mut Context<Self>) -> bool {
867        let Some(index) = self
868            .project_groups
869            .iter()
870            .position(|group| group.key == *key)
871        else {
872            return false;
873        };
874        if index == 0 {
875            return false;
876        }
877        self.project_groups.swap(index - 1, index);
878        cx.emit(MultiWorkspaceEvent::ProjectGroupsChanged);
879        self.serialize(cx);
880        cx.notify();
881        true
882    }
883
884    pub fn move_project_group_down(
885        &mut self,
886        key: &ProjectGroupKey,
887        cx: &mut Context<Self>,
888    ) -> bool {
889        let Some(index) = self
890            .project_groups
891            .iter()
892            .position(|group| group.key == *key)
893        else {
894            return false;
895        };
896        if index + 1 >= self.project_groups.len() {
897            return false;
898        }
899        self.project_groups.swap(index, index + 1);
900        cx.emit(MultiWorkspaceEvent::ProjectGroupsChanged);
901        self.serialize(cx);
902        cx.notify();
903        true
904    }
905
906    pub fn workspaces_for_project_group(
907        &self,
908        key: &ProjectGroupKey,
909        cx: &App,
910    ) -> Option<Vec<Entity<Workspace>>> {
911        let has_group = self.project_groups.iter().any(|group| group.key == *key)
912            || self
913                .retained_workspaces
914                .iter()
915                .any(|workspace| workspace.read(cx).project_group_key(cx) == *key);
916
917        has_group.then(|| {
918            self.retained_workspaces
919                .iter()
920                .filter(|workspace| workspace.read(cx).project_group_key(cx) == *key)
921                .cloned()
922                .collect()
923        })
924    }
925
926    pub fn close_workspace(
927        &mut self,
928        workspace: &Entity<Workspace>,
929        window: &mut Window,
930        cx: &mut Context<Self>,
931    ) -> Task<Result<bool>> {
932        let group_key = workspace.read(cx).project_group_key(cx);
933        let excluded_workspace = workspace.clone();
934
935        self.remove(
936            [workspace.clone()],
937            move |this, window, cx| {
938                if let Some(workspace) = this
939                    .workspaces_for_project_group(&group_key, cx)
940                    .unwrap_or_default()
941                    .into_iter()
942                    .find(|candidate| candidate != &excluded_workspace)
943                {
944                    return Task::ready(Ok(workspace));
945                }
946
947                let current_group_index = this
948                    .project_groups
949                    .iter()
950                    .position(|group| group.key == group_key);
951
952                if let Some(current_group_index) = current_group_index {
953                    for distance in 1..this.project_groups.len() {
954                        for neighboring_index in [
955                            current_group_index.checked_add(distance),
956                            current_group_index.checked_sub(distance),
957                        ]
958                        .into_iter()
959                        .flatten()
960                        {
961                            let Some(neighboring_group) =
962                                this.project_groups.get(neighboring_index)
963                            else {
964                                continue;
965                            };
966
967                            if let Some(workspace) = this
968                                .last_active_workspace_for_group(&neighboring_group.key, cx)
969                                .or_else(|| {
970                                    this.workspaces_for_project_group(&neighboring_group.key, cx)
971                                        .unwrap_or_default()
972                                        .into_iter()
973                                        .find(|candidate| candidate != &excluded_workspace)
974                                })
975                            {
976                                return Task::ready(Ok(workspace));
977                            }
978                        }
979                    }
980                }
981
982                let neighboring_group_key = current_group_index.and_then(|index| {
983                    this.project_groups
984                        .get(index + 1)
985                        .or_else(|| {
986                            index
987                                .checked_sub(1)
988                                .and_then(|previous| this.project_groups.get(previous))
989                        })
990                        .map(|group| group.key.clone())
991                });
992
993                if let Some(neighboring_group_key) = neighboring_group_key
994                    && neighboring_group_key.host().is_none()
995                {
996                    return this.find_or_create_local_workspace(
997                        neighboring_group_key.path_list().clone(),
998                        Some(neighboring_group_key),
999                        std::slice::from_ref(&excluded_workspace),
1000                        None,
1001                        OpenMode::Activate,
1002                        window,
1003                        cx,
1004                    );
1005                }
1006
1007                let app_state = this.workspace().read(cx).app_state().clone();
1008                let project = Project::local(
1009                    app_state.client.clone(),
1010                    app_state.node_runtime.clone(),
1011                    app_state.user_store.clone(),
1012                    app_state.languages.clone(),
1013                    app_state.fs.clone(),
1014                    None,
1015                    project::LocalProjectFlags::default(),
1016                    cx,
1017                );
1018                let new_workspace =
1019                    cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1020                Task::ready(Ok(new_workspace))
1021            },
1022            window,
1023            cx,
1024        )
1025    }
1026
1027    pub fn remove_project_group(
1028        &mut self,
1029        group_key: &ProjectGroupKey,
1030        window: &mut Window,
1031        cx: &mut Context<Self>,
1032    ) -> Task<Result<bool>> {
1033        let pos = self
1034            .project_groups
1035            .iter()
1036            .position(|group| group.key == *group_key);
1037        let workspaces = self
1038            .workspaces_for_project_group(group_key, cx)
1039            .unwrap_or_default();
1040
1041        // Compute the neighbor while the group is still in the list.
1042        let neighbor_key = pos.and_then(|pos| {
1043            self.project_groups
1044                .get(pos + 1)
1045                .or_else(|| pos.checked_sub(1).and_then(|i| self.project_groups.get(i)))
1046                .map(|group| group.key.clone())
1047        });
1048
1049        // Now remove the group.
1050        self.project_groups.retain(|group| group.key != *group_key);
1051        cx.emit(MultiWorkspaceEvent::ProjectGroupsChanged);
1052
1053        let excluded_workspaces = workspaces.clone();
1054        self.remove(
1055            workspaces,
1056            move |this, window, cx| {
1057                if let Some(neighbor_key) = neighbor_key
1058                    && neighbor_key.host().is_none()
1059                {
1060                    return this.find_or_create_local_workspace(
1061                        neighbor_key.path_list().clone(),
1062                        Some(neighbor_key.clone()),
1063                        &excluded_workspaces,
1064                        None,
1065                        OpenMode::Activate,
1066                        window,
1067                        cx,
1068                    );
1069                }
1070
1071                // No other project groups remain — create an empty workspace.
1072                let app_state = this.workspace().read(cx).app_state().clone();
1073                let project = Project::local(
1074                    app_state.client.clone(),
1075                    app_state.node_runtime.clone(),
1076                    app_state.user_store.clone(),
1077                    app_state.languages.clone(),
1078                    app_state.fs.clone(),
1079                    None,
1080                    project::LocalProjectFlags::default(),
1081                    cx,
1082                );
1083                let new_workspace =
1084                    cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1085                Task::ready(Ok(new_workspace))
1086            },
1087            window,
1088            cx,
1089        )
1090    }
1091
1092    /// Goes through sqlite: serialize -> close -> open new window
1093    /// This avoids issues with pending tasks having the wrong window
1094    pub fn open_project_group_in_new_window(
1095        &mut self,
1096        key: &ProjectGroupKey,
1097        window: &mut Window,
1098        cx: &mut Context<Self>,
1099    ) -> Task<Result<()>> {
1100        let paths: Vec<PathBuf> = key.path_list().ordered_paths().cloned().collect();
1101        if paths.is_empty() {
1102            return Task::ready(Ok(()));
1103        }
1104
1105        let app_state = self.workspace().read(cx).app_state().clone();
1106
1107        let workspaces: Vec<_> = self
1108            .workspaces_for_project_group(key, cx)
1109            .unwrap_or_default();
1110        let mut serialization_tasks = Vec::new();
1111        for workspace in &workspaces {
1112            serialization_tasks.push(workspace.update(cx, |workspace, inner_cx| {
1113                workspace.flush_serialization(window, inner_cx)
1114            }));
1115        }
1116
1117        let remove_task = self.remove_project_group(key, window, cx);
1118
1119        cx.spawn(async move |_this, cx| {
1120            futures::future::join_all(serialization_tasks).await;
1121
1122            let removed = remove_task.await?;
1123            if !removed {
1124                return Ok(());
1125            }
1126
1127            cx.update(|cx| {
1128                Workspace::new_local(paths, app_state, None, None, None, OpenMode::NewWindow, cx)
1129            })
1130            .await?;
1131
1132            Ok(())
1133        })
1134    }
1135
1136    /// Finds an existing workspace whose root paths and host exactly match.
1137    pub fn workspace_for_paths(
1138        &self,
1139        path_list: &PathList,
1140        host: Option<&RemoteConnectionOptions>,
1141        cx: &App,
1142    ) -> Option<Entity<Workspace>> {
1143        self.workspace_for_paths_excluding(path_list, host, &[], cx)
1144    }
1145
1146    fn workspace_for_paths_excluding(
1147        &self,
1148        path_list: &PathList,
1149        host: Option<&RemoteConnectionOptions>,
1150        excluding: &[Entity<Workspace>],
1151        cx: &App,
1152    ) -> Option<Entity<Workspace>> {
1153        for workspace in self.workspaces() {
1154            if excluding.contains(workspace) {
1155                continue;
1156            }
1157            let root_paths = PathList::new(&workspace.read(cx).root_paths(cx));
1158            let key = workspace.read(cx).project_group_key(cx);
1159            let host_matches = key.host().as_ref() == host;
1160            let paths_match = root_paths == *path_list;
1161            if host_matches && paths_match {
1162                return Some(workspace.clone());
1163            }
1164        }
1165
1166        None
1167    }
1168
1169    /// Finds an existing workspace whose paths match, or creates a new one.
1170    ///
1171    /// For local projects (`host` is `None`), this delegates to
1172    /// [`Self::find_or_create_local_workspace`]. For remote projects, it
1173    /// tries an exact path match and, if no existing workspace is found,
1174    /// calls `connect_remote` to establish a connection and creates a new
1175    /// remote workspace.
1176    ///
1177    /// The `connect_remote` closure is responsible for any user-facing
1178    /// connection UI (e.g. password prompts). It receives the connection
1179    /// options and should return a [`Task`] that resolves to the
1180    /// [`RemoteClient`] session, or `None` if the connection was
1181    /// cancelled.
1182    pub fn find_or_create_workspace(
1183        &mut self,
1184        paths: PathList,
1185        host: Option<RemoteConnectionOptions>,
1186        provisional_project_group_key: Option<ProjectGroupKey>,
1187        connect_remote: impl FnOnce(
1188            RemoteConnectionOptions,
1189            &mut Window,
1190            &mut Context<Self>,
1191        ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
1192        + 'static,
1193        excluding: &[Entity<Workspace>],
1194        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1195        open_mode: OpenMode,
1196        window: &mut Window,
1197        cx: &mut Context<Self>,
1198    ) -> Task<Result<Entity<Workspace>>> {
1199        self.find_or_create_workspace_with_source_workspace(
1200            paths,
1201            host,
1202            provisional_project_group_key,
1203            connect_remote,
1204            excluding,
1205            init,
1206            open_mode,
1207            None,
1208            window,
1209            cx,
1210        )
1211    }
1212
1213    pub fn find_or_create_workspace_with_source_workspace(
1214        &mut self,
1215        paths: PathList,
1216        host: Option<RemoteConnectionOptions>,
1217        provisional_project_group_key: Option<ProjectGroupKey>,
1218        connect_remote: impl FnOnce(
1219            RemoteConnectionOptions,
1220            &mut Window,
1221            &mut Context<Self>,
1222        ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
1223        + 'static,
1224        excluding: &[Entity<Workspace>],
1225        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1226        open_mode: OpenMode,
1227        source_workspace: Option<WeakEntity<Workspace>>,
1228        window: &mut Window,
1229        cx: &mut Context<Self>,
1230    ) -> Task<Result<Entity<Workspace>>> {
1231        if let Some(workspace) =
1232            self.workspace_for_paths_excluding(&paths, host.as_ref(), excluding, cx)
1233        {
1234            self.activate(workspace.clone(), source_workspace, window, cx);
1235            return Task::ready(Ok(workspace));
1236        }
1237
1238        let Some(connection_options) = host else {
1239            return self.find_or_create_local_workspace_with_source_workspace(
1240                paths,
1241                provisional_project_group_key,
1242                excluding,
1243                init,
1244                open_mode,
1245                source_workspace,
1246                window,
1247                cx,
1248            );
1249        };
1250
1251        let app_state = self.workspace().read(cx).app_state().clone();
1252        let window_handle = window.window_handle().downcast::<MultiWorkspace>();
1253        let connect_task = connect_remote(connection_options.clone(), window, cx);
1254        let paths_vec = paths.paths().to_vec();
1255
1256        cx.spawn(async move |_this, cx| {
1257            let session = connect_task
1258                .await?
1259                .ok_or_else(|| anyhow::anyhow!("Remote connection was cancelled"))?;
1260
1261            let new_project = cx.update(|cx| {
1262                Project::remote(
1263                    session,
1264                    app_state.client.clone(),
1265                    app_state.node_runtime.clone(),
1266                    app_state.user_store.clone(),
1267                    app_state.languages.clone(),
1268                    app_state.fs.clone(),
1269                    true,
1270                    cx,
1271                )
1272            });
1273
1274            let effective_paths_vec =
1275                if let Some(project_group) = provisional_project_group_key.as_ref() {
1276                    let resolve_tasks = cx.update(|cx| {
1277                        let project = new_project.read(cx);
1278                        paths_vec
1279                            .iter()
1280                            .map(|path| project.resolve_abs_path(&path.to_string_lossy(), cx))
1281                            .collect::<Vec<_>>()
1282                    });
1283                    let resolved = futures::future::join_all(resolve_tasks).await;
1284                    // `resolve_abs_path` returns `None` for both "definitely
1285                    // absent" and transport errors (it swallows the error via
1286                    // `log_err`). This is a weaker guarantee than the local
1287                    // `Ok(None)` check, but it matches how the rest of the
1288                    // codebase consumes this API.
1289                    let all_paths_missing =
1290                        !paths_vec.is_empty() && resolved.iter().all(|resolved| resolved.is_none());
1291
1292                    if all_paths_missing {
1293                        project_group.path_list().paths().to_vec()
1294                    } else {
1295                        paths_vec
1296                    }
1297                } else {
1298                    paths_vec
1299                };
1300
1301            let window_handle =
1302                window_handle.ok_or_else(|| anyhow::anyhow!("Window is not a MultiWorkspace"))?;
1303
1304            open_remote_project_with_existing_connection(
1305                connection_options,
1306                new_project,
1307                effective_paths_vec,
1308                app_state,
1309                window_handle,
1310                provisional_project_group_key,
1311                source_workspace,
1312                cx,
1313            )
1314            .await?;
1315
1316            window_handle.update(cx, |multi_workspace, window, cx| {
1317                let workspace = multi_workspace.workspace().clone();
1318                multi_workspace.add(workspace.clone(), window, cx);
1319                workspace
1320            })
1321        })
1322    }
1323
1324    /// Finds an existing workspace in this multi-workspace whose paths match,
1325    /// or creates a new one (deserializing its saved state from the database).
1326    /// Never searches other windows or matches workspaces with a superset of
1327    /// the requested paths.
1328    ///
1329    /// `excluding` lists workspaces that should be skipped during the search
1330    /// (e.g. workspaces that are about to be removed).
1331    pub fn find_or_create_local_workspace(
1332        &mut self,
1333        path_list: PathList,
1334        project_group: Option<ProjectGroupKey>,
1335        excluding: &[Entity<Workspace>],
1336        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1337        open_mode: OpenMode,
1338        window: &mut Window,
1339        cx: &mut Context<Self>,
1340    ) -> Task<Result<Entity<Workspace>>> {
1341        self.find_or_create_local_workspace_with_source_workspace(
1342            path_list,
1343            project_group,
1344            excluding,
1345            init,
1346            open_mode,
1347            None,
1348            window,
1349            cx,
1350        )
1351    }
1352
1353    pub fn find_or_create_local_workspace_with_source_workspace(
1354        &mut self,
1355        path_list: PathList,
1356        project_group: Option<ProjectGroupKey>,
1357        excluding: &[Entity<Workspace>],
1358        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1359        open_mode: OpenMode,
1360        source_workspace: Option<WeakEntity<Workspace>>,
1361        window: &mut Window,
1362        cx: &mut Context<Self>,
1363    ) -> Task<Result<Entity<Workspace>>> {
1364        if let Some(workspace) = self.workspace_for_paths_excluding(&path_list, None, excluding, cx)
1365        {
1366            self.activate(workspace.clone(), source_workspace, window, cx);
1367            return Task::ready(Ok(workspace));
1368        }
1369
1370        let paths = path_list.paths().to_vec();
1371        let app_state = self.workspace().read(cx).app_state().clone();
1372        let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
1373        let fs = <dyn Fs>::global(cx);
1374        let excluding = excluding.to_vec();
1375
1376        cx.spawn(async move |_this, cx| {
1377            let effective_path_list = if let Some(project_group) = project_group {
1378                let metadata_tasks: Vec<_> = paths
1379                    .iter()
1380                    .map(|path| fs.metadata(path.as_path()))
1381                    .collect();
1382                let metadata_results = futures::future::join_all(metadata_tasks).await;
1383                // Only fall back when every path is definitely absent; real
1384                // filesystem errors should not be treated as "missing".
1385                let all_paths_missing = !paths.is_empty()
1386                    && metadata_results
1387                        .into_iter()
1388                        // Ok(None) means the path is definitely absent
1389                        .all(|result| matches!(result, Ok(None)));
1390
1391                if all_paths_missing {
1392                    project_group.path_list().clone()
1393                } else {
1394                    PathList::new(&paths)
1395                }
1396            } else {
1397                PathList::new(&paths)
1398            };
1399
1400            if let Some(requesting_window) = requesting_window
1401                && let Some(workspace) = requesting_window
1402                    .update(cx, |multi_workspace, window, cx| {
1403                        multi_workspace
1404                            .workspace_for_paths_excluding(
1405                                &effective_path_list,
1406                                None,
1407                                &excluding,
1408                                cx,
1409                            )
1410                            .inspect(|workspace| {
1411                                multi_workspace.activate(
1412                                    workspace.clone(),
1413                                    source_workspace.clone(),
1414                                    window,
1415                                    cx,
1416                                );
1417                            })
1418                    })
1419                    .ok()
1420                    .flatten()
1421            {
1422                return Ok(workspace);
1423            }
1424
1425            let result = cx
1426                .update(|cx| {
1427                    Workspace::new_local(
1428                        effective_path_list.paths().to_vec(),
1429                        app_state,
1430                        requesting_window,
1431                        None,
1432                        init,
1433                        open_mode,
1434                        cx,
1435                    )
1436                })
1437                .await?;
1438            Ok(result.workspace)
1439        })
1440    }
1441
1442    pub fn workspace(&self) -> &Entity<Workspace> {
1443        &self.active_workspace
1444    }
1445
1446    pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
1447        let active_is_retained = self.is_workspace_retained(&self.active_workspace);
1448        self.retained_workspaces
1449            .iter()
1450            .chain(std::iter::once(&self.active_workspace).filter(move |_| !active_is_retained))
1451    }
1452
1453    /// Adds a workspace to this window as persistent without changing which
1454    /// workspace is active. Unlike `activate()`, this always inserts into the
1455    /// persistent list regardless of sidebar state — it's used for system-
1456    /// initiated additions like deserialization and worktree discovery.
1457    pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
1458        if self.is_workspace_retained(&workspace) {
1459            return;
1460        }
1461
1462        if workspace != self.active_workspace {
1463            self.register_workspace(&workspace, window, cx);
1464        }
1465
1466        let key = workspace.read(cx).project_group_key(cx);
1467        self.retain_workspace(workspace, key, cx);
1468        telemetry::event!(
1469            "Workspace Added",
1470            workspace_count = self.retained_workspaces.len()
1471        );
1472        cx.notify();
1473    }
1474
1475    /// Ensures the workspace is in the multiworkspace and makes it the active one.
1476    pub fn activate(
1477        &mut self,
1478        workspace: Entity<Workspace>,
1479        source_workspace: Option<WeakEntity<Workspace>>,
1480        window: &mut Window,
1481        cx: &mut Context<Self>,
1482    ) {
1483        if self.workspace() == &workspace {
1484            self.focus_active_workspace(window, cx);
1485            return;
1486        }
1487
1488        let old_active_workspace = self.active_workspace.clone();
1489        let old_active_was_retained = self.active_workspace_is_retained();
1490        let workspace_was_retained = self.is_workspace_retained(&workspace);
1491        let should_retain_workspaces = self.multi_workspace_enabled(cx);
1492
1493        if should_retain_workspaces && !old_active_was_retained {
1494            let key = old_active_workspace.read(cx).project_group_key(cx);
1495            self.retain_workspace(old_active_workspace.clone(), key, cx);
1496        }
1497
1498        if !workspace_was_retained {
1499            self.register_workspace(&workspace, window, cx);
1500
1501            if should_retain_workspaces {
1502                let key = workspace.read(cx).project_group_key(cx);
1503                self.retain_workspace(workspace.clone(), key, cx);
1504            }
1505        }
1506
1507        self.active_workspace = workspace;
1508        // Publish the new active workspace before anyone reads the shared cell
1509        // to decide who owns the window chrome.
1510        self.active_workspace_id
1511            .set(self.active_workspace.entity_id());
1512
1513        let active_key = self.active_workspace.read(cx).project_group_key(cx);
1514        if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == active_key) {
1515            group.last_active_workspace = Some(self.active_workspace.downgrade());
1516        }
1517
1518        if !should_retain_workspaces && !old_active_was_retained {
1519            self.detach_workspace(&old_active_workspace, cx);
1520        }
1521
1522        // The platform window is shared across all workspaces in this window.
1523        // The previously-active workspace left the title and edited indicator
1524        // reflecting its own state, so re-apply them from the newly-active
1525        // workspace (which is now the chrome owner per `owns_window_chrome`).
1526        self.active_workspace.update(cx, |workspace, cx| {
1527            workspace.refresh_window_state(window, cx);
1528        });
1529
1530        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace });
1531        self.serialize(cx);
1532        self.focus_active_workspace(window, cx);
1533        cx.notify();
1534    }
1535
1536    /// Adds `workspace` as a retained background tab without switching the
1537    /// active workspace to it or moving focus. Mirrors the registration and
1538    /// retention bookkeeping `activate` performs for the incoming workspace,
1539    /// but leaves the currently-active workspace focused.
1540    ///
1541    /// Used when something opens a workspace the user should not be yanked
1542    /// into — e.g. the agent's `create_thread` tool spawning a sibling
1543    /// worktree in the background.
1544    pub fn add_background_workspace(
1545        &mut self,
1546        workspace: Entity<Workspace>,
1547        window: &mut Window,
1548        cx: &mut Context<Self>,
1549    ) {
1550        if self.workspace() == &workspace || self.is_workspace_retained(&workspace) {
1551            return;
1552        }
1553        self.register_workspace(&workspace, window, cx);
1554        let key = workspace.read(cx).project_group_key(cx);
1555        self.retain_workspace(workspace, key, cx);
1556        cx.notify();
1557    }
1558
1559    /// Promotes the currently active workspace to persistent if it is
1560    /// transient, so it is retained across workspace switches even when
1561    /// the sidebar is closed. No-op if the workspace is already persistent.
1562    pub fn retain_active_workspace(&mut self, cx: &mut Context<Self>) {
1563        let workspace = self.active_workspace.clone();
1564        if self.is_workspace_retained(&workspace) {
1565            return;
1566        }
1567
1568        let key = workspace.read(cx).project_group_key(cx);
1569        self.retain_workspace(workspace, key, cx);
1570        self.serialize(cx);
1571        cx.notify();
1572    }
1573
1574    /// Collapses to a single workspace, discarding all groups.
1575    /// Used when multi-workspace is disabled by settings.
1576    fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1577        if self.sidebar_open {
1578            self.close_sidebar(window, cx);
1579        }
1580
1581        let active_workspace = self.active_workspace.clone();
1582        for workspace in self.retained_workspaces.clone() {
1583            if workspace != active_workspace {
1584                self.detach_workspace(&workspace, cx);
1585            }
1586        }
1587
1588        self.retained_workspaces.clear();
1589        self.project_groups.clear();
1590        cx.notify();
1591    }
1592
1593    /// Detaches a workspace: clears session state, DB binding, cached
1594    /// group key, and emits `WorkspaceRemoved`. The DB row is preserved
1595    /// so the workspace still appears in the recent-projects list.
1596    fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1597        self.retained_workspaces
1598            .retain(|retained| retained != workspace);
1599        for group in &mut self.project_groups {
1600            if group
1601                .last_active_workspace
1602                .as_ref()
1603                .and_then(WeakEntity::upgrade)
1604                .as_ref()
1605                == Some(workspace)
1606            {
1607                group.last_active_workspace = None;
1608            }
1609        }
1610        cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
1611        workspace.update(cx, |workspace, _cx| {
1612            workspace.session_id.take();
1613            workspace._schedule_serialize_workspace.take();
1614            workspace._serialize_workspace_task.take();
1615        });
1616
1617        if let Some(workspace_id) = workspace.read(cx).database_id() {
1618            let db = crate::persistence::WorkspaceDb::global(cx);
1619            self.pending_removal_tasks.retain(|task| !task.is_ready());
1620            self.pending_removal_tasks
1621                .push(cx.background_spawn(async move {
1622                    db.set_session_binding(workspace_id, None, None)
1623                        .await
1624                        .log_err();
1625                }));
1626        }
1627    }
1628
1629    fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1630        if self.sidebar_open() {
1631            let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
1632            workspace.update(cx, |workspace, _| {
1633                workspace.set_sidebar_focus_handle(sidebar_focus_handle);
1634            });
1635        }
1636    }
1637
1638    pub fn serialize(&mut self, cx: &mut Context<Self>) {
1639        self._serialize_task = Some(cx.spawn(async move |this, cx| {
1640            let Some((window_id, state)) = this
1641                .read_with(cx, |this, cx| {
1642                    let state = MultiWorkspaceState {
1643                        active_workspace_id: this.workspace().read(cx).database_id(),
1644                        project_groups: this
1645                            .project_groups
1646                            .iter()
1647                            .map(|group| {
1648                                crate::persistence::model::SerializedProjectGroup::from_group(
1649                                    &group.key,
1650                                    group.expanded,
1651                                )
1652                            })
1653                            .collect::<Vec<_>>(),
1654                        sidebar_open: this.sidebar_open,
1655                        sidebar_state: this.sidebar.as_ref().and_then(|s| s.serialized_state(cx)),
1656                    };
1657                    (this.window_id, state)
1658                })
1659                .ok()
1660            else {
1661                return;
1662            };
1663            let kvp = cx.update(|cx| db::kvp::KeyValueStore::global(cx));
1664            crate::persistence::write_multi_workspace_state(&kvp, window_id, state).await;
1665        }));
1666    }
1667
1668    /// Returns the in-flight serialization task (if any) so the caller can
1669    /// await it. Used by the quit handler to ensure pending DB writes
1670    /// complete before the process exits.
1671    pub fn flush_serialization(&mut self) -> Task<()> {
1672        self._serialize_task.take().unwrap_or(Task::ready(()))
1673    }
1674
1675    fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
1676        let mut tasks: Vec<Task<()>> = Vec::new();
1677        if let Some(task) = self._serialize_task.take() {
1678            tasks.push(task);
1679        }
1680        tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
1681
1682        async move {
1683            futures::future::join_all(tasks).await;
1684        }
1685    }
1686
1687    pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
1688        // If a dock panel is zoomed, focus it instead of the center pane.
1689        // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
1690        // which closes the zoomed dock.
1691        let focus_handle = {
1692            let workspace = self.workspace().read(cx);
1693            let mut target = None;
1694            for dock in workspace.all_docks() {
1695                let dock = dock.read(cx);
1696                if dock.is_open() {
1697                    if let Some(panel) = dock.active_panel() {
1698                        if panel.is_zoomed(window, cx) {
1699                            target = Some(panel.panel_focus_handle(cx));
1700                            break;
1701                        }
1702                    }
1703                }
1704            }
1705            target.unwrap_or_else(|| {
1706                let pane = workspace.active_pane().clone();
1707                pane.read(cx).focus_handle(cx)
1708            })
1709        };
1710        window.focus(&focus_handle, cx);
1711    }
1712
1713    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
1714        self.workspace().read(cx).panel::<T>(cx)
1715    }
1716
1717    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
1718        self.workspace().read(cx).active_modal::<V>(cx)
1719    }
1720
1721    pub fn add_panel<T: Panel>(
1722        &mut self,
1723        panel: Entity<T>,
1724        window: &mut Window,
1725        cx: &mut Context<Self>,
1726    ) {
1727        self.workspace().update(cx, |workspace, cx| {
1728            workspace.add_panel(panel, window, cx);
1729        });
1730    }
1731
1732    pub fn focus_panel<T: Panel>(
1733        &mut self,
1734        window: &mut Window,
1735        cx: &mut Context<Self>,
1736    ) -> Option<Entity<T>> {
1737        self.workspace()
1738            .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
1739    }
1740
1741    // used in a test
1742    pub fn toggle_modal<V: ModalView, B>(
1743        &mut self,
1744        window: &mut Window,
1745        cx: &mut Context<Self>,
1746        build: B,
1747    ) where
1748        B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
1749    {
1750        self.workspace().update(cx, |workspace, cx| {
1751            workspace.toggle_modal(window, cx, build);
1752        });
1753    }
1754
1755    pub fn toggle_dock(
1756        &mut self,
1757        dock_side: DockPosition,
1758        window: &mut Window,
1759        cx: &mut Context<Self>,
1760    ) {
1761        self.workspace().update(cx, |workspace, cx| {
1762            workspace.toggle_dock(dock_side, window, cx);
1763        });
1764    }
1765
1766    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
1767        self.workspace().read(cx).active_item_as::<I>(cx)
1768    }
1769
1770    pub fn items_of_type<'a, T: Item>(
1771        &'a self,
1772        cx: &'a App,
1773    ) -> impl 'a + Iterator<Item = Entity<T>> {
1774        self.workspace().read(cx).items_of_type::<T>(cx)
1775    }
1776
1777    pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
1778        self.workspace().read(cx).database_id()
1779    }
1780
1781    pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
1782        let tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
1783            .into_iter()
1784            .filter(|task| !task.is_ready())
1785            .collect();
1786        tasks
1787    }
1788
1789    #[cfg(any(test, feature = "test-support"))]
1790    pub fn test_expand_all_groups(&mut self) {
1791        self.set_all_groups_expanded(true);
1792    }
1793
1794    #[cfg(any(test, feature = "test-support"))]
1795    pub fn assert_project_group_key_integrity(&self, cx: &App) -> anyhow::Result<()> {
1796        let mut retained_ids: collections::HashSet<EntityId> = Default::default();
1797        for workspace in &self.retained_workspaces {
1798            anyhow::ensure!(
1799                retained_ids.insert(workspace.entity_id()),
1800                "workspace {:?} is retained more than once",
1801                workspace.entity_id(),
1802            );
1803
1804            let live_key = workspace.read(cx).project_group_key(cx);
1805            anyhow::ensure!(
1806                self.project_groups
1807                    .iter()
1808                    .any(|group| group.key == live_key),
1809                "workspace {:?} has live key {:?} but no project-group metadata",
1810                workspace.entity_id(),
1811                live_key,
1812            );
1813        }
1814        Ok(())
1815    }
1816
1817    #[cfg(any(test, feature = "test-support"))]
1818    pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
1819        self.workspace().update(cx, |workspace, _cx| {
1820            workspace.set_random_database_id();
1821        });
1822    }
1823
1824    #[cfg(any(test, feature = "test-support"))]
1825    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1826        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1827        Self::new(workspace, window, cx)
1828    }
1829
1830    #[cfg(any(test, feature = "test-support"))]
1831    pub fn test_add_workspace(
1832        &mut self,
1833        project: Entity<Project>,
1834        window: &mut Window,
1835        cx: &mut Context<Self>,
1836    ) -> Entity<Workspace> {
1837        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1838        self.activate(workspace.clone(), None, window, cx);
1839        workspace
1840    }
1841
1842    #[cfg(any(test, feature = "test-support"))]
1843    pub fn test_add_project_group(&mut self, group: ProjectGroup) {
1844        self.project_groups.push(ProjectGroupState {
1845            key: group.key,
1846            expanded: group.expanded,
1847            last_active_workspace: None,
1848        });
1849    }
1850
1851    #[cfg(any(test, feature = "test-support"))]
1852    pub fn create_test_workspace(
1853        &mut self,
1854        window: &mut Window,
1855        cx: &mut Context<Self>,
1856    ) -> Task<()> {
1857        let app_state = self.workspace().read(cx).app_state().clone();
1858        let project = Project::local(
1859            app_state.client.clone(),
1860            app_state.node_runtime.clone(),
1861            app_state.user_store.clone(),
1862            app_state.languages.clone(),
1863            app_state.fs.clone(),
1864            None,
1865            project::LocalProjectFlags::default(),
1866            cx,
1867        );
1868        let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1869        self.activate(new_workspace.clone(), None, window, cx);
1870
1871        let weak_workspace = new_workspace.downgrade();
1872        let db = crate::persistence::WorkspaceDb::global(cx);
1873        cx.spawn_in(window, async move |this, cx| {
1874            let workspace_id = db.next_id().await.unwrap();
1875            let workspace = weak_workspace.upgrade().unwrap();
1876            let task: Task<()> = this
1877                .update_in(cx, |this, window, cx| {
1878                    let session_id = workspace.read(cx).session_id();
1879                    let window_id = window.window_handle().window_id().as_u64();
1880                    workspace.update(cx, |workspace, _cx| {
1881                        workspace.set_database_id(workspace_id);
1882                    });
1883                    this.serialize(cx);
1884                    let db = db.clone();
1885                    cx.background_spawn(async move {
1886                        db.set_session_binding(workspace_id, session_id, Some(window_id))
1887                            .await
1888                            .log_err();
1889                    })
1890                })
1891                .unwrap();
1892            task.await
1893        })
1894    }
1895
1896    /// Assigns random database IDs to all retained workspaces, flushes
1897    /// workspace serialization (SQLite) and multi-workspace state (KVP),
1898    /// and writes session bindings so the serialized data can be read
1899    /// back by `last_session_workspace_locations` +
1900    /// `read_serialized_multi_workspaces`.
1901    #[cfg(any(test, feature = "test-support"))]
1902    pub fn flush_all_serialization(
1903        &mut self,
1904        window: &mut Window,
1905        cx: &mut Context<Self>,
1906    ) -> Vec<Task<()>> {
1907        for workspace in self.workspaces() {
1908            workspace.update(cx, |ws, _cx| {
1909                if ws.database_id().is_none() {
1910                    ws.set_random_database_id();
1911                }
1912            });
1913        }
1914
1915        let session_id = self.workspace().read(cx).session_id();
1916        let window_id_u64 = window.window_handle().window_id().as_u64();
1917
1918        let mut tasks: Vec<Task<()>> = Vec::new();
1919        for workspace in self.workspaces() {
1920            tasks.push(workspace.update(cx, |ws, cx| ws.flush_serialization(window, cx)));
1921            if let Some(db_id) = workspace.read(cx).database_id() {
1922                let db = crate::persistence::WorkspaceDb::global(cx);
1923                let session_id = session_id.clone();
1924                tasks.push(cx.background_spawn(async move {
1925                    db.set_session_binding(db_id, session_id, Some(window_id_u64))
1926                        .await
1927                        .log_err();
1928                }));
1929            }
1930        }
1931        self.serialize(cx);
1932        tasks
1933    }
1934
1935    /// Removes one or more workspaces from this multi-workspace.
1936    ///
1937    /// If the active workspace is among those being removed,
1938    /// `fallback_workspace` is called **synchronously before the removal
1939    /// begins** to produce a `Task` that resolves to the workspace that
1940    /// should become active. The fallback must not be one of the
1941    /// workspaces being removed.
1942    ///
1943    /// Returns `true` if any workspaces were actually removed.
1944    pub fn remove(
1945        &mut self,
1946        workspaces: impl IntoIterator<Item = Entity<Workspace>>,
1947        fallback_workspace: impl FnOnce(
1948            &mut Self,
1949            &mut Window,
1950            &mut Context<Self>,
1951        ) -> Task<Result<Entity<Workspace>>>,
1952        window: &mut Window,
1953        cx: &mut Context<Self>,
1954    ) -> Task<Result<bool>> {
1955        let workspaces: Vec<_> = workspaces.into_iter().collect();
1956
1957        if workspaces.is_empty() {
1958            return Task::ready(Ok(false));
1959        }
1960
1961        let removing_active = workspaces.iter().any(|ws| ws == self.workspace());
1962        let original_active = self.workspace().clone();
1963
1964        let fallback_task = removing_active.then(|| fallback_workspace(self, window, cx));
1965
1966        cx.spawn_in(window, async move |this, cx| {
1967            // Run the standard workspace close lifecycle for every workspace
1968            // being removed from this window. This handles save prompting and
1969            // session cleanup consistently with other replace-in-window flows.
1970            for workspace in &workspaces {
1971                let should_continue = workspace
1972                    .update_in(cx, |workspace, window, cx| {
1973                        workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
1974                    })?
1975                    .await?;
1976
1977                if !should_continue {
1978                    return Ok(false);
1979                }
1980            }
1981
1982            // If we're removing the active workspace, await the
1983            // fallback and switch to it before tearing anything down.
1984            // Otherwise restore the original active workspace in case
1985            // prompting switched away from it.
1986            if let Some(fallback_task) = fallback_task {
1987                let new_active = fallback_task.await?;
1988
1989                this.update_in(cx, |this, window, cx| {
1990                    assert!(
1991                        !workspaces.contains(&new_active),
1992                        "fallback workspace must not be one of the workspaces being removed"
1993                    );
1994                    this.activate(new_active, None, window, cx);
1995                })?;
1996            } else {
1997                this.update_in(cx, |this, window, cx| {
1998                    if *this.workspace() != original_active {
1999                        this.activate(original_active, None, window, cx);
2000                    }
2001                })?;
2002            }
2003
2004            // Actually remove the workspaces.
2005            this.update_in(cx, |this, _, cx| {
2006                let mut removed_any = false;
2007
2008                for workspace in &workspaces {
2009                    let was_retained = this.is_workspace_retained(workspace);
2010                    if was_retained {
2011                        this.detach_workspace(workspace, cx);
2012                        removed_any = true;
2013                    }
2014                }
2015
2016                if removed_any {
2017                    this.serialize(cx);
2018                    cx.notify();
2019                }
2020
2021                Ok(removed_any)
2022            })?
2023        })
2024    }
2025
2026    pub fn open_project(
2027        &mut self,
2028        paths: Vec<PathBuf>,
2029        open_mode: OpenMode,
2030        window: &mut Window,
2031        cx: &mut Context<Self>,
2032    ) -> Task<Result<Entity<Workspace>>> {
2033        if self.multi_workspace_enabled(cx) {
2034            let empty_workspace = if self
2035                .active_workspace
2036                .read(cx)
2037                .project()
2038                .read(cx)
2039                .visible_worktrees(cx)
2040                .next()
2041                .is_none()
2042            {
2043                Some(self.active_workspace.clone())
2044            } else {
2045                None
2046            };
2047
2048            cx.spawn_in(window, async move |this, cx| {
2049                if let Some(empty_workspace) = empty_workspace.as_ref() {
2050                    let should_continue = empty_workspace
2051                        .update_in(cx, |workspace, window, cx| {
2052                            workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
2053                        })?
2054                        .await?;
2055                    if !should_continue {
2056                        return Ok(empty_workspace.clone());
2057                    }
2058                }
2059
2060                let create_task = this.update_in(cx, |this, window, cx| {
2061                    this.find_or_create_local_workspace(
2062                        PathList::new(&paths),
2063                        None,
2064                        empty_workspace.as_slice(),
2065                        None,
2066                        OpenMode::Activate,
2067                        window,
2068                        cx,
2069                    )
2070                })?;
2071                let new_workspace = create_task.await?;
2072
2073                if let Some(empty_workspace) = empty_workspace {
2074                    this.update(cx, |this, cx| {
2075                        if this.is_workspace_retained(&empty_workspace) {
2076                            this.detach_workspace(&empty_workspace, cx);
2077                        }
2078                    })?;
2079                }
2080
2081                Ok(new_workspace)
2082            })
2083        } else {
2084            let workspace = self.workspace().clone();
2085            cx.spawn_in(window, async move |_this, cx| {
2086                let should_continue = workspace
2087                    .update_in(cx, |workspace, window, cx| {
2088                        workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
2089                    })?
2090                    .await?;
2091                if should_continue {
2092                    workspace
2093                        .update_in(cx, |workspace, window, cx| {
2094                            workspace.open_workspace_for_paths(open_mode, paths, window, cx)
2095                        })?
2096                        .await
2097                } else {
2098                    Ok(workspace)
2099                }
2100            })
2101        }
2102    }
2103}
2104
2105impl Render for MultiWorkspace {
2106    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2107        let multi_workspace_enabled = self.multi_workspace_enabled(cx);
2108        let sidebar_side = self.sidebar_side(cx);
2109        let sidebar_on_right = sidebar_side == SidebarSide::Right;
2110
2111        let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
2112            self.sidebar.as_ref().map(|sidebar_handle| {
2113                let weak = cx.weak_entity();
2114
2115                let sidebar_width = sidebar_handle.width(cx);
2116                let resize_handle = deferred(
2117                    div()
2118                        .id("sidebar-resize-handle")
2119                        .absolute()
2120                        .when(!sidebar_on_right, |el| {
2121                            el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
2122                        })
2123                        .when(sidebar_on_right, |el| {
2124                            el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
2125                        })
2126                        .top(px(0.))
2127                        .h_full()
2128                        .w(SIDEBAR_RESIZE_HANDLE_SIZE)
2129                        .cursor_col_resize()
2130                        .on_drag(DraggedSidebar, |dragged, _, _, cx| {
2131                            cx.stop_propagation();
2132                            cx.new(|_| dragged.clone())
2133                        })
2134                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
2135                            cx.stop_propagation();
2136                        })
2137                        .on_mouse_up(MouseButton::Left, move |event, _, cx| {
2138                            if event.click_count == 2 {
2139                                weak.update(cx, |this, cx| {
2140                                    if let Some(sidebar) = this.sidebar.as_mut() {
2141                                        sidebar.set_width(None, cx);
2142                                    }
2143                                    this.serialize(cx);
2144                                })
2145                                .ok();
2146                                cx.stop_propagation();
2147                            } else {
2148                                weak.update(cx, |this, cx| {
2149                                    this.serialize(cx);
2150                                })
2151                                .ok();
2152                            }
2153                        })
2154                        .occlude(),
2155                );
2156
2157                div()
2158                    .id("sidebar-container")
2159                    .relative()
2160                    .h_full()
2161                    .w(sidebar_width)
2162                    .flex_shrink_0()
2163                    .child(sidebar_handle.to_any())
2164                    .child(resize_handle)
2165                    .into_any_element()
2166            })
2167        } else {
2168            None
2169        };
2170
2171        let (left_sidebar, right_sidebar) = if sidebar_on_right {
2172            (None, sidebar)
2173        } else {
2174            (sidebar, None)
2175        };
2176
2177        let ui_font = theme_settings::setup_ui_font(window, cx);
2178        let text_color = cx.theme().colors().text;
2179
2180        let workspace = self.workspace().clone();
2181        let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
2182        let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
2183
2184        client_side_decorations(
2185            root.key_context(workspace_key_context)
2186                .relative()
2187                .size_full()
2188                .font(ui_font)
2189                .text_color(text_color)
2190                .on_action(cx.listener(Self::close_window))
2191                .when(self.multi_workspace_enabled(cx), |this| {
2192                    this.on_action(cx.listener(
2193                        |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
2194                            this.toggle_sidebar(window, cx);
2195                        },
2196                    ))
2197                    .on_action(cx.listener(
2198                        |this: &mut Self, _: &CloseWorkspaceSidebar, window, cx| {
2199                            this.close_sidebar_action(window, cx);
2200                        },
2201                    ))
2202                    .on_action(cx.listener(
2203                        |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
2204                            this.focus_sidebar(window, cx);
2205                        },
2206                    ))
2207                    .on_action(cx.listener(
2208                        |this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
2209                            if let Some(sidebar) = &this.sidebar {
2210                                sidebar.toggle_thread_switcher(action.select_last, window, cx);
2211                            }
2212                        },
2213                    ))
2214                    .on_action(cx.listener(|this: &mut Self, _: &NextProject, window, cx| {
2215                        if let Some(sidebar) = &this.sidebar {
2216                            sidebar.cycle_project(true, window, cx);
2217                        }
2218                    }))
2219                    .on_action(
2220                        cx.listener(|this: &mut Self, _: &PreviousProject, window, cx| {
2221                            if let Some(sidebar) = &this.sidebar {
2222                                sidebar.cycle_project(false, window, cx);
2223                            }
2224                        }),
2225                    )
2226                    .on_action(cx.listener(|this: &mut Self, _: &NextThread, window, cx| {
2227                        if let Some(sidebar) = &this.sidebar {
2228                            sidebar.cycle_thread(true, window, cx);
2229                        }
2230                    }))
2231                    .on_action(
2232                        cx.listener(|this: &mut Self, _: &PreviousThread, window, cx| {
2233                            if let Some(sidebar) = &this.sidebar {
2234                                sidebar.cycle_thread(false, window, cx);
2235                            }
2236                        }),
2237                    )
2238                    .when(self.project_group_keys().len() >= 2, |el| {
2239                        el.on_action(cx.listener(
2240                            |this: &mut Self, _: &MoveProjectToNewWindow, window, cx| {
2241                                let key =
2242                                    this.project_group_key_for_workspace(this.workspace(), cx);
2243                                this.open_project_group_in_new_window(&key, window, cx)
2244                                    .detach_and_log_err(cx);
2245                            },
2246                        ))
2247                    })
2248                })
2249                .when(
2250                    self.sidebar_open() && self.multi_workspace_enabled(cx),
2251                    |this| {
2252                        this.on_drag_move(cx.listener(
2253                            move |this: &mut Self,
2254                                  e: &DragMoveEvent<DraggedSidebar>,
2255                                  window,
2256                                  cx| {
2257                                if let Some(sidebar) = &this.sidebar {
2258                                    let new_width = if sidebar_on_right {
2259                                        window.bounds().size.width - e.event.position.x
2260                                    } else {
2261                                        e.event.position.x
2262                                    };
2263                                    sidebar.set_width(Some(new_width), cx);
2264                                }
2265                            },
2266                        ))
2267                    },
2268                )
2269                .children(left_sidebar)
2270                .child(
2271                    div()
2272                        .flex()
2273                        .flex_1()
2274                        .size_full()
2275                        .overflow_hidden()
2276                        .child(self.workspace().clone()),
2277                )
2278                .children(right_sidebar)
2279                .child(self.workspace().read(cx).modal_layer.clone())
2280                .children(self.sidebar_overlay.as_ref().map(|view| {
2281                    deferred(div().absolute().size_full().inset_0().occlude().child(
2282                        v_flex().h(px(0.0)).top_20().items_center().child(
2283                            h_flex().occlude().child(view.clone()).on_mouse_down(
2284                                MouseButton::Left,
2285                                |_, _, cx| {
2286                                    cx.stop_propagation();
2287                                },
2288                            ),
2289                        ),
2290                    ))
2291                    .with_priority(2)
2292                })),
2293            window,
2294            cx,
2295        )
2296    }
2297}
2298
Served at tenant.openagents/omega Member data and write actions are omitted.