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