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