Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:16:04.172Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

title_bar.rs

1389 lines · 56.1 KB · rust
1mod application_menu;
2pub mod collab;
3mod onboarding_banner;
4mod plan_chip;
5mod title_bar_settings;
6mod update_version;
7
8use crate::application_menu::{ApplicationMenu, show_menus};
9use crate::plan_chip::PlanChip;
10use agent_settings::{AgentSettings, WindowLayout};
11use arrayvec::ArrayVec;
12use git_ui::worktree_picker::WorktreePicker;
13pub use platform_title_bar::{
14    self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar,
15    ShowNextWindowTab, ShowPreviousWindowTab,
16};
17use project::{linked_worktree_short_name, repo_identity_path};
18
19#[cfg(not(target_os = "macos"))]
20use crate::application_menu::{
21    ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
22};
23
24use auto_update::AutoUpdateStatus;
25use call::ActiveCall;
26use client::{Client, UserStore, zed_urls};
27use command_palette_hooks::CommandPaletteFilter;
28
29use gpui::{
30    Action, Anchor, Animation, AnimationExt, AnyElement, App, Context, Element, Entity, Focusable,
31    InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
32    StatefulInteractiveElement, Styled, Subscription, TaskExt, WeakEntity, Window, actions, div,
33    pulsating_between,
34};
35use onboarding_banner::OnboardingBanner;
36use project::{
37    Project, git_store::GitStoreEvent, project_settings::ProjectSettings,
38    trusted_worktrees::TrustedWorktrees,
39};
40use remote::RemoteConnectionOptions;
41use settings::{Settings as _, SettingsStore};
42
43use std::any::TypeId;
44use std::sync::Arc;
45use std::time::Duration;
46use theme::ActiveTheme;
47use title_bar_settings::TitleBarSettings;
48use ui::{
49    Avatar, ButtonLike, ContextMenu, ContextMenuEntry, IconWithIndicator, Indicator, PopoverMenu,
50    PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
51};
52use update_version::UpdateVersion;
53use util::ResultExt;
54use workspace::{AccessibleMode, MultiWorkspace, Workspace, notifications::NotifyTaskExt as _};
55
56use zed_actions::{OpenOnboarding, OpenRemote};
57
58pub use onboarding_banner::restore_banner;
59
60const MAX_PROJECT_NAME_LENGTH: usize = 40;
61const MAX_BRANCH_NAME_LENGTH: usize = 40;
62const MAX_SHORT_SHA_LENGTH: usize = 8;
63
64actions!(
65    collab,
66    [
67        /// Toggles the user menu dropdown.
68        ToggleUserMenu,
69        /// Toggles the project menu dropdown.
70        ToggleProjectMenu,
71        /// Switches to a different git branch.
72        SwitchBranch,
73        /// A debug action to simulate an update being available to test the update banner UI.
74        SimulateUpdateAvailable
75    ]
76);
77
78actions!(
79    workspace,
80    [
81        /// Switches to the classic, editor-focused panel layout.
82        UseClassicLayout,
83        /// Switches to the agentic panel layout.
84        UseAgenticLayout,
85    ]
86);
87
88pub fn init(cx: &mut App) {
89    platform_title_bar::PlatformTitleBar::init(cx);
90
91    update_layout_action_filter(cx);
92
93    cx.observe_global::<SettingsStore>(update_layout_action_filter)
94        .detach();
95
96    cx.observe_new(|workspace: &mut Workspace, window, cx| {
97        let Some(window) = window else {
98            return;
99        };
100        let multi_workspace = workspace.multi_workspace().cloned();
101        let item = cx.new(|cx| TitleBar::new("title-bar", workspace, multi_workspace, window, cx));
102        workspace.set_titlebar_item(item.into(), window, cx);
103
104        workspace.register_action(|_workspace, _: &UseClassicLayout, _window, cx| {
105            set_window_layout(WindowLayout::Editor(None), cx);
106        });
107
108        workspace.register_action(|_workspace, _: &UseAgenticLayout, _window, cx| {
109            set_window_layout(WindowLayout::Agent(None), cx);
110        });
111
112        workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| {
113            if let Some(titlebar) = workspace
114                .titlebar_item()
115                .and_then(|item| item.downcast::<TitleBar>().ok())
116            {
117                titlebar.update(cx, |titlebar, cx| {
118                    titlebar.toggle_update_simulation(cx);
119                });
120            }
121        });
122
123        #[cfg(not(target_os = "macos"))]
124        workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
125            if let Some(titlebar) = workspace
126                .titlebar_item()
127                .and_then(|item| item.downcast::<TitleBar>().ok())
128            {
129                titlebar.update(cx, |titlebar, cx| {
130                    if let Some(ref menu) = titlebar.application_menu {
131                        menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
132                    }
133                });
134            }
135        });
136
137        #[cfg(not(target_os = "macos"))]
138        workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
139            if let Some(titlebar) = workspace
140                .titlebar_item()
141                .and_then(|item| item.downcast::<TitleBar>().ok())
142            {
143                titlebar.update(cx, |titlebar, cx| {
144                    if let Some(ref menu) = titlebar.application_menu {
145                        menu.update(cx, |menu, cx| {
146                            menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
147                        });
148                    }
149                });
150            }
151        });
152
153        #[cfg(not(target_os = "macos"))]
154        workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
155            if let Some(titlebar) = workspace
156                .titlebar_item()
157                .and_then(|item| item.downcast::<TitleBar>().ok())
158            {
159                titlebar.update(cx, |titlebar, cx| {
160                    if let Some(ref menu) = titlebar.application_menu {
161                        menu.update(cx, |menu, cx| {
162                            menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
163                        });
164                    }
165                });
166            }
167        });
168    })
169    .detach();
170}
171
172/// Hides or shows the panel layout actions in the command palette based on
173/// whether AI is currently disabled.
174fn update_layout_action_filter(cx: &mut App) {
175    let disable_ai = project::DisableAiSettings::get_global(cx).disable_ai;
176    let layout_actions = [
177        TypeId::of::<UseClassicLayout>(),
178        TypeId::of::<UseAgenticLayout>(),
179    ];
180    CommandPaletteFilter::update_global(cx, |filter, _| {
181        if disable_ai {
182            filter.hide_action_types(&layout_actions);
183        } else {
184            filter.show_action_types(layout_actions.iter());
185        }
186    });
187}
188
189fn set_window_layout(layout: WindowLayout, cx: &App) {
190    let fs = <dyn fs::Fs>::global(cx);
191    drop(AgentSettings::set_layout(layout, fs, cx));
192}
193
194pub struct TitleBar {
195    platform_titlebar: Entity<PlatformTitleBar>,
196    project: Entity<Project>,
197    user_store: Entity<UserStore>,
198    client: Arc<Client>,
199    workspace: WeakEntity<Workspace>,
200    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
201    application_menu: Option<Entity<ApplicationMenu>>,
202    _subscriptions: Vec<Subscription>,
203    banner: Option<Entity<OnboardingBanner>>,
204    update_version: Entity<UpdateVersion>,
205    screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
206    _diagnostics_subscription: Option<gpui::Subscription>,
207}
208
209impl Render for TitleBar {
210    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
211        if self.multi_workspace.is_none() {
212            if let Some(mw) = self
213                .workspace
214                .upgrade()
215                .and_then(|ws| ws.read(cx).multi_workspace().cloned())
216            {
217                self.multi_workspace = Some(mw.clone());
218                self.platform_titlebar.update(cx, |titlebar, _cx| {
219                    titlebar.set_multi_workspace(mw);
220                });
221            }
222        }
223
224        let title_bar_settings = *TitleBarSettings::get_global(cx);
225        let button_layout = title_bar_settings.button_layout;
226        let is_git_enabled = ProjectSettings::get_global(cx).git.enabled.status;
227
228        let show_menus = show_menus(cx);
229
230        let mut children = <ArrayVec<_, 5>>::new();
231
232        let mut project_name = None;
233        let mut repository = None;
234        let mut linked_worktree_name = None;
235        if let Some(worktree) = self.effective_active_worktree(cx) {
236            repository = self.get_repository_for_worktree(&worktree, cx);
237            let worktree_abs_path = worktree.read(cx).abs_path();
238            project_name = worktree
239                .read(cx)
240                .root_name()
241                .file_name()
242                .map(|name| SharedString::from(name.to_string()));
243            if let Some(repo) = &repository {
244                let repo = repo.read(cx);
245                linked_worktree_name = repo
246                    .main_worktree_abs_path()
247                    .and_then(|main_worktree_path| {
248                        linked_worktree_short_name(
249                            main_worktree_path,
250                            repo.work_directory_abs_path.as_ref(),
251                        )
252                    })
253                    .or_else(|| {
254                        repo.is_linked_worktree()
255                            .then_some(project_name.clone())
256                            .flatten()
257                    });
258
259                let identity = repo_identity_path(&repo.common_dir_abs_path);
260
261                let display_name = if identity.extension() == Some(std::ffi::OsStr::new("git")) {
262                    identity.file_stem()
263                } else {
264                    identity.file_name()
265                };
266
267                if let Some(repo_name) = display_name.and_then(|n| n.to_str()) {
268                    let visible_worktrees_in_repo = self.visible_worktrees_in_repository(repo, cx);
269                    let name = if visible_worktrees_in_repo == 1 {
270                        if let Ok(relative) =
271                            worktree_abs_path.strip_prefix(&*repo.work_directory_abs_path)
272                        {
273                            if relative.as_os_str().is_empty() {
274                                repo_name.to_string()
275                            } else {
276                                format!("{}/{}", repo_name, relative.display())
277                            }
278                        } else {
279                            repo_name.to_string()
280                        }
281                    } else {
282                        repo_name.to_string()
283                    };
284                    project_name = Some(SharedString::from(name));
285                }
286            }
287        }
288
289        children.push(
290            h_flex()
291                .h_full()
292                .gap_0p5()
293                .map(|title_bar| {
294                    let mut render_project_items = title_bar_settings.show_branch_name
295                        || title_bar_settings.show_project_items;
296                    title_bar
297                        .when_some(
298                            self.application_menu.clone().filter(|_| !show_menus),
299                            |title_bar, menu| {
300                                // Hide the project/branch items to make room when the
301                                // menu bar is expanded -- except in accessible mode,
302                                // where the menu bar is always expanded but those
303                                // controls must still remain reachable.
304                                render_project_items &= !menu
305                                    .update(cx, |menu, cx| menu.all_menus_shown(cx))
306                                    || cx.accessible_mode();
307                                title_bar.child(menu)
308                            },
309                        )
310                        .when(render_project_items, |title_bar| {
311                            title_bar
312                                .when(title_bar_settings.show_project_items, |title_bar| {
313                                    title_bar
314                                        .children(self.render_project_host(cx))
315                                        .child(self.render_project_name(project_name, window, cx))
316                                })
317                                .when_some(
318                                    repository.filter(|_| is_git_enabled),
319                                    |title_bar, repository| {
320                                        title_bar.children(self.render_worktree_and_branch(
321                                            repository,
322                                            linked_worktree_name,
323                                            cx,
324                                        ))
325                                    },
326                                )
327                        })
328                })
329                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
330                .into_any_element(),
331        );
332
333        children.push(self.render_collaborator_list(window, cx).into_any_element());
334
335        if title_bar_settings.show_onboarding_banner {
336            if let Some(banner) = &self.banner {
337                children.push(banner.clone().into_any_element())
338            }
339        }
340
341        let status = self.client.status();
342        let status = &*status.borrow();
343        let user = self.user_store.read(cx).current_user();
344        let is_signing_in = user.is_none()
345            && matches!(
346                status,
347                client::Status::Authenticating
348                    | client::Status::Authenticated
349                    | client::Status::Connecting
350            );
351        let is_signed_out_or_auth_error = user.is_none()
352            && matches!(
353                status,
354                client::Status::SignedOut | client::Status::AuthenticationError
355            );
356
357        children.push(
358            h_flex()
359                .pr_1()
360                .gap_1()
361                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
362                .child(self.render_call_controls(window, cx))
363                .children(self.render_connection_status(status, cx))
364                .child(self.update_version.clone())
365                .when(
366                    user.is_none()
367                        && is_signed_out_or_auth_error
368                        && TitleBarSettings::get_global(cx).show_sign_in,
369                    |this| this.child(self.render_sign_in_button(cx)),
370                )
371                .when(is_signing_in, |this| {
372                    this.child(
373                        Label::new("Signing in…")
374                            .size(LabelSize::Small)
375                            .color(Color::Muted)
376                            .with_animation(
377                                "signing-in",
378                                Animation::new(Duration::from_secs(2))
379                                    .repeat()
380                                    .with_easing(pulsating_between(0.4, 0.8)),
381                                |label, delta| label.alpha(delta),
382                            ),
383                    )
384                })
385                .when(TitleBarSettings::get_global(cx).show_user_menu, |this| {
386                    this.child(self.render_user_menu_button(cx))
387                })
388                .into_any_element(),
389        );
390
391        if show_menus {
392            self.platform_titlebar.update(cx, |this, _| {
393                this.set_button_layout(button_layout);
394                this.set_children(
395                    self.application_menu
396                        .clone()
397                        .map(|menu| menu.into_any_element()),
398                );
399            });
400
401            let height = platform_title_bar_height(window);
402            let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
403                platform_titlebar.title_bar_color(window, cx)
404            });
405
406            v_flex()
407                .w_full()
408                .child(self.platform_titlebar.clone().into_any_element())
409                .child(
410                    h_flex()
411                        .bg(title_bar_color)
412                        .h(height)
413                        .pl_2()
414                        .justify_between()
415                        .w_full()
416                        .children(children),
417                )
418                .into_any_element()
419        } else {
420            self.platform_titlebar.update(cx, |this, _| {
421                this.set_button_layout(button_layout);
422                this.set_children(children);
423            });
424            self.platform_titlebar.clone().into_any_element()
425        }
426    }
427}
428
429impl TitleBar {
430    pub fn new(
431        id: impl Into<ElementId>,
432        workspace: &Workspace,
433        multi_workspace: Option<WeakEntity<MultiWorkspace>>,
434        window: &mut Window,
435        cx: &mut Context<Self>,
436    ) -> Self {
437        let project = workspace.project().clone();
438        let git_store = project.read(cx).git_store().clone();
439        let user_store = workspace.app_state().user_store.clone();
440        let client = workspace.app_state().client.clone();
441        let active_call = ActiveCall::global(cx);
442
443        let platform_style = PlatformStyle::platform();
444        let application_menu = match platform_style {
445            PlatformStyle::Mac => {
446                if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
447                    Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
448                } else {
449                    None
450                }
451            }
452            PlatformStyle::Linux | PlatformStyle::Windows => {
453                Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
454            }
455        };
456
457        let mut subscriptions = Vec::new();
458        subscriptions.push(
459            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
460                cx.notify()
461            }),
462        );
463
464        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
465        subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
466        subscriptions.push(
467            cx.subscribe(&git_store, move |_, _, event, cx| match event {
468                GitStoreEvent::ActiveRepositoryChanged(_)
469                | GitStoreEvent::RepositoryUpdated(_, _, true) => {
470                    cx.notify();
471                }
472                _ => {}
473            }),
474        );
475        subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
476        if let Some(workspace_entity) = workspace.weak_handle().upgrade() {
477            subscriptions.push(cx.subscribe(
478                &workspace_entity,
479                |_, _, event: &workspace::Event, cx| {
480                    if matches!(event, workspace::Event::WorktreeCreationChanged) {
481                        cx.notify();
482                    }
483                },
484            ));
485        }
486        subscriptions.push(cx.observe_button_layout_changed(window, |_, _, cx| cx.notify()));
487        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
488            subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
489                cx.notify();
490            }));
491        }
492
493        let update_version = cx.new(|cx| UpdateVersion::new(cx));
494        let platform_titlebar = cx.new(|cx| {
495            let mut titlebar = PlatformTitleBar::new(id, cx);
496            if let Some(mw) = multi_workspace.clone() {
497                titlebar = titlebar.with_multi_workspace(mw);
498            }
499            titlebar
500        });
501
502        let banner = None;
503
504        let mut this = Self {
505            platform_titlebar,
506            application_menu,
507            workspace: workspace.weak_handle(),
508            multi_workspace,
509            project,
510            user_store,
511            client,
512            _subscriptions: subscriptions,
513            banner,
514            update_version,
515            screen_share_popover_handle: PopoverMenuHandle::default(),
516            _diagnostics_subscription: None,
517        };
518
519        this.observe_diagnostics(cx);
520
521        this
522    }
523
524    fn worktree_count(&self, cx: &App) -> usize {
525        self.project.read(cx).visible_worktrees(cx).count()
526    }
527
528    fn toggle_update_simulation(&mut self, cx: &mut Context<Self>) {
529        self.update_version
530            .update(cx, |banner, cx| banner.update_simulation(cx));
531        cx.notify();
532    }
533
534    /// Returns the worktree to display in the title bar.
535    /// - Prefer the worktree owning the project's active repository
536    /// - Fall back to the first visible worktree
537    pub fn effective_active_worktree(&self, cx: &App) -> Option<Entity<project::Worktree>> {
538        let project = self.project.read(cx);
539
540        if let Some(repo) = project.active_repository(cx) {
541            let repo = repo.read(cx);
542            let repo_path = &repo.work_directory_abs_path;
543
544            for worktree in project.visible_worktrees(cx) {
545                let worktree_path = worktree.read(cx).abs_path();
546                if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
547                    return Some(worktree);
548                }
549            }
550        }
551
552        project.visible_worktrees(cx).next()
553    }
554
555    fn get_repository_for_worktree(
556        &self,
557        worktree: &Entity<project::Worktree>,
558        cx: &App,
559    ) -> Option<Entity<project::git_store::Repository>> {
560        let project = self.project.read(cx);
561        let git_store = project.git_store().read(cx);
562        let worktree_path = worktree.read(cx).abs_path();
563
564        git_store
565            .repositories()
566            .values()
567            .filter(|repo| {
568                let repo_path = &repo.read(cx).work_directory_abs_path;
569                worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref())
570            })
571            .max_by_key(|repo| repo.read(cx).work_directory_abs_path.as_os_str().len())
572            .cloned()
573    }
574
575    fn visible_worktrees_in_repository(
576        &self,
577        repository: &project::git_store::Repository,
578        cx: &App,
579    ) -> usize {
580        let repo_path = &repository.work_directory_abs_path;
581        self.project
582            .read(cx)
583            .visible_worktrees(cx)
584            .filter(|worktree| {
585                let worktree_path = worktree.read(cx).abs_path();
586                worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref())
587            })
588            .count()
589    }
590
591    fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
592        let workspace = self.workspace.clone();
593
594        let options = self.project.read(cx).remote_connection_options(cx)?;
595        let host: SharedString = options.display_name().into();
596
597        let (nickname, tooltip_title, icon) = match options {
598            RemoteConnectionOptions::Ssh(options) => (
599                options.nickname.map(|nick| nick.into()),
600                "Remote Project",
601                IconName::Server,
602            ),
603            RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
604            RemoteConnectionOptions::Docker(_dev_container_connection) => {
605                (None, "Dev Container", IconName::Box)
606            }
607            #[cfg(any(test, feature = "test-support"))]
608            RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
609        };
610
611        let nickname = nickname.unwrap_or_else(|| host.clone());
612
613        let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
614            remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
615            remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
616            remote::ConnectionState::HeartbeatMissed => (
617                Color::Warning,
618                format!("Connection attempt to {host} missed. Retrying..."),
619            ),
620            remote::ConnectionState::Reconnecting => (
621                Color::Warning,
622                format!("Lost connection to {host}. Reconnecting..."),
623            ),
624            remote::ConnectionState::Disconnected => {
625                (Color::Error, format!("Disconnected from {host}"))
626            }
627        };
628
629        let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
630            remote::ConnectionState::Connecting => Color::Info,
631            remote::ConnectionState::Connected => Color::Default,
632            remote::ConnectionState::HeartbeatMissed => Color::Warning,
633            remote::ConnectionState::Reconnecting => Color::Warning,
634            remote::ConnectionState::Disconnected => Color::Error,
635        };
636
637        let meta = SharedString::from(meta);
638
639        Some(
640            PopoverMenu::new("remote-project-menu")
641                .menu(move |window, cx| {
642                    let workspace_entity = workspace.upgrade()?;
643                    let fs = workspace_entity.read(cx).project().read(cx).fs().clone();
644                    Some(recent_projects::RemoteServerProjects::popover(
645                        fs,
646                        workspace.clone(),
647                        None,
648                        window,
649                        cx,
650                    ))
651                })
652                .trigger_with_tooltip(
653                    ButtonLike::new("remote_project")
654                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
655                        .child(
656                            h_flex()
657                                .gap_2()
658                                .max_w_32()
659                                .child(
660                                    IconWithIndicator::new(
661                                        Icon::new(icon).size(IconSize::Small).color(icon_color),
662                                        Some(Indicator::dot().color(indicator_color)),
663                                    )
664                                    .indicator_border_color(Some(
665                                        cx.theme().colors().title_bar_background,
666                                    ))
667                                    .into_any_element(),
668                                )
669                                .child(Label::new(nickname).size(LabelSize::Small).truncate()),
670                        ),
671                    move |_window, cx| {
672                        Tooltip::with_meta(
673                            tooltip_title,
674                            Some(&OpenRemote::default()),
675                            meta.clone(),
676                            cx,
677                        )
678                    },
679                )
680                .anchor(gpui::Anchor::TopLeft)
681                .into_any_element(),
682        )
683    }
684
685
686    pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
687        if self.project.read(cx).is_via_remote_server() {
688            return self.render_remote_project_connection(cx);
689        }
690
691        if self.project.read(cx).is_disconnected(cx) {
692            return Some(
693                Button::new("disconnected", "Disconnected")
694                    .disabled(true)
695                    .color(Color::Disabled)
696                    .label_size(LabelSize::Small)
697                    .into_any_element(),
698            );
699        }
700
701        let host = self.project.read(cx).host()?;
702        let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
703        let participant_index = self
704            .user_store
705            .read(cx)
706            .participant_indices()
707            .get(&host_user.legacy_id)?;
708
709        Some(
710            Button::new("project_owner_trigger", host_user.username.clone())
711                .color(Color::Player(participant_index.0))
712                .label_size(LabelSize::Small)
713                .tab_index(0isize)
714                .tooltip(move |_, cx| {
715                    let tooltip_title = format!(
716                        "{} is sharing this project. Click to follow.",
717                        host_user.username
718                    );
719
720                    Tooltip::with_meta(tooltip_title, None, "Click to Follow", cx)
721                })
722                .on_click({
723                    let host_peer_id = host.peer_id;
724                    cx.listener(move |this, _, window, cx| {
725                        this.workspace
726                            .update(cx, |workspace, cx| {
727                                workspace.follow(host_peer_id, window, cx);
728                            })
729                            .log_err();
730                    })
731                })
732                .into_any_element(),
733        )
734    }
735
736    fn render_project_name(
737        &self,
738        name: Option<SharedString>,
739        _: &mut Window,
740        cx: &mut Context<Self>,
741    ) -> impl IntoElement {
742        let workspace = self.workspace.clone();
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
753            .multi_workspace
754            .as_ref()
755            .and_then(|mw| mw.upgrade())
756            .map(|mw| mw.read(cx).sidebar_open())
757            .unwrap_or(false)
758            && PlatformTitleBar::is_multi_workspace_enabled(cx);
759
760        let is_threads_list_view_active = self
761            .multi_workspace
762            .as_ref()
763            .and_then(|mw| mw.upgrade())
764            .map(|mw| mw.read(cx).is_threads_list_view_active(cx))
765            .unwrap_or(false);
766
767        if is_sidebar_open && is_threads_list_view_active {
768            return self
769                .render_recent_projects_popover(display_name, is_project_selected, cx)
770                .into_any_element();
771        }
772
773        let focus_handle = workspace
774            .upgrade()
775            .map(|w| w.read(cx).focus_handle(cx))
776            .unwrap_or_else(|| cx.focus_handle());
777
778        let window_project_groups: Vec<_> = self
779            .multi_workspace
780            .as_ref()
781            .and_then(|mw| mw.upgrade())
782            .map(|mw| mw.read(cx).project_group_keys())
783            .unwrap_or_default();
784
785        PopoverMenu::new("recent-projects-menu")
786            .menu(move |window, cx| {
787                Some(recent_projects::RecentProjects::popover(
788                    workspace.clone(),
789                    window_project_groups.clone(),
790                    None,
791                    focus_handle.clone(),
792                    window,
793                    cx,
794                ))
795            })
796            .trigger_with_tooltip(
797                Button::new("project_name_trigger", display_name)
798                    .label_size(LabelSize::Small)
799                    .tab_index(0isize)
800                    .when(self.worktree_count(cx) > 1, |this| {
801                        this.end_icon(
802                            Icon::new(IconName::ChevronDown)
803                                .size(IconSize::XSmall)
804                                .color(Color::Muted),
805                        )
806                    })
807                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
808                    .when(!is_project_selected, |s| s.color(Color::Muted)),
809                move |_window, cx| {
810                    Tooltip::for_action("Recent Projects", &zed_actions::OpenRecent::default(), cx)
811                },
812            )
813            .anchor(gpui::Anchor::TopLeft)
814            .into_any_element()
815    }
816
817    fn render_recent_projects_popover(
818        &self,
819        display_name: String,
820        is_project_selected: bool,
821        cx: &mut Context<Self>,
822    ) -> impl IntoElement {
823        let workspace = self.workspace.clone();
824
825        let focus_handle = workspace
826            .upgrade()
827            .map(|w| w.read(cx).focus_handle(cx))
828            .unwrap_or_else(|| cx.focus_handle());
829
830        let window_project_groups: Vec<_> = self
831            .multi_workspace
832            .as_ref()
833            .and_then(|mw| mw.upgrade())
834            .map(|mw| mw.read(cx).project_group_keys())
835            .unwrap_or_default();
836
837        PopoverMenu::new("sidebar-title-recent-projects-menu")
838            .menu(move |window, cx| {
839                Some(recent_projects::RecentProjects::popover(
840                    workspace.clone(),
841                    window_project_groups.clone(),
842                    None,
843                    focus_handle.clone(),
844                    window,
845                    cx,
846                ))
847            })
848            .trigger_with_tooltip(
849                Button::new("project_name_trigger", display_name)
850                    .label_size(LabelSize::Small)
851                    .tab_index(0isize)
852                    .when(self.worktree_count(cx) > 1, |this| {
853                        this.end_icon(
854                            Icon::new(IconName::ChevronDown)
855                                .size(IconSize::XSmall)
856                                .color(Color::Muted),
857                        )
858                    })
859                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
860                    .when(!is_project_selected, |s| s.color(Color::Muted)),
861                move |_window, cx| {
862                    Tooltip::for_action("Recent Projects", &zed_actions::OpenRecent::default(), cx)
863                },
864            )
865            .anchor(gpui::Anchor::TopLeft)
866    }
867
868    fn render_worktree_and_branch(
869        &self,
870        repository: Entity<project::git_store::Repository>,
871        linked_worktree_name: Option<SharedString>,
872        cx: &mut Context<Self>,
873    ) -> Option<AnyElement> {
874        let workspace = self.workspace.upgrade()?;
875
876        let (branch_name, icon_info, is_detached_head) = {
877            let repo = repository.read(cx);
878
879            let is_detached_head = repo.branch.is_none();
880
881            let branch_name = repo
882                .branch
883                .as_ref()
884                .map(|branch| branch.name())
885                .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
886                .or_else(|| {
887                    repo.head_commit.as_ref().map(|commit| {
888                        commit
889                            .sha
890                            .chars()
891                            .take(MAX_SHORT_SHA_LENGTH)
892                            .collect::<String>()
893                    })
894                });
895
896            let status = repo.status_summary();
897            let tracked = status.index + status.worktree;
898            let icon_info = if status.conflict > 0 {
899                (IconName::Warning, Color::VersionControlConflict)
900            } else if tracked.modified > 0 {
901                (IconName::SquareDot, Color::VersionControlModified)
902            } else if tracked.added > 0 || status.untracked > 0 {
903                (IconName::SquarePlus, Color::VersionControlAdded)
904            } else if tracked.deleted > 0 {
905                (IconName::SquareMinus, Color::VersionControlDeleted)
906            } else {
907                (IconName::GitBranch, Color::Muted)
908            };
909
910            (branch_name, icon_info, is_detached_head)
911        };
912
913        let settings = TitleBarSettings::get_global(cx);
914        let effective_repository = Some(repository);
915
916        let worktree_label: SharedString = linked_worktree_name.unwrap_or_else(|| "main".into());
917
918        let (creation_in_progress, is_switch) = self
919            .workspace
920            .upgrade()
921            .map(|ws| {
922                let creation = ws.read(cx).active_worktree_creation();
923                (creation.label.clone(), creation.is_switch)
924            })
925            .unwrap_or((None, false));
926        let is_creating = creation_in_progress.is_some();
927
928        let display_label: SharedString = if let Some(ref name) = creation_in_progress {
929            if is_switch {
930                format!("Loading {}…", name).into()
931            } else {
932                format!("Creating {}…", name).into()
933            }
934        } else {
935            worktree_label.clone()
936        };
937
938        let worktree_button = settings.show_worktree_name.then(|| {
939            let project = self.project.clone();
940            let workspace_handle = workspace.downgrade();
941            PopoverMenu::new("worktree-picker-menu")
942                .menu(move |window, cx| {
943                    // When opened from the title bar, focus is on the trigger
944                    // button (not a dock), so `focused_dock` is `None`. That's
945                    // fine — there's no prior dock focus to restore.
946                    Some(cx.new(|cx| {
947                        WorktreePicker::new(project.clone(), workspace_handle.clone(), window, cx)
948                    }))
949                })
950                .trigger_with_tooltip(
951                    Button::new("worktree_picker_trigger", display_label)
952                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
953                        .label_size(LabelSize::Small)
954                        .color(Color::Muted)
955                        .tab_index(0isize)
956                        .loading(is_creating)
957                        .start_icon(
958                            Icon::new(IconName::GitWorktree)
959                                .size(IconSize::XSmall)
960                                .color(Color::Muted),
961                        ),
962                    move |_window, cx| {
963                        Tooltip::with_meta(
964                            "Worktree",
965                            Some(&zed_actions::git::Worktree),
966                            format!("Currently In Use: {}", worktree_label),
967                            cx,
968                        )
969                    },
970                )
971                .anchor(gpui::Anchor::TopLeft)
972        });
973
974        let branch_picker = branch_name.and_then(|branch_name| {
975            settings.show_branch_name.then(|| {
976                let branch_tooltip_label = branch_name.clone();
977                let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon {
978                    icon_info
979                } else {
980                    (IconName::GitBranch, Color::Muted)
981                };
982
983                let trigger = if is_detached_head {
984                    Button::new("project_branch_trigger", "Create Branch")
985                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
986                        .label_size(LabelSize::Small)
987                        .tab_index(0isize)
988                        .start_icon(
989                            Icon::new(IconName::GitBranchPlus)
990                                .size(IconSize::XSmall)
991                                .color(Color::Muted),
992                        )
993                } else {
994                    Button::new("project_branch_trigger", branch_name)
995                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
996                        .label_size(LabelSize::Small)
997                        .color(Color::Muted)
998                        .tab_index(0isize)
999                        .start_icon(
1000                            Icon::new(branch_icon)
1001                                .size(IconSize::XSmall)
1002                                .color(branch_icon_color),
1003                        )
1004                };
1005
1006                PopoverMenu::new("branch-menu")
1007                    .menu(move |window, cx| {
1008                        Some(git_ui::git_picker::popover(
1009                            workspace.downgrade(),
1010                            effective_repository.clone(),
1011                            git_ui::git_picker::GitPickerTab::Branches,
1012                            gpui::rems(34.),
1013                            window,
1014                            cx,
1015                        ))
1016                    })
1017                    .trigger_with_tooltip(trigger, move |_window, cx| {
1018                        let meta = if is_detached_head {
1019                            format!("Detached HEAD: {}", branch_tooltip_label)
1020                        } else {
1021                            format!("Currently Checked Out: {}", branch_tooltip_label)
1022                        };
1023                        Tooltip::with_meta(
1024                            "Branch & Stash",
1025                            Some(&zed_actions::git::Branch),
1026                            meta,
1027                            cx,
1028                        )
1029                    })
1030                    .anchor(gpui::Anchor::TopLeft)
1031            })
1032        });
1033
1034        if worktree_button.is_none() && branch_picker.is_none() {
1035            return None;
1036        }
1037
1038        let show_separator = worktree_button.is_some() && branch_picker.is_some();
1039
1040        Some(
1041            h_flex()
1042                .gap_px()
1043                .children(worktree_button)
1044                .when(show_separator, |this| {
1045                    this.child(
1046                        Label::new("/")
1047                            .size(LabelSize::Small)
1048                            .color(Color::Muted)
1049                            .alpha(0.25),
1050                    )
1051                })
1052                .children(branch_picker)
1053                .into_any_element(),
1054        )
1055    }
1056
1057    fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1058        if window.is_window_active() {
1059            ActiveCall::global(cx)
1060                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
1061                .detach_and_log_err(cx);
1062        } else if cx.active_window().is_none() {
1063            ActiveCall::global(cx)
1064                .update(cx, |call, cx| call.set_location(None, cx))
1065                .detach_and_log_err(cx);
1066        }
1067        self.workspace
1068            .update(cx, |workspace, cx| {
1069                workspace.update_active_view_for_followers(window, cx);
1070            })
1071            .ok();
1072    }
1073
1074    fn active_call_changed(&mut self, cx: &mut Context<Self>) {
1075        self.observe_diagnostics(cx);
1076        cx.notify();
1077    }
1078
1079    fn observe_diagnostics(&mut self, cx: &mut Context<Self>) {
1080        let diagnostics = ActiveCall::global(cx)
1081            .read(cx)
1082            .room()
1083            .and_then(|room| room.read(cx).diagnostics().cloned());
1084
1085        if let Some(diagnostics) = diagnostics {
1086            self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify()));
1087        } else {
1088            self._diagnostics_subscription = None;
1089        }
1090    }
1091
1092    fn share_project(&mut self, cx: &mut Context<Self>) {
1093        let active_call = ActiveCall::global(cx);
1094        let project = self.project.clone();
1095        active_call
1096            .update(cx, |call, cx| call.share_project(project, cx))
1097            .detach_and_log_err(cx);
1098    }
1099
1100    fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1101        let active_call = ActiveCall::global(cx);
1102        let project = self.project.clone();
1103        active_call
1104            .update(cx, |call, cx| call.unshare_project(project, cx))
1105            .log_err();
1106    }
1107
1108    fn render_connection_status(
1109        &self,
1110        status: &client::Status,
1111        cx: &mut Context<Self>,
1112    ) -> Option<AnyElement> {
1113        match status {
1114            client::Status::ConnectionError
1115            | client::Status::ConnectionLost
1116            | client::Status::Reauthenticating
1117            | client::Status::Reconnecting
1118            | client::Status::ReconnectionError { .. } => Some(
1119                div()
1120                    .id("disconnected")
1121                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
1122                    .tooltip(Tooltip::text("Disconnected"))
1123                    .into_any_element(),
1124            ),
1125            client::Status::UpgradeRequired => {
1126                let auto_updater = auto_update::AutoUpdater::get(cx);
1127                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
1128                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Omega to Collaborate",
1129                    Some(AutoUpdateStatus::Installing { .. })
1130                    | Some(AutoUpdateStatus::Downloading { .. })
1131                    | Some(AutoUpdateStatus::Checking) => "Updating...",
1132                    Some(AutoUpdateStatus::Idle)
1133                    | Some(AutoUpdateStatus::Errored { .. })
1134                    | None => "Please update Omega to Collaborate",
1135                };
1136
1137                Some(
1138                    Button::new("connection-status", label)
1139                        .label_size(LabelSize::Small)
1140                        .on_click(|_, window, cx| {
1141                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
1142                                && auto_updater.read(cx).status().is_updated()
1143                            {
1144                                workspace::reload(cx);
1145                                return;
1146                            }
1147                            auto_update::check(&Default::default(), window, cx);
1148                        })
1149                        .into_any_element(),
1150                )
1151            }
1152            _ => None,
1153        }
1154    }
1155
1156    pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
1157        Button::new("omega_identity", "Omega Identity")
1158            .label_size(LabelSize::Small)
1159            .tab_index(0isize)
1160            .on_click(|_, window, cx| {
1161                window.dispatch_action(OpenOnboarding.boxed_clone(), cx);
1162            })
1163    }
1164
1165    pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
1166        let show_update_button = self.update_version.read(cx).show_update_in_menu_bar();
1167
1168        let user_store = self.user_store.clone();
1169        let workspace = self.workspace.clone();
1170        let user = user_store.read(cx).current_user();
1171
1172        let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
1173        let username = user.as_ref().map(|u| u.username.clone());
1174
1175        let is_signed_in = user.is_some();
1176
1177        let current_organization = user_store.read(cx).current_organization();
1178        let business_organization = current_organization
1179            .as_ref()
1180            .filter(|organization| !organization.is_personal);
1181        let organizations: Vec<_> = user_store
1182            .read(cx)
1183            .organizations()
1184            .iter()
1185            .map(|organization| {
1186                let plan = user_store.read(cx).plan_for_organization(&organization.id);
1187                (organization.clone(), plan)
1188            })
1189            .collect();
1190
1191        let show_user_picture = TitleBarSettings::get_global(cx).show_user_picture;
1192
1193        let trigger = if is_signed_in && show_user_picture {
1194            let avatar = user_avatar.map(|avatar| Avatar::new(avatar)).map(|avatar| {
1195                if show_update_button {
1196                    avatar.indicator(
1197                        div()
1198                            .absolute()
1199                            .bottom_0()
1200                            .right_0()
1201                            .child(Indicator::dot().color(Color::Accent)),
1202                    )
1203                } else {
1204                    avatar
1205                }
1206            });
1207
1208            ButtonLike::new("user-menu")
1209                .aria_label("User menu")
1210                .tab_index(0isize)
1211                .child(
1212                    h_flex()
1213                        .when_some(business_organization, |this, organization| {
1214                            this.gap_2()
1215                                .child(Label::new(&organization.name).size(LabelSize::Small))
1216                        })
1217                        .children(avatar),
1218                )
1219        } else {
1220            ButtonLike::new("user-menu")
1221                .aria_label("User menu")
1222                .tab_index(0isize)
1223                .child(Icon::new(IconName::ChevronDown).size(IconSize::Small))
1224        };
1225
1226        PopoverMenu::new("user-menu")
1227            .trigger(trigger)
1228            .menu(move |window, cx| {
1229                let username = username.clone();
1230                let current_organization = current_organization.clone();
1231                let organizations = organizations.clone();
1232                let user_store = user_store.clone();
1233                let workspace = workspace.clone();
1234
1235                let ai_enabled = !project::DisableAiSettings::get_global(cx).disable_ai;
1236                let current_layout = AgentSettings::get_layout(cx);
1237                let is_editor = matches!(current_layout, WindowLayout::Editor(_));
1238                let is_agent = matches!(current_layout, WindowLayout::Agent(_));
1239                let is_custom = matches!(current_layout, WindowLayout::Custom(_));
1240
1241                ContextMenu::build(window, cx, |menu, _, _cx| {
1242                    menu.when(is_signed_in, |this| {
1243                        let username = username.clone();
1244                        this.custom_entry(
1245                            move |_window, _cx| {
1246                                let username = username.clone().unwrap_or_default();
1247
1248                                h_flex()
1249                                    .w_full()
1250                                    .justify_between()
1251                                    .child(Label::new(username))
1252                                    .into_any_element()
1253                            },
1254                            move |_, cx| {
1255                                cx.open_url(&zed_urls::account_url(cx));
1256                            },
1257                        )
1258                        .separator()
1259                    })
1260                    .when(show_update_button, |this| {
1261                        this.custom_entry(
1262                            move |_window, _cx| {
1263                                h_flex()
1264                                    .w_full()
1265                                    .gap_1()
1266                                    .justify_between()
1267                                    .child(Label::new("Restart to update Omega").color(Color::Accent))
1268                                    .child(
1269                                        Icon::new(IconName::Download)
1270                                            .size(IconSize::Small)
1271                                            .color(Color::Accent),
1272                                    )
1273                                    .into_any_element()
1274                            },
1275                            move |_, cx| {
1276                                workspace::reload(cx);
1277                            },
1278                        )
1279                        .separator()
1280                    })
1281                    .map(|this| {
1282                        let mut this = this.header("Organization");
1283
1284                        for (organization, plan) in &organizations {
1285                            let organization = organization.clone();
1286                            let plan = *plan;
1287
1288                            let is_current =
1289                                current_organization
1290                                    .as_ref()
1291                                    .is_some_and(|current_organization| {
1292                                        current_organization.id == organization.id
1293                                    });
1294
1295                            this = this.custom_entry(
1296                                {
1297                                    let organization = organization.clone();
1298                                    move |_window, _cx| {
1299                                        h_flex()
1300                                            .w_full()
1301                                            .gap_4()
1302                                            .justify_between()
1303                                            .child(
1304                                                h_flex()
1305                                                    .gap_1()
1306                                                    .child(Label::new(&organization.name))
1307                                                    .when(is_current, |this| {
1308                                                        this.child(
1309                                                            Icon::new(IconName::Check)
1310                                                                .color(Color::Accent),
1311                                                        )
1312                                                    }),
1313                                            )
1314                                            .children(plan.map(|plan| PlanChip::new(plan)))
1315                                            .into_any_element()
1316                                    }
1317                                },
1318                                {
1319                                    let user_store = user_store.clone();
1320                                    let organization = organization.clone();
1321                                    let workspace = workspace.clone();
1322                                    move |window, cx| {
1323                                        let task = user_store.update(cx, |user_store, cx| {
1324                                            user_store
1325                                                .set_current_organization(organization.clone(), cx)
1326                                        });
1327                                        task.detach_and_notify_err(workspace.clone(), window, cx);
1328                                    }
1329                                },
1330                            );
1331                        }
1332
1333                        this.separator()
1334                    })
1335                    .action("Settings", zed_actions::OpenSettings.boxed_clone())
1336                    .action("Keymap", Box::new(zed_actions::OpenKeymap))
1337                    .action(
1338                        "Themes…",
1339                        zed_actions::theme_selector::Toggle::default().boxed_clone(),
1340                    )
1341                    .action(
1342                        "Icon Themes…",
1343                        zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
1344                    )
1345                    .action(
1346                        "Extensions",
1347                        zed_actions::Extensions::default().boxed_clone(),
1348                    )
1349                    .when(ai_enabled, |menu| {
1350                        menu.separator()
1351                            .submenu("Panel Layout", move |menu, _window, _cx| {
1352                                menu.toggleable_entry(
1353                                    "Classic",
1354                                    is_editor,
1355                                    IconPosition::Start,
1356                                    Some(UseClassicLayout.boxed_clone()),
1357                                    move |window, cx| {
1358                                        window.dispatch_action(UseClassicLayout.boxed_clone(), cx);
1359                                    },
1360                                )
1361                                .toggleable_entry(
1362                                    "Agentic",
1363                                    is_agent,
1364                                    IconPosition::Start,
1365                                    Some(UseAgenticLayout.boxed_clone()),
1366                                    move |window, cx| {
1367                                        window.dispatch_action(UseAgenticLayout.boxed_clone(), cx);
1368                                    },
1369                                )
1370                                .when(is_custom, |menu| {
1371                                    menu.item(
1372                                        ContextMenuEntry::new("Custom")
1373                                            .toggleable(IconPosition::Start, true)
1374                                            .disabled(true),
1375                                    )
1376                                })
1377                            })
1378                    })
1379                    .when(is_signed_in, |this| {
1380                        this.separator()
1381                            .action("Sign Out", client::SignOut.boxed_clone())
1382                    })
1383                })
1384                .into()
1385            })
1386            .anchor(Anchor::TopRight)
1387    }
1388}
1389
Served at tenant.openagents/omega Member data and write actions are omitted.