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