multi_workspace.rs

  1use anyhow::Result;
  2use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
  3use gpui::{
  4    AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
  5    ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
  6    actions, deferred, px,
  7};
  8use project::{DisableAiSettings, Project};
  9use settings::Settings;
 10use std::future::Future;
 11use std::path::PathBuf;
 12use ui::prelude::*;
 13use util::ResultExt;
 14
 15const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
 16
 17use crate::{
 18    CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast,
 19    Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId,
 20};
 21
 22actions!(
 23    multi_workspace,
 24    [
 25        /// Creates a new workspace within the current window.
 26        NewWorkspaceInWindow,
 27        /// Switches to the next workspace within the current window.
 28        NextWorkspaceInWindow,
 29        /// Switches to the previous workspace within the current window.
 30        PreviousWorkspaceInWindow,
 31        /// Toggles the workspace switcher sidebar.
 32        ToggleWorkspaceSidebar,
 33        /// Moves focus to or from the workspace sidebar without closing it.
 34        FocusWorkspaceSidebar,
 35    ]
 36);
 37
 38pub enum MultiWorkspaceEvent {
 39    ActiveWorkspaceChanged,
 40    WorkspaceAdded(Entity<Workspace>),
 41    WorkspaceRemoved(EntityId),
 42    SingletonModeChanged,
 43}
 44
 45pub enum SidebarEvent {
 46    Open,
 47    Close,
 48}
 49
 50pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
 51    fn width(&self, cx: &App) -> Pixels;
 52    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
 53    fn has_notifications(&self, cx: &App) -> bool;
 54}
 55
 56pub trait SidebarHandle: 'static + Send + Sync {
 57    fn width(&self, cx: &App) -> Pixels;
 58    fn set_width(&self, width: Option<Pixels>, cx: &mut App);
 59    fn focus_handle(&self, cx: &App) -> FocusHandle;
 60    fn focus(&self, window: &mut Window, cx: &mut App);
 61    fn has_notifications(&self, cx: &App) -> bool;
 62    fn to_any(&self) -> AnyView;
 63    fn entity_id(&self) -> EntityId;
 64}
 65
 66#[derive(Clone)]
 67pub struct DraggedSidebar;
 68
 69impl Render for DraggedSidebar {
 70    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 71        gpui::Empty
 72    }
 73}
 74
 75impl<T: Sidebar> SidebarHandle for Entity<T> {
 76    fn width(&self, cx: &App) -> Pixels {
 77        self.read(cx).width(cx)
 78    }
 79
 80    fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
 81        self.update(cx, |this, cx| this.set_width(width, cx))
 82    }
 83
 84    fn focus_handle(&self, cx: &App) -> FocusHandle {
 85        self.read(cx).focus_handle(cx)
 86    }
 87
 88    fn focus(&self, window: &mut Window, cx: &mut App) {
 89        let handle = self.read(cx).focus_handle(cx);
 90        window.focus(&handle, cx);
 91    }
 92
 93    fn has_notifications(&self, cx: &App) -> bool {
 94        self.read(cx).has_notifications(cx)
 95    }
 96
 97    fn to_any(&self) -> AnyView {
 98        self.clone().into()
 99    }
100
101    fn entity_id(&self) -> EntityId {
102        Entity::entity_id(self)
103    }
104}
105
106pub struct MultiWorkspace {
107    window_id: WindowId,
108    workspaces: Vec<Entity<Workspace>>,
109    active_workspace_index: usize,
110    sidebar: Option<Box<dyn SidebarHandle>>,
111    sidebar_open: bool,
112    is_singleton: bool,
113    _sidebar_subscription: Option<Subscription>,
114    pending_removal_tasks: Vec<Task<()>>,
115    _serialize_task: Option<Task<()>>,
116    _create_task: Option<Task<()>>,
117    _subscriptions: Vec<Subscription>,
118}
119
120impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
121
122impl MultiWorkspace {
123    pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
124        let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
125            if let Some(task) = this._serialize_task.take() {
126                task.detach();
127            }
128            if let Some(task) = this._create_task.take() {
129                task.detach();
130            }
131            for task in std::mem::take(&mut this.pending_removal_tasks) {
132                task.detach();
133            }
134        });
135        let quit_subscription = cx.on_app_quit(Self::app_will_quit);
136        let settings_subscription =
137            cx.observe_global_in::<settings::SettingsStore>(window, |this, window, cx| {
138                if DisableAiSettings::get_global(cx).disable_ai && this.sidebar_open {
139                    this.close_sidebar(window, cx);
140                }
141            });
142        Self::subscribe_to_workspace(&workspace, cx);
143        Self {
144            window_id: window.window_handle().window_id(),
145            workspaces: vec![workspace],
146            active_workspace_index: 0,
147            sidebar: None,
148            sidebar_open: false,
149            is_singleton: true,
150            _sidebar_subscription: None,
151            pending_removal_tasks: Vec::new(),
152            _serialize_task: None,
153            _create_task: None,
154            _subscriptions: vec![
155                release_subscription,
156                quit_subscription,
157                settings_subscription,
158            ],
159        }
160    }
161
162    pub fn register_sidebar<T: Sidebar>(
163        &mut self,
164        sidebar: Entity<T>,
165        window: &mut Window,
166        cx: &mut Context<Self>,
167    ) {
168        let subscription =
169            cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
170                SidebarEvent::Open => this.toggle_sidebar(window, cx),
171                SidebarEvent::Close => {
172                    this.close_sidebar(window, cx);
173                }
174            });
175        self.sidebar = Some(Box::new(sidebar));
176        self._sidebar_subscription = Some(subscription);
177    }
178
179    pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
180        self.sidebar.as_deref()
181    }
182
183    pub fn sidebar_open(&self) -> bool {
184        self.sidebar_open && self.sidebar.is_some()
185    }
186
187    pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
188        self.sidebar
189            .as_ref()
190            .map_or(false, |s| s.has_notifications(cx))
191    }
192
193    pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
194        cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
195    }
196
197    pub fn is_singleton(&self) -> bool {
198        self.is_singleton
199    }
200
201    pub fn set_singleton(
202        &mut self,
203        is_singleton: bool,
204        window: &mut Window,
205        cx: &mut Context<Self>,
206    ) {
207        if self.is_singleton == is_singleton {
208            return;
209        }
210        self.is_singleton = is_singleton;
211        if is_singleton && self.sidebar_open {
212            self.close_sidebar(window, cx);
213        }
214        cx.emit(MultiWorkspaceEvent::SingletonModeChanged);
215        cx.notify();
216    }
217
218    pub fn set_left_dock_expanded_mode(&mut self, agent_mode: bool, cx: &mut Context<Self>) {
219        self.workspace().update(cx, |workspace, cx| {
220            workspace.set_left_dock_expanded_mode(agent_mode, cx);
221        });
222        cx.notify();
223    }
224
225    pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
226        if !self.multi_workspace_enabled(cx) || self.is_singleton {
227            return;
228        }
229
230        if self.sidebar_open {
231            self.close_sidebar(window, cx);
232        } else {
233            self.open_sidebar(cx);
234            if let Some(sidebar) = &self.sidebar {
235                sidebar.focus(window, cx);
236            }
237        }
238    }
239
240    pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
241        if !self.multi_workspace_enabled(cx) || self.is_singleton {
242            return;
243        }
244
245        if self.sidebar_open {
246            let sidebar_is_focused = self
247                .sidebar
248                .as_ref()
249                .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
250
251            if sidebar_is_focused {
252                let pane = self.workspace().read(cx).active_pane().clone();
253                let pane_focus = pane.read(cx).focus_handle(cx);
254                window.focus(&pane_focus, cx);
255            } else if let Some(sidebar) = &self.sidebar {
256                sidebar.focus(window, cx);
257            }
258        } else {
259            self.open_sidebar(cx);
260            if let Some(sidebar) = &self.sidebar {
261                sidebar.focus(window, cx);
262            }
263        }
264    }
265
266    pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
267        if self.is_singleton {
268            return;
269        }
270        self.sidebar_open = true;
271        for workspace in &self.workspaces {
272            workspace.update(cx, |workspace, cx| {
273                workspace.set_workspace_sidebar_open(true, cx);
274            });
275        }
276        self.serialize(cx);
277        cx.notify();
278    }
279
280    fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
281        self.sidebar_open = false;
282        for workspace in &self.workspaces {
283            workspace.update(cx, |workspace, cx| {
284                workspace.set_workspace_sidebar_open(false, cx);
285            });
286        }
287        let pane = self.workspace().read(cx).active_pane().clone();
288        let pane_focus = pane.read(cx).focus_handle(cx);
289        window.focus(&pane_focus, cx);
290        self.serialize(cx);
291        cx.notify();
292    }
293
294    pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
295        cx.spawn_in(window, async move |this, cx| {
296            let workspaces = this.update(cx, |multi_workspace, _cx| {
297                multi_workspace.workspaces().to_vec()
298            })?;
299
300            for workspace in workspaces {
301                let should_continue = workspace
302                    .update_in(cx, |workspace, window, cx| {
303                        workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
304                    })?
305                    .await?;
306                if !should_continue {
307                    return anyhow::Ok(());
308                }
309            }
310
311            cx.update(|window, _cx| {
312                window.remove_window();
313            })?;
314
315            anyhow::Ok(())
316        })
317        .detach_and_log_err(cx);
318    }
319
320    fn subscribe_to_workspace(workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
321        cx.subscribe(workspace, |this, workspace, event, cx| {
322            if let WorkspaceEvent::Activate = event {
323                this.activate(workspace, cx);
324            }
325        })
326        .detach();
327    }
328
329    pub fn is_sidebar_open(&self) -> bool {
330        self.sidebar_open
331    }
332
333    pub fn workspace(&self) -> &Entity<Workspace> {
334        &self.workspaces[self.active_workspace_index]
335    }
336
337    pub fn workspaces(&self) -> &[Entity<Workspace>] {
338        &self.workspaces
339    }
340
341    pub fn active_workspace_index(&self) -> usize {
342        self.active_workspace_index
343    }
344
345    pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
346        if !self.multi_workspace_enabled(cx) {
347            self.workspaces[0] = workspace;
348            self.active_workspace_index = 0;
349            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
350            cx.notify();
351            return;
352        }
353
354        let old_index = self.active_workspace_index;
355        let new_index = self.set_active_workspace(workspace, cx);
356        if old_index != new_index {
357            self.serialize(cx);
358        }
359    }
360
361    fn set_active_workspace(
362        &mut self,
363        workspace: Entity<Workspace>,
364        cx: &mut Context<Self>,
365    ) -> usize {
366        let index = self.add_workspace(workspace, cx);
367        let changed = self.active_workspace_index != index;
368        self.active_workspace_index = index;
369        if changed {
370            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
371        }
372        cx.notify();
373        index
374    }
375
376    /// Adds a workspace to this window without changing which workspace is active.
377    /// Returns the index of the workspace (existing or newly inserted).
378    pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
379        if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
380            index
381        } else {
382            if self.sidebar_open {
383                workspace.update(cx, |workspace, cx| {
384                    workspace.set_workspace_sidebar_open(true, cx);
385                });
386            }
387            Self::subscribe_to_workspace(&workspace, cx);
388            self.workspaces.push(workspace.clone());
389            cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
390            cx.notify();
391            self.workspaces.len() - 1
392        }
393    }
394
395    pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
396        debug_assert!(
397            index < self.workspaces.len(),
398            "workspace index out of bounds"
399        );
400        let changed = self.active_workspace_index != index;
401        self.active_workspace_index = index;
402        self.serialize(cx);
403        self.focus_active_workspace(window, cx);
404        if changed {
405            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
406        }
407        cx.notify();
408    }
409
410    pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
411        if self.workspaces.len() > 1 {
412            let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
413            self.activate_index(next_index, window, cx);
414        }
415    }
416
417    pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
418        if self.workspaces.len() > 1 {
419            let prev_index = if self.active_workspace_index == 0 {
420                self.workspaces.len() - 1
421            } else {
422                self.active_workspace_index - 1
423            };
424            self.activate_index(prev_index, window, cx);
425        }
426    }
427
428    fn serialize(&mut self, cx: &mut App) {
429        let window_id = self.window_id;
430        let state = crate::persistence::model::MultiWorkspaceState {
431            active_workspace_id: self.workspace().read(cx).database_id(),
432            sidebar_open: self.sidebar_open,
433        };
434        self._serialize_task = Some(cx.background_spawn(async move {
435            crate::persistence::write_multi_workspace_state(window_id, state).await;
436        }));
437    }
438
439    /// Returns the in-flight serialization task (if any) so the caller can
440    /// await it. Used by the quit handler to ensure pending DB writes
441    /// complete before the process exits.
442    pub fn flush_serialization(&mut self) -> Task<()> {
443        self._serialize_task.take().unwrap_or(Task::ready(()))
444    }
445
446    fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
447        let mut tasks: Vec<Task<()>> = Vec::new();
448        if let Some(task) = self._serialize_task.take() {
449            tasks.push(task);
450        }
451        if let Some(task) = self._create_task.take() {
452            tasks.push(task);
453        }
454        tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
455
456        async move {
457            futures::future::join_all(tasks).await;
458        }
459    }
460
461    pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
462        // If a dock panel is zoomed, focus it instead of the center pane.
463        // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
464        // which closes the zoomed dock.
465        let focus_handle = {
466            let workspace = self.workspace().read(cx);
467            let mut target = None;
468            for dock in workspace.all_docks() {
469                let dock = dock.read(cx);
470                if dock.is_open() {
471                    if let Some(panel) = dock.active_panel() {
472                        if panel.is_zoomed(window, cx) {
473                            target = Some(panel.panel_focus_handle(cx));
474                            break;
475                        }
476                    }
477                }
478            }
479            target.unwrap_or_else(|| {
480                let pane = workspace.active_pane().clone();
481                pane.read(cx).focus_handle(cx)
482            })
483        };
484        window.focus(&focus_handle, cx);
485    }
486
487    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
488        self.workspace().read(cx).panel::<T>(cx)
489    }
490
491    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
492        self.workspace().read(cx).active_modal::<V>(cx)
493    }
494
495    pub fn add_panel<T: Panel>(
496        &mut self,
497        panel: Entity<T>,
498        position: DockPosition,
499        window: &mut Window,
500        cx: &mut Context<Self>,
501    ) {
502        self.workspace().update(cx, |workspace, cx| {
503            workspace.add_panel(panel, position, window, cx);
504        });
505    }
506
507    pub fn focus_panel<T: Panel>(
508        &mut self,
509        window: &mut Window,
510        cx: &mut Context<Self>,
511    ) -> Option<Entity<T>> {
512        self.workspace()
513            .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
514    }
515
516    // used in a test
517    pub fn toggle_modal<V: ModalView, B>(
518        &mut self,
519        window: &mut Window,
520        cx: &mut Context<Self>,
521        build: B,
522    ) where
523        B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
524    {
525        self.workspace().update(cx, |workspace, cx| {
526            workspace.toggle_modal(window, cx, build);
527        });
528    }
529
530    pub fn toggle_dock(
531        &mut self,
532        dock_side: DockPosition,
533        window: &mut Window,
534        cx: &mut Context<Self>,
535    ) {
536        self.workspace().update(cx, |workspace, cx| {
537            workspace.toggle_dock(dock_side, window, cx);
538        });
539    }
540
541    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
542        self.workspace().read(cx).active_item_as::<I>(cx)
543    }
544
545    pub fn items_of_type<'a, T: Item>(
546        &'a self,
547        cx: &'a App,
548    ) -> impl 'a + Iterator<Item = Entity<T>> {
549        self.workspace().read(cx).items_of_type::<T>(cx)
550    }
551
552    pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
553        self.workspace().read(cx).database_id()
554    }
555
556    pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
557        let mut tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
558            .into_iter()
559            .filter(|task| !task.is_ready())
560            .collect();
561        if let Some(task) = self._create_task.take() {
562            if !task.is_ready() {
563                tasks.push(task);
564            }
565        }
566        tasks
567    }
568
569    #[cfg(any(test, feature = "test-support"))]
570    pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
571        self.workspace().update(cx, |workspace, _cx| {
572            workspace.set_random_database_id();
573        });
574    }
575
576    #[cfg(any(test, feature = "test-support"))]
577    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
578        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
579        Self::new(workspace, window, cx)
580    }
581
582    #[cfg(any(test, feature = "test-support"))]
583    pub fn test_add_workspace(
584        &mut self,
585        project: Entity<Project>,
586        window: &mut Window,
587        cx: &mut Context<Self>,
588    ) -> Entity<Workspace> {
589        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
590        self.activate(workspace.clone(), cx);
591        workspace
592    }
593
594    pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
595        if !self.multi_workspace_enabled(cx) {
596            return;
597        }
598        let app_state = self.workspace().read(cx).app_state().clone();
599        let project = Project::local(
600            app_state.client.clone(),
601            app_state.node_runtime.clone(),
602            app_state.user_store.clone(),
603            app_state.languages.clone(),
604            app_state.fs.clone(),
605            None,
606            project::LocalProjectFlags::default(),
607            cx,
608        );
609        let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
610        self.set_active_workspace(new_workspace.clone(), cx);
611        self.focus_active_workspace(window, cx);
612
613        let weak_workspace = new_workspace.downgrade();
614        self._create_task = Some(cx.spawn_in(window, async move |this, cx| {
615            let result = crate::persistence::DB.next_id().await;
616            this.update_in(cx, |this, window, cx| match result {
617                Ok(workspace_id) => {
618                    if let Some(workspace) = weak_workspace.upgrade() {
619                        let session_id = workspace.read(cx).session_id();
620                        let window_id = window.window_handle().window_id().as_u64();
621                        workspace.update(cx, |workspace, _cx| {
622                            workspace.set_database_id(workspace_id);
623                        });
624                        cx.background_spawn(async move {
625                            crate::persistence::DB
626                                .set_session_binding(workspace_id, session_id, Some(window_id))
627                                .await
628                                .log_err();
629                        })
630                        .detach();
631                    } else {
632                        cx.background_spawn(async move {
633                            crate::persistence::DB
634                                .delete_workspace_by_id(workspace_id)
635                                .await
636                                .log_err();
637                        })
638                        .detach();
639                    }
640                    this.serialize(cx);
641                }
642                Err(error) => {
643                    log::error!("Failed to create workspace: {error:#}");
644                    if let Some(index) = weak_workspace
645                        .upgrade()
646                        .and_then(|w| this.workspaces.iter().position(|ws| *ws == w))
647                    {
648                        this.remove_workspace(index, window, cx);
649                    }
650                    this.workspace().update(cx, |workspace, cx| {
651                        let id = NotificationId::unique::<MultiWorkspace>();
652                        workspace.show_toast(
653                            Toast::new(id, format!("Failed to create workspace: {error}")),
654                            cx,
655                        );
656                    });
657                }
658            })
659            .log_err();
660        }));
661    }
662
663    pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
664        if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
665            return;
666        }
667
668        let removed_workspace = self.workspaces.remove(index);
669
670        if self.active_workspace_index >= self.workspaces.len() {
671            self.active_workspace_index = self.workspaces.len() - 1;
672        } else if self.active_workspace_index > index {
673            self.active_workspace_index -= 1;
674        }
675
676        if let Some(workspace_id) = removed_workspace.read(cx).database_id() {
677            self.pending_removal_tasks.retain(|task| !task.is_ready());
678            self.pending_removal_tasks
679                .push(cx.background_spawn(async move {
680                    crate::persistence::DB
681                        .delete_workspace_by_id(workspace_id)
682                        .await
683                        .log_err();
684                }));
685        }
686
687        self.serialize(cx);
688        self.focus_active_workspace(window, cx);
689        cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(
690            removed_workspace.entity_id(),
691        ));
692        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
693        cx.notify();
694    }
695
696    pub fn open_project(
697        &mut self,
698        paths: Vec<PathBuf>,
699        window: &mut Window,
700        cx: &mut Context<Self>,
701    ) -> Task<Result<()>> {
702        let workspace = self.workspace().clone();
703
704        if self.multi_workspace_enabled(cx) {
705            workspace.update(cx, |workspace, cx| {
706                workspace.open_workspace_for_paths(true, paths, window, cx)
707            })
708        } else {
709            cx.spawn_in(window, async move |_this, cx| {
710                let should_continue = workspace
711                    .update_in(cx, |workspace, window, cx| {
712                        workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
713                    })?
714                    .await?;
715                if should_continue {
716                    workspace
717                        .update_in(cx, |workspace, window, cx| {
718                            workspace.open_workspace_for_paths(true, paths, window, cx)
719                        })?
720                        .await
721                } else {
722                    Ok(())
723                }
724            })
725        }
726    }
727}
728
729impl Render for MultiWorkspace {
730    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
731        let multi_workspace_enabled = self.multi_workspace_enabled(cx);
732
733        let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
734            self.sidebar.as_ref().map(|sidebar_handle| {
735                let weak = cx.weak_entity();
736
737                let sidebar_width = sidebar_handle.width(cx);
738                let resize_handle = deferred(
739                    div()
740                        .id("sidebar-resize-handle")
741                        .absolute()
742                        .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
743                        .top(px(0.))
744                        .h_full()
745                        .w(SIDEBAR_RESIZE_HANDLE_SIZE)
746                        .cursor_col_resize()
747                        .on_drag(DraggedSidebar, |dragged, _, _, cx| {
748                            cx.stop_propagation();
749                            cx.new(|_| dragged.clone())
750                        })
751                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
752                            cx.stop_propagation();
753                        })
754                        .on_mouse_up(MouseButton::Left, move |event, _, cx| {
755                            if event.click_count == 2 {
756                                weak.update(cx, |this, cx| {
757                                    if let Some(sidebar) = this.sidebar.as_mut() {
758                                        sidebar.set_width(None, cx);
759                                    }
760                                })
761                                .ok();
762                                cx.stop_propagation();
763                            }
764                        })
765                        .occlude(),
766                );
767
768                div()
769                    .id("sidebar-container")
770                    .relative()
771                    .h_full()
772                    .w(sidebar_width)
773                    .flex_shrink_0()
774                    .child(sidebar_handle.to_any())
775                    .child(resize_handle)
776                    .into_any_element()
777            })
778        } else {
779            None
780        };
781
782        let ui_font = theme::setup_ui_font(window, cx);
783        let text_color = cx.theme().colors().text;
784
785        let workspace = self.workspace().clone();
786        let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
787        let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
788
789        client_side_decorations(
790            root.key_context(workspace_key_context)
791                .relative()
792                .size_full()
793                .font(ui_font)
794                .text_color(text_color)
795                .on_action(cx.listener(Self::close_window))
796                .on_action(
797                    cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
798                        this.create_workspace(window, cx);
799                    }),
800                )
801                .on_action(
802                    cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
803                        this.activate_next_workspace(window, cx);
804                    }),
805                )
806                .on_action(cx.listener(
807                    |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
808                        this.activate_previous_workspace(window, cx);
809                    },
810                ))
811                .when(self.multi_workspace_enabled(cx), |this| {
812                    this.on_action(cx.listener(
813                        |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
814                            this.toggle_sidebar(window, cx);
815                        },
816                    ))
817                    .on_action(cx.listener(
818                        |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
819                            this.focus_sidebar(window, cx);
820                        },
821                    ))
822                })
823                .when(
824                    self.sidebar_open() && self.multi_workspace_enabled(cx),
825                    |this| {
826                        this.on_drag_move(cx.listener(
827                            |this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
828                                if let Some(sidebar) = &this.sidebar {
829                                    let new_width = e.event.position.x;
830                                    sidebar.set_width(Some(new_width), cx);
831                                }
832                            },
833                        ))
834                        .children(sidebar)
835                    },
836                )
837                .child(
838                    div()
839                        .flex()
840                        .flex_1()
841                        .size_full()
842                        .overflow_hidden()
843                        .child(self.workspace().clone()),
844                )
845                .child(self.workspace().read(cx).modal_layer.clone()),
846            window,
847            cx,
848            Tiling {
849                left: multi_workspace_enabled && self.sidebar_open,
850                ..Tiling::default()
851            },
852        )
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859    use fs::FakeFs;
860    use gpui::TestAppContext;
861    use settings::SettingsStore;
862
863    fn init_test(cx: &mut TestAppContext) {
864        cx.update(|cx| {
865            let settings_store = SettingsStore::test(cx);
866            cx.set_global(settings_store);
867            theme::init(theme::LoadThemes::JustBase, cx);
868            DisableAiSettings::register(cx);
869            cx.update_flags(false, vec!["agent-v2".into()]);
870        });
871    }
872
873    #[gpui::test]
874    async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
875        init_test(cx);
876        let fs = FakeFs::new(cx.executor());
877        let project = Project::test(fs, [], cx).await;
878
879        let (multi_workspace, cx) =
880            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
881
882        multi_workspace.read_with(cx, |mw, cx| {
883            assert!(mw.multi_workspace_enabled(cx));
884        });
885
886        multi_workspace.update_in(cx, |mw, _window, cx| {
887            mw.open_sidebar(cx);
888            assert!(mw.is_sidebar_open());
889        });
890
891        cx.update(|_window, cx| {
892            DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
893        });
894        cx.run_until_parked();
895
896        multi_workspace.read_with(cx, |mw, cx| {
897            assert!(
898                !mw.is_sidebar_open(),
899                "Sidebar should be closed when disable_ai is true"
900            );
901            assert!(
902                !mw.multi_workspace_enabled(cx),
903                "Multi-workspace should be disabled when disable_ai is true"
904            );
905        });
906
907        multi_workspace.update_in(cx, |mw, window, cx| {
908            mw.toggle_sidebar(window, cx);
909        });
910        multi_workspace.read_with(cx, |mw, _cx| {
911            assert!(
912                !mw.is_sidebar_open(),
913                "Sidebar should remain closed when toggled with disable_ai true"
914            );
915        });
916
917        cx.update(|_window, cx| {
918            DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
919        });
920        cx.run_until_parked();
921
922        multi_workspace.read_with(cx, |mw, cx| {
923            assert!(
924                mw.multi_workspace_enabled(cx),
925                "Multi-workspace should be enabled after re-enabling AI"
926            );
927            assert!(
928                !mw.is_sidebar_open(),
929                "Sidebar should still be closed after re-enabling AI (not auto-opened)"
930            );
931        });
932
933        multi_workspace.update_in(cx, |mw, window, cx| {
934            mw.toggle_sidebar(window, cx);
935        });
936        multi_workspace.read_with(cx, |mw, _cx| {
937            assert!(
938                mw.is_sidebar_open(),
939                "Sidebar should open when toggled after re-enabling AI"
940            );
941        });
942    }
943}