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