title_bar.rs

   1mod application_menu;
   2pub mod collab;
   3mod onboarding_banner;
   4mod plan_chip;
   5mod title_bar_settings;
   6mod update_version;
   7
   8#[cfg(feature = "stories")]
   9mod stories;
  10
  11use crate::application_menu::{ApplicationMenu, show_menus};
  12use crate::plan_chip::PlanChip;
  13pub use platform_title_bar::{
  14    self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar,
  15    ShowNextWindowTab, ShowPreviousWindowTab,
  16};
  17
  18#[cfg(not(target_os = "macos"))]
  19use crate::application_menu::{
  20    ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
  21};
  22
  23use auto_update::AutoUpdateStatus;
  24use call::ActiveCall;
  25use client::{Client, UserStore, zed_urls};
  26use cloud_api_types::Plan;
  27use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
  28use gpui::{
  29    Action, AnyElement, App, Context, Corner, Element, Empty, Entity, Focusable,
  30    InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
  31    StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
  32};
  33use onboarding_banner::OnboardingBanner;
  34use project::{
  35    DisableAiSettings, Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees,
  36};
  37use remote::RemoteConnectionOptions;
  38use settings::Settings;
  39use settings::WorktreeId;
  40use std::collections::HashSet;
  41use std::sync::Arc;
  42use theme::ActiveTheme;
  43use title_bar_settings::TitleBarSettings;
  44use ui::{
  45    Avatar, ButtonLike, ContextMenu, Divider, IconWithIndicator, Indicator, PopoverMenu,
  46    PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
  47};
  48use update_version::UpdateVersion;
  49use util::ResultExt;
  50use workspace::{
  51    MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace, WorkspaceId,
  52    notifications::NotifyResultExt,
  53};
  54use zed_actions::OpenRemote;
  55
  56pub use onboarding_banner::restore_banner;
  57
  58#[cfg(feature = "stories")]
  59pub use stories::*;
  60
  61const MAX_PROJECT_NAME_LENGTH: usize = 40;
  62const MAX_BRANCH_NAME_LENGTH: usize = 40;
  63const MAX_SHORT_SHA_LENGTH: usize = 8;
  64
  65actions!(
  66    collab,
  67    [
  68        /// Toggles the user menu dropdown.
  69        ToggleUserMenu,
  70        /// Toggles the project menu dropdown.
  71        ToggleProjectMenu,
  72        /// Switches to a different git branch.
  73        SwitchBranch,
  74        /// A debug action to simulate an update being available to test the update banner UI.
  75        SimulateUpdateAvailable
  76    ]
  77);
  78
  79pub fn init(cx: &mut App) {
  80    platform_title_bar::PlatformTitleBar::init(cx);
  81
  82    cx.observe_new(|workspace: &mut Workspace, window, cx| {
  83        let Some(window) = window else {
  84            return;
  85        };
  86        let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
  87        workspace.set_titlebar_item(item.into(), window, cx);
  88
  89        workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| {
  90            if let Some(titlebar) = workspace
  91                .titlebar_item()
  92                .and_then(|item| item.downcast::<TitleBar>().ok())
  93            {
  94                titlebar.update(cx, |titlebar, cx| {
  95                    titlebar.toggle_update_simulation(cx);
  96                });
  97            }
  98        });
  99
 100        #[cfg(not(target_os = "macos"))]
 101        workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
 102            if let Some(titlebar) = workspace
 103                .titlebar_item()
 104                .and_then(|item| item.downcast::<TitleBar>().ok())
 105            {
 106                titlebar.update(cx, |titlebar, cx| {
 107                    if let Some(ref menu) = titlebar.application_menu {
 108                        menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
 109                    }
 110                });
 111            }
 112        });
 113
 114        #[cfg(not(target_os = "macos"))]
 115        workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
 116            if let Some(titlebar) = workspace
 117                .titlebar_item()
 118                .and_then(|item| item.downcast::<TitleBar>().ok())
 119            {
 120                titlebar.update(cx, |titlebar, cx| {
 121                    if let Some(ref menu) = titlebar.application_menu {
 122                        menu.update(cx, |menu, cx| {
 123                            menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
 124                        });
 125                    }
 126                });
 127            }
 128        });
 129
 130        #[cfg(not(target_os = "macos"))]
 131        workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
 132            if let Some(titlebar) = workspace
 133                .titlebar_item()
 134                .and_then(|item| item.downcast::<TitleBar>().ok())
 135            {
 136                titlebar.update(cx, |titlebar, cx| {
 137                    if let Some(ref menu) = titlebar.application_menu {
 138                        menu.update(cx, |menu, cx| {
 139                            menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
 140                        });
 141                    }
 142                });
 143            }
 144        });
 145    })
 146    .detach();
 147}
 148
 149pub struct TitleBar {
 150    platform_titlebar: Entity<PlatformTitleBar>,
 151    project: Entity<Project>,
 152    user_store: Entity<UserStore>,
 153    client: Arc<Client>,
 154    workspace: WeakEntity<Workspace>,
 155    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 156    application_menu: Option<Entity<ApplicationMenu>>,
 157    _subscriptions: Vec<Subscription>,
 158    banner: Entity<OnboardingBanner>,
 159    update_version: Entity<UpdateVersion>,
 160    screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
 161}
 162
 163impl Render for TitleBar {
 164    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 165        let title_bar_settings = *TitleBarSettings::get_global(cx);
 166
 167        let show_menus = show_menus(cx);
 168
 169        let mut children = Vec::new();
 170
 171        children.push(
 172            h_flex()
 173                .h_full()
 174                .gap_0p5()
 175                .map(|title_bar| {
 176                    let mut render_project_items = title_bar_settings.show_branch_name
 177                        || title_bar_settings.show_project_items;
 178                    title_bar
 179                        .children(self.render_workspace_sidebar_toggle(window, cx))
 180                        .when_some(
 181                            self.application_menu.clone().filter(|_| !show_menus),
 182                            |title_bar, menu| {
 183                                render_project_items &=
 184                                    !menu.update(cx, |menu, cx| menu.all_menus_shown(cx));
 185                                title_bar.child(menu)
 186                            },
 187                        )
 188                        .children(self.render_restricted_mode(cx))
 189                        .when(render_project_items, |title_bar| {
 190                            title_bar
 191                                .when(title_bar_settings.show_project_items, |title_bar| {
 192                                    title_bar
 193                                        .children(self.render_project_host(cx))
 194                                        .child(self.render_project_name(window, cx))
 195                                })
 196                                .when(title_bar_settings.show_branch_name, |title_bar| {
 197                                    title_bar.children(self.render_project_branch(cx))
 198                                })
 199                        })
 200                })
 201                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
 202                .into_any_element(),
 203        );
 204
 205        children.push(self.render_collaborator_list(window, cx).into_any_element());
 206
 207        if title_bar_settings.show_onboarding_banner {
 208            children.push(self.banner.clone().into_any_element())
 209        }
 210
 211        let status = self.client.status();
 212        let status = &*status.borrow();
 213        let user = self.user_store.read(cx).current_user();
 214
 215        let signed_in = user.is_some();
 216
 217        children.push(
 218            h_flex()
 219                .map(|this| {
 220                    if signed_in {
 221                        this.pr_1p5()
 222                    } else {
 223                        this.pr_1()
 224                    }
 225                })
 226                .gap_1()
 227                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
 228                .children(self.render_call_controls(window, cx))
 229                .children(self.render_connection_status(status, cx))
 230                .child(self.update_version.clone())
 231                .when(
 232                    user.is_none() && TitleBarSettings::get_global(cx).show_sign_in,
 233                    |this| this.child(self.render_sign_in_button(cx)),
 234                )
 235                .child(self.render_organization_menu_button(cx))
 236                .when(TitleBarSettings::get_global(cx).show_user_menu, |this| {
 237                    this.child(self.render_user_menu_button(cx))
 238                })
 239                .into_any_element(),
 240        );
 241
 242        if show_menus {
 243            self.platform_titlebar.update(cx, |this, _| {
 244                this.set_children(
 245                    self.application_menu
 246                        .clone()
 247                        .map(|menu| menu.into_any_element()),
 248                );
 249            });
 250
 251            let height = platform_title_bar_height(window);
 252            let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
 253                platform_titlebar.title_bar_color(window, cx)
 254            });
 255
 256            v_flex()
 257                .w_full()
 258                .child(self.platform_titlebar.clone().into_any_element())
 259                .child(
 260                    h_flex()
 261                        .bg(title_bar_color)
 262                        .h(height)
 263                        .pl_2()
 264                        .justify_between()
 265                        .w_full()
 266                        .children(children),
 267                )
 268                .into_any_element()
 269        } else {
 270            self.platform_titlebar.update(cx, |this, _| {
 271                this.set_children(children);
 272            });
 273            self.platform_titlebar.clone().into_any_element()
 274        }
 275    }
 276}
 277
 278impl TitleBar {
 279    pub fn new(
 280        id: impl Into<ElementId>,
 281        workspace: &Workspace,
 282        window: &mut Window,
 283        cx: &mut Context<Self>,
 284    ) -> Self {
 285        let project = workspace.project().clone();
 286        let git_store = project.read(cx).git_store().clone();
 287        let user_store = workspace.app_state().user_store.clone();
 288        let client = workspace.app_state().client.clone();
 289        let active_call = ActiveCall::global(cx);
 290
 291        let platform_style = PlatformStyle::platform();
 292        let application_menu = match platform_style {
 293            PlatformStyle::Mac => {
 294                if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
 295                    Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
 296                } else {
 297                    None
 298                }
 299            }
 300            PlatformStyle::Linux | PlatformStyle::Windows => {
 301                Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
 302            }
 303        };
 304
 305        let mut subscriptions = Vec::new();
 306        subscriptions.push(
 307            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
 308                cx.notify()
 309            }),
 310        );
 311        subscriptions.push(
 312            cx.subscribe(&project, |this, _, event: &project::Event, cx| {
 313                if let project::Event::BufferEdited = event {
 314                    // Clear override when user types in any editor,
 315                    // so the title bar reflects the project they're actually working in
 316                    this.clear_active_worktree_override(cx);
 317                    cx.notify();
 318                }
 319            }),
 320        );
 321        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
 322        subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
 323        subscriptions.push(
 324            cx.subscribe(&git_store, move |this, _, event, cx| match event {
 325                GitStoreEvent::ActiveRepositoryChanged(_) => {
 326                    // Clear override when focus-derived active repo changes
 327                    // (meaning the user focused a file from a different project)
 328                    this.clear_active_worktree_override(cx);
 329                    cx.notify();
 330                }
 331                GitStoreEvent::RepositoryUpdated(_, _, true) => {
 332                    cx.notify();
 333                }
 334                _ => {}
 335            }),
 336        );
 337        subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
 338        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 339            subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
 340                cx.notify();
 341            }));
 342        }
 343
 344        let banner = cx.new(|cx| {
 345            OnboardingBanner::new(
 346                "ACP Claude Code Onboarding",
 347                IconName::AiClaude,
 348                "Claude Agent",
 349                Some("Introducing:".into()),
 350                zed_actions::agent::OpenClaudeAgentOnboardingModal.boxed_clone(),
 351                cx,
 352            )
 353            // When updating this to a non-AI feature release, remove this line.
 354            .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai)
 355        });
 356
 357        let update_version = cx.new(|cx| UpdateVersion::new(cx));
 358        let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
 359
 360        // Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar.
 361        {
 362            let platform_titlebar = platform_titlebar.clone();
 363            let window_handle = window.window_handle();
 364            cx.spawn(async move |this: WeakEntity<TitleBar>, cx| {
 365                let Some(multi_workspace_handle) = window_handle.downcast::<MultiWorkspace>()
 366                else {
 367                    return;
 368                };
 369
 370                let _ = cx.update(|cx| {
 371                    let Ok(multi_workspace) = multi_workspace_handle.entity(cx) else {
 372                        return;
 373                    };
 374
 375                    let is_open = multi_workspace.read(cx).sidebar_open();
 376                    let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx);
 377                    platform_titlebar.update(cx, |titlebar, cx| {
 378                        titlebar.set_workspace_sidebar_open(is_open, cx);
 379                        titlebar.set_sidebar_has_notifications(has_notifications, cx);
 380                    });
 381
 382                    let platform_titlebar = platform_titlebar.clone();
 383                    let subscription = cx.observe(&multi_workspace, move |mw, cx| {
 384                        let is_open = mw.read(cx).sidebar_open();
 385                        let has_notifications = mw.read(cx).sidebar_has_notifications(cx);
 386                        platform_titlebar.update(cx, |titlebar, cx| {
 387                            titlebar.set_workspace_sidebar_open(is_open, cx);
 388                            titlebar.set_sidebar_has_notifications(has_notifications, cx);
 389                        });
 390                    });
 391
 392                    if let Some(this) = this.upgrade() {
 393                        this.update(cx, |this, _| {
 394                            this._subscriptions.push(subscription);
 395                            this.multi_workspace = Some(multi_workspace.downgrade());
 396                        });
 397                    }
 398                });
 399            })
 400            .detach();
 401        }
 402
 403        Self {
 404            platform_titlebar,
 405            application_menu,
 406            workspace: workspace.weak_handle(),
 407            multi_workspace: None,
 408            project,
 409            user_store,
 410            client,
 411            _subscriptions: subscriptions,
 412            banner,
 413            update_version,
 414            screen_share_popover_handle: PopoverMenuHandle::default(),
 415        }
 416    }
 417
 418    fn worktree_count(&self, cx: &App) -> usize {
 419        self.project.read(cx).visible_worktrees(cx).count()
 420    }
 421
 422    fn toggle_update_simulation(&mut self, cx: &mut Context<Self>) {
 423        self.update_version
 424            .update(cx, |banner, cx| banner.update_simulation(cx));
 425        cx.notify();
 426    }
 427
 428    /// Returns the worktree to display in the title bar.
 429    /// - If there's an override set on the workspace, use that (if still valid)
 430    /// - Otherwise, derive from the active repository
 431    /// - Fall back to the first visible worktree
 432    pub fn effective_active_worktree(&self, cx: &App) -> Option<Entity<project::Worktree>> {
 433        let project = self.project.read(cx);
 434
 435        if let Some(workspace) = self.workspace.upgrade() {
 436            if let Some(override_id) = workspace.read(cx).active_worktree_override() {
 437                if let Some(worktree) = project.worktree_for_id(override_id, cx) {
 438                    return Some(worktree);
 439                }
 440            }
 441        }
 442
 443        if let Some(repo) = project.active_repository(cx) {
 444            let repo = repo.read(cx);
 445            let repo_path = &repo.work_directory_abs_path;
 446
 447            for worktree in project.visible_worktrees(cx) {
 448                let worktree_path = worktree.read(cx).abs_path();
 449                if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
 450                    return Some(worktree);
 451                }
 452            }
 453        }
 454
 455        project.visible_worktrees(cx).next()
 456    }
 457
 458    pub fn set_active_worktree_override(
 459        &mut self,
 460        worktree_id: WorktreeId,
 461        cx: &mut Context<Self>,
 462    ) {
 463        if let Some(workspace) = self.workspace.upgrade() {
 464            workspace.update(cx, |workspace, cx| {
 465                workspace.set_active_worktree_override(Some(worktree_id), cx);
 466            });
 467        }
 468        cx.notify();
 469    }
 470
 471    fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
 472        if let Some(workspace) = self.workspace.upgrade() {
 473            workspace.update(cx, |workspace, cx| {
 474                workspace.clear_active_worktree_override(cx);
 475            });
 476        }
 477        cx.notify();
 478    }
 479
 480    fn get_repository_for_worktree(
 481        &self,
 482        worktree: &Entity<project::Worktree>,
 483        cx: &App,
 484    ) -> Option<Entity<project::git_store::Repository>> {
 485        let project = self.project.read(cx);
 486        let git_store = project.git_store().read(cx);
 487        let worktree_path = worktree.read(cx).abs_path();
 488
 489        for repo in git_store.repositories().values() {
 490            let repo_path = &repo.read(cx).work_directory_abs_path;
 491            if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
 492                return Some(repo.clone());
 493            }
 494        }
 495
 496        None
 497    }
 498
 499    fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 500        let workspace = self.workspace.clone();
 501
 502        let options = self.project.read(cx).remote_connection_options(cx)?;
 503        let host: SharedString = options.display_name().into();
 504
 505        let (nickname, tooltip_title, icon) = match options {
 506            RemoteConnectionOptions::Ssh(options) => (
 507                options.nickname.map(|nick| nick.into()),
 508                "Remote Project",
 509                IconName::Server,
 510            ),
 511            RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
 512            RemoteConnectionOptions::Docker(_dev_container_connection) => {
 513                (None, "Dev Container", IconName::Box)
 514            }
 515            #[cfg(any(test, feature = "test-support"))]
 516            RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
 517        };
 518
 519        let nickname = nickname.unwrap_or_else(|| host.clone());
 520
 521        let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
 522            remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
 523            remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
 524            remote::ConnectionState::HeartbeatMissed => (
 525                Color::Warning,
 526                format!("Connection attempt to {host} missed. Retrying..."),
 527            ),
 528            remote::ConnectionState::Reconnecting => (
 529                Color::Warning,
 530                format!("Lost connection to {host}. Reconnecting..."),
 531            ),
 532            remote::ConnectionState::Disconnected => {
 533                (Color::Error, format!("Disconnected from {host}"))
 534            }
 535        };
 536
 537        let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
 538            remote::ConnectionState::Connecting => Color::Info,
 539            remote::ConnectionState::Connected => Color::Default,
 540            remote::ConnectionState::HeartbeatMissed => Color::Warning,
 541            remote::ConnectionState::Reconnecting => Color::Warning,
 542            remote::ConnectionState::Disconnected => Color::Error,
 543        };
 544
 545        let meta = SharedString::from(meta);
 546
 547        Some(
 548            PopoverMenu::new("remote-project-menu")
 549                .menu(move |window, cx| {
 550                    let workspace_entity = workspace.upgrade()?;
 551                    let fs = workspace_entity.read(cx).project().read(cx).fs().clone();
 552                    Some(recent_projects::RemoteServerProjects::popover(
 553                        fs,
 554                        workspace.clone(),
 555                        false,
 556                        window,
 557                        cx,
 558                    ))
 559                })
 560                .trigger_with_tooltip(
 561                    ButtonLike::new("remote_project")
 562                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 563                        .child(
 564                            h_flex()
 565                                .gap_2()
 566                                .max_w_32()
 567                                .child(
 568                                    IconWithIndicator::new(
 569                                        Icon::new(icon).size(IconSize::Small).color(icon_color),
 570                                        Some(Indicator::dot().color(indicator_color)),
 571                                    )
 572                                    .indicator_border_color(Some(
 573                                        cx.theme().colors().title_bar_background,
 574                                    ))
 575                                    .into_any_element(),
 576                                )
 577                                .child(Label::new(nickname).size(LabelSize::Small).truncate()),
 578                        ),
 579                    move |_window, cx| {
 580                        Tooltip::with_meta(
 581                            tooltip_title,
 582                            Some(&OpenRemote {
 583                                from_existing_connection: false,
 584                                create_new_window: false,
 585                            }),
 586                            meta.clone(),
 587                            cx,
 588                        )
 589                    },
 590                )
 591                .anchor(gpui::Corner::TopLeft)
 592                .into_any_element(),
 593        )
 594    }
 595
 596    pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 597        let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
 598            .map(|trusted_worktrees| {
 599                trusted_worktrees
 600                    .read(cx)
 601                    .has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
 602            })
 603            .unwrap_or(false);
 604        if !has_restricted_worktrees {
 605            return None;
 606        }
 607
 608        let button = Button::new("restricted_mode_trigger", "Restricted Mode")
 609            .style(ButtonStyle::Tinted(TintColor::Warning))
 610            .label_size(LabelSize::Small)
 611            .color(Color::Warning)
 612            .start_icon(
 613                Icon::new(IconName::Warning)
 614                    .size(IconSize::Small)
 615                    .color(Color::Warning),
 616            )
 617            .tooltip(|_, cx| {
 618                Tooltip::with_meta(
 619                    "You're in Restricted Mode",
 620                    Some(&ToggleWorktreeSecurity),
 621                    "Mark this project as trusted and unlock all features",
 622                    cx,
 623                )
 624            })
 625            .on_click({
 626                cx.listener(move |this, _, window, cx| {
 627                    this.workspace
 628                        .update(cx, |workspace, cx| {
 629                            workspace.show_worktree_trust_security_modal(true, window, cx)
 630                        })
 631                        .log_err();
 632                })
 633            });
 634
 635        if cfg!(macos_sdk_26) {
 636            // Make up for Tahoe's traffic light buttons having less spacing around them
 637            Some(div().child(button).ml_0p5().into_any_element())
 638        } else {
 639            Some(button.into_any_element())
 640        }
 641    }
 642
 643    pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 644        if self.project.read(cx).is_via_remote_server() {
 645            return self.render_remote_project_connection(cx);
 646        }
 647
 648        if self.project.read(cx).is_disconnected(cx) {
 649            return Some(
 650                Button::new("disconnected", "Disconnected")
 651                    .disabled(true)
 652                    .color(Color::Disabled)
 653                    .label_size(LabelSize::Small)
 654                    .into_any_element(),
 655            );
 656        }
 657
 658        let host = self.project.read(cx).host()?;
 659        let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
 660        let participant_index = self
 661            .user_store
 662            .read(cx)
 663            .participant_indices()
 664            .get(&host_user.id)?;
 665
 666        Some(
 667            Button::new("project_owner_trigger", host_user.github_login.clone())
 668                .color(Color::Player(participant_index.0))
 669                .label_size(LabelSize::Small)
 670                .tooltip(move |_, cx| {
 671                    let tooltip_title = format!(
 672                        "{} is sharing this project. Click to follow.",
 673                        host_user.github_login
 674                    );
 675
 676                    Tooltip::with_meta(tooltip_title, None, "Click to Follow", cx)
 677                })
 678                .on_click({
 679                    let host_peer_id = host.peer_id;
 680                    cx.listener(move |this, _, window, cx| {
 681                        this.workspace
 682                            .update(cx, |workspace, cx| {
 683                                workspace.follow(host_peer_id, window, cx);
 684                            })
 685                            .log_err();
 686                    })
 687                })
 688                .into_any_element(),
 689        )
 690    }
 691
 692    fn render_workspace_sidebar_toggle(
 693        &self,
 694        _window: &mut Window,
 695        cx: &mut Context<Self>,
 696    ) -> Option<AnyElement> {
 697        if !cx.has_flag::<AgentV2FeatureFlag>() || DisableAiSettings::get_global(cx).disable_ai {
 698            return None;
 699        }
 700
 701        let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
 702
 703        if is_sidebar_open {
 704            return None;
 705        }
 706
 707        let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications();
 708
 709        Some(
 710            h_flex()
 711                .h_full()
 712                .gap_0p5()
 713                .child(
 714                    IconButton::new(
 715                        "toggle-workspace-sidebar",
 716                        IconName::ThreadsSidebarLeftClosed,
 717                    )
 718                    .icon_size(IconSize::Small)
 719                    .when(has_notifications, |button| {
 720                        button
 721                            .indicator(Indicator::dot().color(Color::Accent))
 722                            .indicator_border_color(Some(cx.theme().colors().title_bar_background))
 723                    })
 724                    .tooltip(move |_, cx| {
 725                        Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
 726                    })
 727                    .on_click(|_, window, cx| {
 728                        window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
 729                    }),
 730                )
 731                .child(Divider::vertical().color(ui::DividerColor::Border))
 732                .into_any_element(),
 733        )
 734    }
 735
 736    pub fn render_project_name(&self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 737        let workspace = self.workspace.clone();
 738
 739        let name = self.effective_active_worktree(cx).map(|worktree| {
 740            let worktree = worktree.read(cx);
 741            SharedString::from(worktree.root_name().as_unix_str().to_string())
 742        });
 743
 744        let is_project_selected = name.is_some();
 745
 746        let display_name = if let Some(ref name) = name {
 747            util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
 748        } else {
 749            "Open Recent Project".to_string()
 750        };
 751
 752        let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
 753
 754        if is_sidebar_open {
 755            return self
 756                .render_project_name_with_sidebar_popover(display_name, is_project_selected, cx)
 757                .into_any_element();
 758        }
 759
 760        let focus_handle = workspace
 761            .upgrade()
 762            .map(|w| w.read(cx).focus_handle(cx))
 763            .unwrap_or_else(|| cx.focus_handle());
 764
 765        let excluded_workspace_ids: HashSet<WorkspaceId> = self
 766            .multi_workspace
 767            .as_ref()
 768            .and_then(|mw| mw.upgrade())
 769            .map(|mw| {
 770                mw.read(cx)
 771                    .workspaces()
 772                    .iter()
 773                    .filter_map(|ws| ws.read(cx).database_id())
 774                    .collect()
 775            })
 776            .unwrap_or_default();
 777
 778        PopoverMenu::new("recent-projects-menu")
 779            .menu(move |window, cx| {
 780                Some(recent_projects::RecentProjects::popover(
 781                    workspace.clone(),
 782                    excluded_workspace_ids.clone(),
 783                    false,
 784                    focus_handle.clone(),
 785                    window,
 786                    cx,
 787                ))
 788            })
 789            .trigger_with_tooltip(
 790                Button::new("project_name_trigger", display_name)
 791                    .label_size(LabelSize::Small)
 792                    .when(self.worktree_count(cx) > 1, |this| {
 793                        this.end_icon(
 794                            Icon::new(IconName::ChevronDown)
 795                                .size(IconSize::XSmall)
 796                                .color(Color::Muted),
 797                        )
 798                    })
 799                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 800                    .when(!is_project_selected, |s| s.color(Color::Muted)),
 801                move |_window, cx| {
 802                    Tooltip::for_action(
 803                        "Recent Projects",
 804                        &zed_actions::OpenRecent {
 805                            create_new_window: false,
 806                        },
 807                        cx,
 808                    )
 809                },
 810            )
 811            .anchor(gpui::Corner::TopLeft)
 812            .into_any_element()
 813    }
 814
 815    /// When the sidebar is open, the title bar's project name button becomes a
 816    /// plain button that toggles the sidebar's popover (so the popover is always
 817    /// anchored to the sidebar). Both buttons show their selected state together.
 818    fn render_project_name_with_sidebar_popover(
 819        &self,
 820        display_name: String,
 821        is_project_selected: bool,
 822        cx: &mut Context<Self>,
 823    ) -> impl IntoElement {
 824        let multi_workspace = self.multi_workspace.clone();
 825
 826        let is_popover_deployed = multi_workspace
 827            .as_ref()
 828            .and_then(|mw| mw.upgrade())
 829            .map(|mw| mw.read(cx).is_recent_projects_popover_deployed(cx))
 830            .unwrap_or(false);
 831
 832        Button::new("project_name_trigger", display_name)
 833            .label_size(LabelSize::Small)
 834            .when(self.worktree_count(cx) > 1, |this| {
 835                this.end_icon(
 836                    Icon::new(IconName::ChevronDown)
 837                        .size(IconSize::XSmall)
 838                        .color(Color::Muted),
 839                )
 840            })
 841            .toggle_state(is_popover_deployed)
 842            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 843            .when(!is_project_selected, |s| s.color(Color::Muted))
 844            .tooltip(move |_window, cx| {
 845                Tooltip::for_action(
 846                    "Recent Projects",
 847                    &zed_actions::OpenRecent {
 848                        create_new_window: false,
 849                    },
 850                    cx,
 851                )
 852            })
 853            .on_click(move |_, window, cx| {
 854                if let Some(mw) = multi_workspace.as_ref().and_then(|mw| mw.upgrade()) {
 855                    mw.update(cx, |mw, cx| {
 856                        mw.toggle_recent_projects_popover(window, cx);
 857                    });
 858                }
 859            })
 860    }
 861
 862    pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
 863        let effective_worktree = self.effective_active_worktree(cx)?;
 864        let repository = self.get_repository_for_worktree(&effective_worktree, cx)?;
 865        let workspace = self.workspace.upgrade()?;
 866
 867        let (branch_name, icon_info) = {
 868            let repo = repository.read(cx);
 869            let branch_name = repo
 870                .branch
 871                .as_ref()
 872                .map(|branch| branch.name())
 873                .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
 874                .or_else(|| {
 875                    repo.head_commit.as_ref().map(|commit| {
 876                        commit
 877                            .sha
 878                            .chars()
 879                            .take(MAX_SHORT_SHA_LENGTH)
 880                            .collect::<String>()
 881                    })
 882                });
 883
 884            let status = repo.status_summary();
 885            let tracked = status.index + status.worktree;
 886            let icon_info = if status.conflict > 0 {
 887                (IconName::Warning, Color::VersionControlConflict)
 888            } else if tracked.modified > 0 {
 889                (IconName::SquareDot, Color::VersionControlModified)
 890            } else if tracked.added > 0 || status.untracked > 0 {
 891                (IconName::SquarePlus, Color::VersionControlAdded)
 892            } else if tracked.deleted > 0 {
 893                (IconName::SquareMinus, Color::VersionControlDeleted)
 894            } else {
 895                (IconName::GitBranch, Color::Muted)
 896            };
 897
 898            (branch_name, icon_info)
 899        };
 900
 901        let settings = TitleBarSettings::get_global(cx);
 902
 903        let effective_repository = Some(repository);
 904
 905        Some(
 906            PopoverMenu::new("branch-menu")
 907                .menu(move |window, cx| {
 908                    Some(git_ui::git_picker::popover(
 909                        workspace.downgrade(),
 910                        effective_repository.clone(),
 911                        git_ui::git_picker::GitPickerTab::Branches,
 912                        gpui::rems(34.),
 913                        window,
 914                        cx,
 915                    ))
 916                })
 917                .trigger_with_tooltip(
 918                    Button::new("project_branch_trigger", branch_name?)
 919                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 920                        .label_size(LabelSize::Small)
 921                        .color(Color::Muted)
 922                        .when(settings.show_branch_icon, |branch_button| {
 923                            let (icon, icon_color) = icon_info;
 924                            branch_button.start_icon(
 925                                Icon::new(icon).size(IconSize::Indicator).color(icon_color),
 926                            )
 927                        }),
 928                    move |_window, cx| {
 929                        Tooltip::with_meta(
 930                            "Recent Branches",
 931                            Some(&zed_actions::git::Branch),
 932                            "Local branches only",
 933                            cx,
 934                        )
 935                    },
 936                )
 937                .anchor(gpui::Corner::TopLeft),
 938        )
 939    }
 940
 941    fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 942        if window.is_window_active() {
 943            ActiveCall::global(cx)
 944                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
 945                .detach_and_log_err(cx);
 946        } else if cx.active_window().is_none() {
 947            ActiveCall::global(cx)
 948                .update(cx, |call, cx| call.set_location(None, cx))
 949                .detach_and_log_err(cx);
 950        }
 951        self.workspace
 952            .update(cx, |workspace, cx| {
 953                workspace.update_active_view_for_followers(window, cx);
 954            })
 955            .ok();
 956    }
 957
 958    fn active_call_changed(&mut self, cx: &mut Context<Self>) {
 959        cx.notify();
 960    }
 961
 962    fn share_project(&mut self, cx: &mut Context<Self>) {
 963        let active_call = ActiveCall::global(cx);
 964        let project = self.project.clone();
 965        active_call
 966            .update(cx, |call, cx| call.share_project(project, cx))
 967            .detach_and_log_err(cx);
 968    }
 969
 970    fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
 971        let active_call = ActiveCall::global(cx);
 972        let project = self.project.clone();
 973        active_call
 974            .update(cx, |call, cx| call.unshare_project(project, cx))
 975            .log_err();
 976    }
 977
 978    fn render_connection_status(
 979        &self,
 980        status: &client::Status,
 981        cx: &mut Context<Self>,
 982    ) -> Option<AnyElement> {
 983        match status {
 984            client::Status::ConnectionError
 985            | client::Status::ConnectionLost
 986            | client::Status::Reauthenticating
 987            | client::Status::Reconnecting
 988            | client::Status::ReconnectionError { .. } => Some(
 989                div()
 990                    .id("disconnected")
 991                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
 992                    .tooltip(Tooltip::text("Disconnected"))
 993                    .into_any_element(),
 994            ),
 995            client::Status::UpgradeRequired => {
 996                let auto_updater = auto_update::AutoUpdater::get(cx);
 997                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
 998                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
 999                    Some(AutoUpdateStatus::Installing { .. })
1000                    | Some(AutoUpdateStatus::Downloading { .. })
1001                    | Some(AutoUpdateStatus::Checking) => "Updating...",
1002                    Some(AutoUpdateStatus::Idle)
1003                    | Some(AutoUpdateStatus::Errored { .. })
1004                    | None => "Please update Zed to Collaborate",
1005                };
1006
1007                Some(
1008                    Button::new("connection-status", label)
1009                        .label_size(LabelSize::Small)
1010                        .on_click(|_, window, cx| {
1011                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
1012                                && auto_updater.read(cx).status().is_updated()
1013                            {
1014                                workspace::reload(cx);
1015                                return;
1016                            }
1017                            auto_update::check(&Default::default(), window, cx);
1018                        })
1019                        .into_any_element(),
1020                )
1021            }
1022            _ => None,
1023        }
1024    }
1025
1026    pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
1027        let client = self.client.clone();
1028        let workspace = self.workspace.clone();
1029        Button::new("sign_in", "Sign In")
1030            .label_size(LabelSize::Small)
1031            .on_click(move |_, window, cx| {
1032                let client = client.clone();
1033                let workspace = workspace.clone();
1034                window
1035                    .spawn(cx, async move |mut cx| {
1036                        client
1037                            .sign_in_with_optional_connect(true, cx)
1038                            .await
1039                            .notify_workspace_async_err(workspace, &mut cx);
1040                    })
1041                    .detach();
1042            })
1043    }
1044
1045    pub fn render_organization_menu_button(&mut self, cx: &mut Context<Self>) -> AnyElement {
1046        let Some(organization) = self.user_store.read(cx).current_organization() else {
1047            return Empty.into_any_element();
1048        };
1049
1050        PopoverMenu::new("organization-menu")
1051            .anchor(Corner::TopRight)
1052            .menu({
1053                let user_store = self.user_store.clone();
1054                move |window, cx| {
1055                    ContextMenu::build(window, cx, |mut menu, _window, cx| {
1056                        menu = menu.header("Organizations").separator();
1057
1058                        let current_organization = user_store.read(cx).current_organization();
1059
1060                        for organization in user_store.read(cx).organizations() {
1061                            let organization = organization.clone();
1062                            let plan = user_store.read(cx).plan_for_organization(&organization.id);
1063
1064                            let is_current =
1065                                current_organization
1066                                    .as_ref()
1067                                    .is_some_and(|current_organization| {
1068                                        current_organization.id == organization.id
1069                                    });
1070
1071                            menu = menu.custom_entry(
1072                                {
1073                                    let organization = organization.clone();
1074                                    move |_window, _cx| {
1075                                        h_flex()
1076                                            .w_full()
1077                                            .gap_1()
1078                                            .child(
1079                                                div()
1080                                                    .flex_none()
1081                                                    .when(!is_current, |parent| parent.invisible())
1082                                                    .child(Icon::new(IconName::Check)),
1083                                            )
1084                                            .child(
1085                                                h_flex()
1086                                                    .w_full()
1087                                                    .gap_3()
1088                                                    .justify_between()
1089                                                    .child(Label::new(&organization.name))
1090                                                    .child(PlanChip::new(
1091                                                        plan.unwrap_or(Plan::ZedFree),
1092                                                    )),
1093                                            )
1094                                            .into_any_element()
1095                                    }
1096                                },
1097                                {
1098                                    let user_store = user_store.clone();
1099                                    let organization = organization.clone();
1100                                    move |_window, cx| {
1101                                        user_store.update(cx, |user_store, cx| {
1102                                            user_store
1103                                                .set_current_organization(organization.clone(), cx);
1104                                        });
1105                                    }
1106                                },
1107                            );
1108                        }
1109
1110                        menu
1111                    })
1112                    .into()
1113                }
1114            })
1115            .trigger_with_tooltip(
1116                Button::new("organization-menu", &organization.name)
1117                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1118                    .label_size(LabelSize::Small),
1119                Tooltip::text("Toggle Organization Menu"),
1120            )
1121            .anchor(gpui::Corner::TopRight)
1122            .into_any_element()
1123    }
1124
1125    pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
1126        let show_update_badge = self.update_version.read(cx).show_update_in_menu_bar();
1127
1128        let user_store = self.user_store.read(cx);
1129        let user = user_store.current_user();
1130
1131        let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
1132        let user_login = user.as_ref().map(|u| u.github_login.clone());
1133
1134        let is_signed_in = user.is_some();
1135
1136        let has_subscription_period = user_store.subscription_period().is_some();
1137        let plan = user_store.plan().filter(|_| {
1138            // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
1139            has_subscription_period
1140        });
1141
1142        PopoverMenu::new("user-menu")
1143            .anchor(Corner::TopRight)
1144            .menu(move |window, cx| {
1145                ContextMenu::build(window, cx, |menu, _, _cx| {
1146                    let user_login = user_login.clone();
1147
1148                    menu.when(is_signed_in, |this| {
1149                        this.custom_entry(
1150                            move |_window, _cx| {
1151                                let user_login = user_login.clone().unwrap_or_default();
1152
1153                                h_flex()
1154                                    .w_full()
1155                                    .justify_between()
1156                                    .child(Label::new(user_login))
1157                                    .child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
1158                                    .into_any_element()
1159                            },
1160                            move |_, cx| {
1161                                cx.open_url(&zed_urls::account_url(cx));
1162                            },
1163                        )
1164                        .separator()
1165                    })
1166                    .when(show_update_badge, |this| {
1167                        this.custom_entry(
1168                            move |_window, _cx| {
1169                                h_flex()
1170                                    .w_full()
1171                                    .gap_1()
1172                                    .justify_between()
1173                                    .child(Label::new("Restart to update Zed").color(Color::Accent))
1174                                    .child(
1175                                        Icon::new(IconName::Download)
1176                                            .size(IconSize::Small)
1177                                            .color(Color::Accent),
1178                                    )
1179                                    .into_any_element()
1180                            },
1181                            move |_, cx| {
1182                                workspace::reload(cx);
1183                            },
1184                        )
1185                        .separator()
1186                    })
1187                    .action("Settings", zed_actions::OpenSettings.boxed_clone())
1188                    .action("Keymap", Box::new(zed_actions::OpenKeymap))
1189                    .action(
1190                        "Themes…",
1191                        zed_actions::theme_selector::Toggle::default().boxed_clone(),
1192                    )
1193                    .action(
1194                        "Icon Themes…",
1195                        zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
1196                    )
1197                    .action(
1198                        "Extensions",
1199                        zed_actions::Extensions::default().boxed_clone(),
1200                    )
1201                    .when(is_signed_in, |this| {
1202                        this.separator()
1203                            .action("Sign Out", client::SignOut.boxed_clone())
1204                    })
1205                })
1206                .into()
1207            })
1208            .map(|this| {
1209                if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture {
1210                    let avatar =
1211                        user_avatar
1212                            .clone()
1213                            .map(|avatar| Avatar::new(avatar))
1214                            .map(|avatar| {
1215                                if show_update_badge {
1216                                    avatar.indicator(
1217                                        div()
1218                                            .absolute()
1219                                            .bottom_0()
1220                                            .right_0()
1221                                            .child(Indicator::dot().color(Color::Accent)),
1222                                    )
1223                                } else {
1224                                    avatar
1225                                }
1226                            });
1227                    this.trigger_with_tooltip(
1228                        ButtonLike::new("user-menu").children(avatar),
1229                        Tooltip::text("Toggle User Menu"),
1230                    )
1231                } else {
1232                    this.trigger_with_tooltip(
1233                        IconButton::new("user-menu", IconName::ChevronDown)
1234                            .icon_size(IconSize::Small),
1235                        Tooltip::text("Toggle User Menu"),
1236                    )
1237                }
1238            })
1239            .anchor(gpui::Corner::TopRight)
1240    }
1241}