Skip to repository content1389 lines · 56.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:10:57.126Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
title_bar.rs
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