From a5673e9f721d3e563c76bc0523a24e3d29bdd5a7 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 16:42:19 -0700 Subject: [PATCH 1/8] title_bar: strip collab/call/livekit surface Step 1 of removing Zed's hosted collab feature from PaddleBoard. - Delete the entire crates/title_bar/src/collab.rs submodule (722 LOC): collaborator list, call controls, screen-share popover. - Remove ActiveCall observers and the share_project / unshare_project / observe_diagnostics / active_call_changed methods from title_bar.rs. - Drop call, channel, livekit_client from title_bar/Cargo.toml. - Update the "Please restart/update PaddleBoard to Collaborate" upgrade-required strings to drop the "to Collaborate" suffix; the surface still applies to the cloud LLM connection. cargo check -p title_bar is clean. The collab_ui crate still references title_bar::collab::* and will fail to build until it is itself stripped in a later commit on this branch. Release Notes: - N/A Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 3 - crates/title_bar/Cargo.toml | 5 - crates/title_bar/src/collab.rs | 722 ------------------------------ crates/title_bar/src/title_bar.rs | 70 +-- 4 files changed, 6 insertions(+), 794 deletions(-) delete mode 100644 crates/title_bar/src/collab.rs diff --git a/Cargo.lock b/Cargo.lock index 8633173166..da6551c8b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19422,8 +19422,6 @@ dependencies = [ "anyhow", "arrayvec", "auto_update", - "call", - "channel", "chrono", "client", "cloud_api_types", @@ -19433,7 +19431,6 @@ dependencies = [ "git_ui", "gpui", "icons", - "livekit_client", "notifications", "paddleboard_actions", "platform_title_bar", diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 6d325eadde..75d42aa15c 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -16,7 +16,6 @@ doctest = false default = [] test-support = [ - "call/test-support", "client/test-support", "gpui/test-support", @@ -34,8 +33,6 @@ anyhow.workspace = true auto_update.workspace = true fs.workspace = true platform_title_bar.workspace = true -call.workspace = true -channel.workspace = true chrono.workspace = true client.workspace = true cloud_api_types.workspace = true @@ -44,7 +41,6 @@ feature_flags.workspace = true git_ui.workspace = true gpui = { workspace = true, features = ["screen-capture"] } icons.workspace = true -livekit_client.workspace = true notifications.workspace = true project.workspace = true recent_projects.workspace = true @@ -68,7 +64,6 @@ arrayvec = "0.7.6" windows.workspace = true [dev-dependencies] -call = { workspace = true, features = ["test-support"] } client = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } notifications = { workspace = true, features = ["test-support"] } diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs deleted file mode 100644 index 36b2c95021..0000000000 --- a/crates/title_bar/src/collab.rs +++ /dev/null @@ -1,722 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -use call::{ActiveCall, Room}; -use channel::ChannelStore; -use client::{User, proto::PeerId}; -use gpui::{ - AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, TaskExt, - WeakEntity, canvas, point, -}; -use gpui::{App, Task, Window}; -use icons::IconName; -use livekit_client::ConnectionQuality; -use project::WorktreeSettings; -use remote_connection::RemoteConnectionModal; -use rpc::proto::{self}; -use settings::{Settings as _, SettingsLocation}; -use theme::ActiveTheme; -use ui::{ - Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor, - Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, -}; -use util::rel_path::RelPath; -use workspace::{ParticipantLocation, notifications::DetachAndPromptErr}; -use paddleboard_actions::ShowCallStats; - -use crate::TitleBar; - -fn format_stat(value: Option, format: impl Fn(f64) -> String) -> String { - match value { - Some(v) => format(v), - None => "—".to_string(), - } -} - -pub fn toggle_screen_sharing( - screen: anyhow::Result>>, - window: &mut Window, - cx: &mut App, -) { - let call = ActiveCall::global(cx).read(cx); - let toggle_screen_sharing = match screen { - Ok(screen) => { - let Some(room) = call.room().cloned() else { - return; - }; - - room.update(cx, |room, cx| { - let clicked_on_currently_shared_screen = - room.shared_screen_id().is_some_and(|screen_id| { - Some(screen_id) - == screen - .as_deref() - .and_then(|s| s.metadata().ok().map(|meta| meta.id)) - }); - let should_unshare_current_screen = room.is_sharing_screen(); - let unshared_current_screen = should_unshare_current_screen.then(|| { - telemetry::event!( - "Screen Share Disabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) - }); - if let Some(screen) = screen { - if !should_unshare_current_screen { - telemetry::event!( - "Screen Share Enabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - } - cx.spawn(async move |room, cx| { - unshared_current_screen.transpose()?; - if !clicked_on_currently_shared_screen { - room.update(cx, |room, cx| room.share_screen(screen, cx))? - .await - } else { - Ok(()) - } - }) - } else { - Task::ready(Ok(())) - } - }) - } - Err(e) => Task::ready(Err(e)), - }; - toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); -} - -pub fn toggle_mute(cx: &mut App) { - let call = ActiveCall::global(cx).read(cx); - if let Some(room) = call.room().cloned() { - room.update(cx, |room, cx| { - let operation = if room.is_muted() { - "Microphone Enabled" - } else { - "Microphone Disabled" - }; - telemetry::event!( - operation, - room_id = room.id(), - channel_id = room.channel_id(), - ); - - room.toggle_mute(cx) - }); - } -} - -pub fn toggle_deafen(cx: &mut App) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - room.update(cx, |room, cx| room.toggle_deafen(cx)); - } -} - -fn render_color_ribbon(color: Hsla) -> impl Element { - canvas( - move |_, _, _| {}, - move |bounds, _, window, _| { - let height = bounds.size.height; - let horizontal_offset = height; - let vertical_offset = height / 2.0; - let mut path = Path::new(bounds.bottom_left()); - path.curve_to( - bounds.origin + point(horizontal_offset, vertical_offset), - bounds.origin + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.top_right() + point(-horizontal_offset, vertical_offset)); - path.curve_to( - bounds.bottom_right(), - bounds.top_right() + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.bottom_left()); - window.paint_path(path, color); - }, - ) - .h_1() - .w_full() -} - -impl TitleBar { - pub(crate) fn render_collaborator_list( - &self, - _: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let room = ActiveCall::global(cx).read(cx).room().cloned(); - let current_user = self.user_store.read(cx).current_user(); - let client = self.client.clone(); - let project_id = self.project.read(cx).remote_id(); - let workspace = self.workspace.upgrade(); - - h_flex() - .id("collaborator-list") - .w_full() - .gap_1() - .overflow_x_scroll() - .when_some( - current_user.zip(client.peer_id()).zip(room), - |this, ((current_user, peer_id), room)| { - let player_colors = cx.theme().players(); - let room = room.read(cx); - let mut remote_participants = - room.remote_participants().values().collect::>(); - remote_participants.sort_by_key(|p| p.participant_index.0); - - let current_user_face_pile = self.render_collaborator( - ¤t_user, - peer_id, - true, - room.is_speaking(), - room.is_muted(), - None, - room, - project_id, - ¤t_user, - cx, - ); - - this.children(current_user_face_pile.map(|face_pile| { - v_flex() - .on_mouse_down(MouseButton::Left, |_, window, _| { - window.prevent_default() - }) - .child(face_pile) - .child(render_color_ribbon(player_colors.local().cursor)) - })) - .children(remote_participants.iter().filter_map(|collaborator| { - let player_color = - player_colors.color_for_participant(collaborator.participant_index.0); - let is_following = workspace - .as_ref()? - .read(cx) - .is_being_followed(collaborator.peer_id); - let is_present = project_id.is_some_and(|project_id| { - collaborator.location - == ParticipantLocation::SharedProject { project_id } - }); - - let facepile = self.render_collaborator( - &collaborator.user, - collaborator.peer_id, - is_present, - collaborator.speaking, - collaborator.muted, - is_following.then_some(player_color.selection), - room, - project_id, - ¤t_user, - cx, - )?; - - Some( - v_flex() - .id(("collaborator", collaborator.user.legacy_id)) - .child(facepile) - .child(render_color_ribbon(player_color.cursor)) - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| { - window.prevent_default() - }) - .on_click({ - let peer_id = collaborator.peer_id; - cx.listener(move |this, _, window, cx| { - cx.stop_propagation(); - - this.workspace - .update(cx, |workspace, cx| { - if is_following { - workspace.unfollow(peer_id, window, cx); - } else { - workspace.follow(peer_id, window, cx); - } - }) - .ok(); - }) - }) - .occlude() - .tooltip({ - let login = collaborator.user.github_login.clone(); - Tooltip::text(format!("Follow {login}")) - }), - ) - })) - }, - ) - } - - fn render_collaborator( - &self, - user: &Arc, - peer_id: PeerId, - is_present: bool, - is_speaking: bool, - is_muted: bool, - leader_selection_color: Option, - room: &Room, - project_id: Option, - current_user: &Arc, - cx: &App, - ) -> Option
{ - if room.role_for_user(user.legacy_id) == Some(proto::ChannelRole::Guest) { - return None; - } - - const FACEPILE_LIMIT: usize = 3; - let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); - let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT); - - Some( - div() - .m_0p5() - .p_0p5() - // When the collaborator is not followed, still draw this wrapper div, but leave - // it transparent, so that it does not shift the layout when following. - .when_some(leader_selection_color, |div, color| { - div.rounded_sm().bg(color) - }) - .child( - Facepile::empty() - .child( - Avatar::new(user.avatar_uri.clone()) - .grayscale(!is_present) - .border_color(if is_speaking { - cx.theme().status().info - } else { - // We draw the border in a transparent color rather to avoid - // the layout shift that would come with adding/removing the border. - gpui::transparent_black() - }) - .when(is_muted, |avatar| { - avatar.indicator( - AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted) - .tooltip({ - let github_login = user.github_login.clone(); - Tooltip::text(format!("{} is muted", github_login)) - }), - ) - }), - ) - .children(followers.iter().take(FACEPILE_LIMIT).filter_map( - |follower_peer_id| { - let follower = room - .remote_participants() - .values() - .find_map(|p| { - (p.peer_id == *follower_peer_id).then_some(&p.user) - }) - .or_else(|| { - (self.client.peer_id() == Some(*follower_peer_id)) - .then_some(current_user) - })? - .clone(); - - Some(div().mt(-px(4.)).child( - Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)), - )) - }, - )) - .children(if extra_count > 0 { - Some( - Label::new(format!("+{extra_count}")) - .ml_1() - .into_any_element(), - ) - } else { - None - }), - ), - ) - } - - pub(crate) fn render_call_controls( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec { - let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else { - return Vec::new(); - }; - - let is_connecting_to_project = self - .workspace - .update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .is_some() - }) - .unwrap_or(false); - - let room = room.read(cx); - let project = self.project.read(cx); - let is_local = project.is_local() || project.is_via_remote_server(); - let is_shared = is_local && project.is_shared(); - let is_muted = room.is_muted(); - let muted_by_user = room.muted_by_user(); - let is_deafened = room.is_deafened().unwrap_or(false); - let is_screen_sharing = room.is_sharing_screen(); - let can_use_microphone = room.can_use_microphone(); - let can_share_projects = room.can_share_projects(); - let screen_sharing_supported = cx.is_screen_capture_supported(); - - let stats = room - .diagnostics() - .map(|d| d.read(cx).stats().clone()) - .unwrap_or_default(); - - let channel_store = ChannelStore::global(cx); - let channel = room - .channel_id() - .and_then(|channel_id| channel_store.read(cx).channel_for_id(channel_id).cloned()); - - let mut children = Vec::new(); - - let effective_quality = stats.effective_quality.unwrap_or(ConnectionQuality::Lost); - let (signal_icon, signal_color, quality_label) = match effective_quality { - ConnectionQuality::Excellent => { - (IconName::SignalHigh, Some(Color::Success), "Excellent") - } - ConnectionQuality::Good => (IconName::SignalHigh, None, "Good"), - ConnectionQuality::Poor => (IconName::SignalMedium, Some(Color::Warning), "Poor"), - ConnectionQuality::Lost => (IconName::SignalLow, Some(Color::Error), "Lost"), - }; - - let quality_label: SharedString = quality_label.into(); - - children.push( - h_flex() - .gap_1() - .child( - IconButton::new("leave-call", IconName::Exit) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text("Leave Call")) - .icon_size(IconSize::Small) - .on_click(move |_, _window, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }), - ) - .child(Divider::vertical().color(DividerColor::Border)) - .into_any_element(), - ); - - children.push( - IconButton::new("call-quality", signal_icon) - .icon_size(IconSize::Small) - .when_some(signal_color, |button, color| button.icon_color(color)) - .tooltip(move |_window, cx| { - let quality_label = quality_label.clone(); - let latency = format_stat(stats.latency_ms, |v| format!("{:.0}ms", v)); - let jitter = format_stat(stats.jitter_ms, |v| format!("{:.0}ms", v)); - let packet_loss = format_stat(stats.packet_loss_pct, |v| format!("{:.1}%", v)); - let input_lag = - format_stat(stats.input_lag.map(|d| d.as_secs_f64() * 1000.0), |v| { - format!("{:.1}ms", v) - }); - - Tooltip::with_meta( - format!("Connection: {quality_label}"), - Some(&ShowCallStats), - format!( - "Latency: {latency} · Jitter: {jitter} · Loss: {packet_loss} · Input lag: {input_lag}", - ), - cx, - ) - }) - .on_click(move |_, window, cx| { - window.dispatch_action(Box::new(ShowCallStats), cx); - }) - .into_any_element(), - ); - - if is_local && can_share_projects && !is_connecting_to_project { - let is_sharing_disabled = channel.is_some_and(|channel| match channel.visibility { - proto::ChannelVisibility::Public => project.visible_worktrees(cx).any(|worktree| { - let worktree_id = worktree.read(cx).id(); - - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - WorktreeSettings::get(settings_location, cx).prevent_sharing_in_public_channels - }), - proto::ChannelVisibility::Members => false, - }); - - children.push( - Button::new( - "toggle_sharing", - if is_shared { "Unshare" } else { "Share" }, - ) - .tooltip(Tooltip::text(if is_shared { - "Stop sharing project with call participants" - } else { - "Share project with call participants" - })) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_shared) - .label_size(LabelSize::Small) - .when(is_sharing_disabled, |parent| { - parent.disabled(true).tooltip(Tooltip::text( - "This project may not be shared in a public channel.", - )) - }) - .on_click(cx.listener(move |this, _, window, cx| { - if is_shared { - this.unshare_project(window, cx); - } else { - this.share_project(cx); - } - })) - .into_any_element(), - ); - } - - if can_use_microphone { - children.push( - IconButton::new( - "mute-microphone", - if is_muted { - IconName::MicMute - } else { - IconName::Mic - }, - ) - .tooltip(move |_window, cx| { - if is_muted { - if is_deafened { - Tooltip::with_meta( - "Unmute Microphone", - None, - "Audio will be unmuted", - cx, - ) - } else { - Tooltip::simple("Unmute Microphone", cx) - } - } else { - Tooltip::simple("Mute Microphone", cx) - } - }) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_muted) - .selected_style(ButtonStyle::Tinted(TintColor::Error)) - .on_click(move |_, _window, cx| toggle_mute(cx)) - .into_any_element(), - ); - } - - children.push( - IconButton::new( - "mute-sound", - if is_deafened { - IconName::AudioOff - } else { - IconName::AudioOn - }, - ) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Error)) - .icon_size(IconSize::Small) - .toggle_state(is_deafened) - .tooltip(move |_window, cx| { - if is_deafened { - let label = "Unmute Audio"; - - if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be unmuted", cx) - } else { - Tooltip::simple(label, cx) - } - } else { - let label = "Mute Audio"; - - if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be muted", cx) - } else { - Tooltip::simple(label, cx) - } - } - }) - .on_click(move |_, _, cx| toggle_deafen(cx)) - .into_any_element(), - ); - - if can_use_microphone && screen_sharing_supported { - #[cfg(target_os = "linux")] - let is_wayland = gpui::guess_compositor() == "Wayland"; - #[cfg(not(target_os = "linux"))] - let is_wayland = false; - - let trigger = IconButton::new("screen-share", IconName::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_screen_sharing) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text(if is_screen_sharing { - "Stop Sharing Screen" - } else { - "Share Screen" - })) - .on_click(move |_, window, cx| { - let should_share = ActiveCall::global(cx) - .read(cx) - .room() - .is_some_and(|room| !room.read(cx).is_sharing_screen()); - - #[cfg(target_os = "linux")] - { - if is_wayland - && let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() - { - let task = room.update(cx, |room, cx| { - if should_share { - room.share_screen_wayland(cx) - } else { - room.unshare_screen(true, cx) - .map(|()| Task::ready(Ok(()))) - .unwrap_or_else(|e| Task::ready(Err(e))) - } - }); - task.detach_and_prompt_err( - "Sharing Screen Failed", - window, - cx, - |e, _, _| Some(format!("{e:?}")), - ); - } - } - if !is_wayland { - window - .spawn(cx, async move |cx| { - let screen = if should_share { - cx.update(|_, cx| pick_default_screen(cx))?.await - } else { - Ok(None) - }; - cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; - - Result::<_, anyhow::Error>::Ok(()) - }) - .detach(); - } - }); - - if is_wayland { - children.push(trigger.into_any_element()); - } else { - children.push( - SplitButton::new( - trigger.render(window, cx), - self.render_screen_list().into_any_element(), - ) - .style(SplitButtonStyle::Transparent) - .into_any_element(), - ); - } - } - - children.push(div().pr_2().into_any_element()); - - children - } - - fn render_screen_list(&self) -> impl IntoElement { - PopoverMenu::new("screen-share-screen-list") - .with_handle(self.screen_share_popover_handle.clone()) - .trigger( - ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger") - .child( - h_flex() - .mx_neg_0p5() - .h_full() - .justify_center() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ) - .toggle_state(self.screen_share_popover_handle.is_deployed()), - ) - .menu(|window, cx| { - let screens = cx.screen_capture_sources(); - Some(ContextMenu::build(window, cx, |context_menu, _, cx| { - cx.spawn(async move |this: WeakEntity, cx| { - let screens = screens.await??; - this.update(cx, |this, cx| { - let active_screenshare_id = ActiveCall::global(cx) - .read(cx) - .room() - .and_then(|room| room.read(cx).shared_screen_id()); - for screen in screens { - let Ok(meta) = screen.metadata() else { - continue; - }; - - let label = meta - .label - .clone() - .unwrap_or_else(|| SharedString::from("Unknown screen")); - let resolution = SharedString::from(format!( - "{} × {}", - meta.resolution.width.0, meta.resolution.height.0 - )); - this.push_item(ContextMenuItem::CustomEntry { - entry_render: Box::new(move |_, _| { - h_flex() - .gap_2() - .child( - Icon::new(IconName::Screen) - .size(IconSize::XSmall) - .map(|this| { - if active_screenshare_id == Some(meta.id) { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - }), - ) - .child(Label::new(label.clone())) - .child( - Label::new(resolution.clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .into_any() - }), - selectable: true, - documentation_aside: None, - handler: Rc::new(move |_, window, cx| { - toggle_screen_sharing(Ok(Some(screen.clone())), window, cx); - }), - }); - } - }) - }) - .detach_and_log_err(cx); - context_menu - })) - }) - } -} - -/// Picks the screen to share when clicking on the main screen sharing button. -fn pick_default_screen(cx: &App) -> Task>>> { - let source = cx.screen_capture_sources(); - cx.spawn(async move |_| { - let available_sources = source.await??; - Ok(available_sources - .iter() - .find(|it| { - it.as_ref() - .metadata() - .is_ok_and(|meta| meta.is_main.unwrap_or_default()) - }) - .or_else(|| available_sources.first()) - .cloned()) - }) -} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index c34c1a8f5a..032f0aeb71 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -1,5 +1,4 @@ mod application_menu; -pub mod collab; mod onboarding_banner; mod plan_chip; mod title_bar_settings; @@ -22,7 +21,6 @@ use crate::application_menu::{ }; use auto_update::AutoUpdateStatus; -use call::ActiveCall; use client::{Client, UserStore, zed_urls}; use cloud_api_types::Plan; use feature_flags::{FeatureFlagAppExt as _, SkillsFeatureFlag}; @@ -30,7 +28,7 @@ use feature_flags::{FeatureFlagAppExt as _, SkillsFeatureFlag}; use gpui::{ Action, Anchor, Animation, AnimationExt, AnyElement, App, Context, Element, Entity, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, - StatefulInteractiveElement, Styled, Subscription, TaskExt, WeakEntity, Window, actions, div, + StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, pulsating_between, }; use onboarding_banner::OnboardingBanner; @@ -47,7 +45,7 @@ use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ Avatar, ButtonLike, ContextMenu, ContextMenuEntry, IconWithIndicator, Indicator, PopoverMenu, - PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height, + TintColor, Tooltip, prelude::*, utils::platform_title_bar_height, }; use update_version::UpdateVersion; use util::ResultExt; @@ -157,8 +155,6 @@ pub struct TitleBar { _subscriptions: Vec, banner: Option>, update_version: Entity, - screen_share_popover_handle: PopoverMenuHandle, - _diagnostics_subscription: Option, } impl Render for TitleBar { @@ -276,8 +272,6 @@ impl Render for TitleBar { .into_any_element(), ); - children.push(self.render_collaborator_list(window, cx).into_any_element()); - if title_bar_settings.show_onboarding_banner { if let Some(banner) = &self.banner { children.push(banner.clone().into_any_element()) @@ -313,7 +307,6 @@ impl Render for TitleBar { }) .gap_1() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .children(self.render_call_controls(window, cx)) .children(self.render_connection_status(status, cx)) .child(self.update_version.clone()) .when( @@ -392,7 +385,6 @@ impl TitleBar { let git_store = project.read(cx).git_store().clone(); let user_store = workspace.app_state().user_store.clone(); let client = workspace.app_state().client.clone(); - let active_call = ActiveCall::global(cx); let platform_style = PlatformStyle::platform(); let application_menu = match platform_style { @@ -415,7 +407,6 @@ impl TitleBar { }), ); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed)); subscriptions.push( cx.subscribe(&git_store, move |_, _, event, cx| match event { @@ -470,7 +461,7 @@ impl TitleBar { .visible_when(|cx| cx.has_flag::()) })); - let mut this = Self { + Self { platform_titlebar, application_menu, workspace: workspace.weak_handle(), @@ -481,13 +472,7 @@ impl TitleBar { _subscriptions: subscriptions, banner, update_version, - screen_share_popover_handle: PopoverMenuHandle::default(), - _diagnostics_subscription: None, - }; - - this.observe_diagnostics(cx); - - this + } } fn worktree_count(&self, cx: &App) -> usize { @@ -1052,15 +1037,6 @@ impl TitleBar { } fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { - if window.is_window_active() { - ActiveCall::global(cx) - .update(cx, |call, cx| call.set_location(Some(&self.project), cx)) - .detach_and_log_err(cx); - } else if cx.active_window().is_none() { - ActiveCall::global(cx) - .update(cx, |call, cx| call.set_location(None, cx)) - .detach_and_log_err(cx); - } self.workspace .update(cx, |workspace, cx| { workspace.update_active_view_for_followers(window, cx); @@ -1068,40 +1044,6 @@ impl TitleBar { .ok(); } - fn active_call_changed(&mut self, cx: &mut Context) { - self.observe_diagnostics(cx); - cx.notify(); - } - - fn observe_diagnostics(&mut self, cx: &mut Context) { - let diagnostics = ActiveCall::global(cx) - .read(cx) - .room() - .and_then(|room| room.read(cx).diagnostics().cloned()); - - if let Some(diagnostics) = diagnostics { - self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify())); - } else { - self._diagnostics_subscription = None; - } - } - - fn share_project(&mut self, cx: &mut Context) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.share_project(project, cx)) - .detach_and_log_err(cx); - } - - fn unshare_project(&mut self, _: &mut Window, cx: &mut Context) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.unshare_project(project, cx)) - .log_err(); - } - fn render_connection_status( &self, status: &client::Status, @@ -1122,13 +1064,13 @@ impl TitleBar { client::Status::UpgradeRequired => { let auto_updater = auto_update::AutoUpdater::get(cx); let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { - Some(AutoUpdateStatus::Updated { .. }) => "Please restart PaddleBoard to Collaborate", + Some(AutoUpdateStatus::Updated { .. }) => "Please restart PaddleBoard", Some(AutoUpdateStatus::Installing { .. }) | Some(AutoUpdateStatus::Downloading { .. }) | Some(AutoUpdateStatus::Checking) => "Updating...", Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored { .. }) - | None => "Please update PaddleBoard to Collaborate", + | None => "Please update PaddleBoard", }; Some( From e420de86ad2cd9393d581aa9d59d67b1dec34963 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 16:48:40 -0700 Subject: [PATCH 2/8] file_finder: drop channel-notes search integration Step 2 of removing Zed's hosted collab feature from PaddleBoard. The file finder used to surface "Channel Notes" entries inline with file matches, dispatching workspace::OpenChannelNotesById on confirm. Without the channel/collab_ui crates there's no channel store to read from, so drop the entire Match::Channel variant and every match arm that handled it. - Remove Match::Channel from the Match enum and every place it's matched (relative_path, abs_path, panel_match, match_score, labels_for_match, confirm, render_match, and the test helpers). - Remove the channel-matching block (~60 lines) that read channels from ChannelStore and scored them by name. - Drop the channel_store field from FileFinderDelegate. - Drop channel, client, and fuzzy from file_finder/Cargo.toml. `fuzzy::StringMatch{,Candidate}` were only used by the channel path; `client::ChannelId` likewise. cargo check -p file_finder --tests is clean. The include_channels setting in FileFinderSettings stays (settings_content/open_path_prompt crates untouched); it just no longer has anything to gate. Release Notes: - Removed the file finder's "Channel Notes" inline matches (Zed Cloud collab feature, never functional in PaddleBoard). Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 3 - crates/file_finder/Cargo.toml | 3 - crates/file_finder/src/file_finder.rs | 133 ++------------------ crates/file_finder/src/file_finder_tests.rs | 2 - 4 files changed, 13 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da6551c8b7..4caa8c204f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6513,14 +6513,11 @@ name = "file_finder" version = "0.1.0" dependencies = [ "anyhow", - "channel", - "client", "collections", "ctor", "editor", "file_icons", "futures 0.3.32", - "fuzzy", "fuzzy_nucleo", "gpui", "language", diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 967fdd34b4..68166f1162 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -14,13 +14,10 @@ doctest = false [dependencies] anyhow.workspace = true -channel.workspace = true -client.workspace = true collections.workspace = true editor.workspace = true file_icons.workspace = true futures.workspace = true -fuzzy.workspace = true fuzzy_nucleo.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index edc41669a4..28ee1bd1ca 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -4,12 +4,9 @@ mod file_finder_tests; use futures::future::join_all; pub use open_path_prompt::OpenPathDelegate; -use channel::ChannelStore; -use client::ChannelId; use collections::HashMap; use editor::Editor; use file_icons::FileIcons; -use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy_nucleo::{PathMatch, PathMatchCandidate}; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, @@ -49,7 +46,7 @@ use util::{ rel_path::RelPath, }; use workspace::{ - ModalView, OpenChannelNotesById, OpenOptions, OpenVisible, SplitDirection, Workspace, + ModalView, OpenOptions, OpenVisible, SplitDirection, Workspace, item::PreviewTabsSettings, notifications::NotifyResultExt, pane, }; use paddleboard_actions::search::ToggleIncludeIgnored; @@ -338,7 +335,6 @@ impl FileFinder { path: m.0.path.clone(), }, Match::CreateNew(p) => p.clone(), - Match::Channel { .. } => return, }; let open_task = workspace.update(cx, move |workspace, cx| { workspace.split_path_preview(path, false, Some(split_direction), window, cx) @@ -397,7 +393,6 @@ pub struct FileFinderDelegate { file_finder: WeakEntity, workspace: WeakEntity, project: Entity, - channel_store: Option>, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, @@ -463,11 +458,6 @@ enum Match { panel_match: Option, }, Search(ProjectPanelOrdMatch), - Channel { - channel_id: ChannelId, - channel_name: SharedString, - string_match: StringMatch, - }, CreateNew(ProjectPath), } @@ -476,7 +466,7 @@ impl Match { match self { Match::History { path, .. } => Some(&path.project.path), Match::Search(panel_match) => Some(&panel_match.0.path), - Match::Channel { .. } | Match::CreateNew(_) => None, + Match::CreateNew(_) => None, } } @@ -490,7 +480,7 @@ impl Match { .read(cx) .absolutize(&path_match.path), ), - Match::Channel { .. } | Match::CreateNew(_) => None, + Match::CreateNew(_) => None, } } @@ -498,7 +488,7 @@ impl Match { match self { Match::History { panel_match, .. } => panel_match.as_ref(), Match::Search(panel_match) => Some(panel_match), - Match::Channel { .. } | Match::CreateNew(_) => None, + Match::CreateNew(_) => None, } } } @@ -678,7 +668,6 @@ impl Matches { match m { Match::History { panel_match, .. } => panel_match.as_ref().map_or(0.0, |pm| pm.0.score), Match::Search(pm) => pm.0.score, - Match::Channel { string_match, .. } => string_match.score, Match::CreateNew(_) => 0.0, } } @@ -917,16 +906,10 @@ impl FileFinderDelegate { cx: &mut Context, ) -> Self { Self::subscribe_to_updates(&project, window, cx); - let channel_store = if FileFinderSettings::get_global(cx).include_channels { - ChannelStore::try_global(cx) - } else { - None - }; Self { file_finder, workspace, project, - channel_store, search_count: 0, latest_search_id: 0, latest_search_did_cancel: false, @@ -1059,68 +1042,6 @@ impl FileFinderDelegate { path_style, ); - // Add channel matches - if let Some(channel_store) = &self.channel_store { - let channel_store = channel_store.read(cx); - let channels: Vec<_> = channel_store.channels().cloned().collect(); - if !channels.is_empty() { - let candidates = channels - .iter() - .enumerate() - .map(|(id, channel)| StringMatchCandidate::new(id, &channel.name)); - let channel_query = query.path_query(); - let query_lower = channel_query.to_lowercase(); - let mut channel_matches = Vec::new(); - for candidate in candidates { - let channel_name = candidate.string; - let name_lower = channel_name.to_lowercase(); - - let mut positions = Vec::new(); - let mut query_idx = 0; - for (name_idx, name_char) in name_lower.char_indices() { - if query_idx < query_lower.len() { - let query_char = - query_lower[query_idx..].chars().next().unwrap_or_default(); - if name_char == query_char { - positions.push(name_idx); - query_idx += query_char.len_utf8(); - } - } - } - - if query_idx == query_lower.len() { - let channel = &channels[candidate.id]; - let score = if name_lower == query_lower { - 1.0 - } else if name_lower.starts_with(&query_lower) { - 0.8 - } else { - 0.5 * (query_lower.len() as f64 / name_lower.len() as f64) - }; - channel_matches.push(Match::Channel { - channel_id: channel.id, - channel_name: channel.name.clone(), - string_match: StringMatch { - candidate_id: candidate.id, - score, - positions, - string: channel_name, - }, - }); - } - } - for channel_match in channel_matches { - match self - .matches - .position(&channel_match, self.currently_opened_path.as_ref()) - { - Ok(_duplicate) => {} - Err(ix) => self.matches.matches.insert(ix, channel_match), - } - } - } - } - let query_path = query.raw_query.as_str(); if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) { let available_worktree = self @@ -1236,16 +1157,6 @@ impl FileFinderDelegate { } } Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style), - Match::Channel { - channel_name, - string_match, - .. - } => ( - channel_name.to_string(), - string_match.positions.clone(), - "Channel Notes".to_string(), - vec![], - ), Match::CreateNew(project_path) => ( format!("Create file: {}", project_path.path.display(path_style)), vec![], @@ -1614,16 +1525,6 @@ impl PickerDelegate for FileFinderDelegate { if let Some(m) = self.matches.get(self.selected_index()) && let Some(workspace) = self.workspace.upgrade() { - // Channel matches are handled separately since they dispatch an action - // rather than directly opening a file path. - if let Match::Channel { channel_id, .. } = m { - let channel_id = channel_id.0; - let finder = self.file_finder.clone(); - window.dispatch_action(OpenChannelNotesById { channel_id }.boxed_clone(), cx); - finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err(); - return; - } - let open_task = workspace.update(cx, |workspace, cx| { let split_or_open = |workspace: &mut Workspace, @@ -1716,7 +1617,6 @@ impl PickerDelegate for FileFinderDelegate { window, cx, ), - Match::Channel { .. } => unreachable!("handled above"), } }); @@ -1782,10 +1682,6 @@ impl PickerDelegate for FileFinderDelegate { .flex_none() .size(IconSize::Small.rems()) .into_any_element(), - Match::Channel { .. } => v_flex() - .flex_none() - .size(IconSize::Small.rems()) - .into_any_element(), Match::CreateNew(_) => Icon::new(IconName::Plus) .color(Color::Muted) .size(IconSize::Small) @@ -1793,18 +1689,15 @@ impl PickerDelegate for FileFinderDelegate { }; let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx); - let file_icon = match path_match { - Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)), - _ => maybe!({ - if !settings.file_icons { - return None; - } - let abs_path = path_match.abs_path(&self.project, cx)?; - let file_name = abs_path.file_name()?; - let icon = FileIcons::get_icon(file_name.as_ref(), cx)?; - Some(Icon::from_path(icon).color(Color::Muted)) - }), - }; + let file_icon = maybe!({ + if !settings.file_icons { + return None; + } + let abs_path = path_match.abs_path(&self.project, cx)?; + let file_name = abs_path.file_name()?; + let icon = FileIcons::get_icon(file_name.as_ref(), cx)?; + Some(Icon::from_path(icon).color(Color::Muted)) + }); Some( ListItem::new(ix) diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index a9d67dd31a..5dd593b3c7 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -4093,7 +4093,6 @@ fn collect_search_matches(picker: &Picker) -> SearchEntries search_entries.search_matches.push(path_match.0.clone()); } Match::CreateNew(_) => {} - Match::Channel { .. } => {} } } search_entries @@ -4128,7 +4127,6 @@ fn assert_match_at_position( Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()), Match::Search(path_match) => path_match.0.path.file_name(), Match::CreateNew(project_path) => project_path.path.file_name(), - Match::Channel { channel_name, .. } => Some(channel_name.as_str()), } .unwrap(); assert_eq!(match_file_name, expected_file_name); From 9099e043e4ed1ac55b4eca86ed4cfda49dafe680 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 16:51:58 -0700 Subject: [PATCH 3/8] notifications: delete the Zed Cloud NotificationStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3 of removing Zed's hosted collab feature from PaddleBoard. NotificationStore handled the rpc::Notification stream from the Zed Cloud collab server: contact requests, channel invitations, channel-message mentions. Without the channel/collab_ui crates there's nothing left to surface from it. Only collab_ui (going away) and collab integration tests (going away) used it. The local StatusToast UI is a separate surface in this same crate and stays — agent_ui, keymap_editor, etc. all use it. - Delete crates/notifications/src/notification_store.rs (428 LOC). - Reduce notifications.rs to just `pub mod status_toast;`. - Strip channel, client, rpc, sum_tree, time, anyhow, futures-lite, util from Cargo.toml — all were only used by NotificationStore. cargo check -p notifications is clean. paddleboard still calls notifications::init() (in main.rs and zed.rs) which no longer exists; those call sites are removed in a later commit on this branch as part of stripping paddleboard's collab_ui wiring. Release Notes: - N/A Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 8 - crates/notifications/Cargo.toml | 14 - .../notifications/src/notification_store.rs | 428 ------------------ crates/notifications/src/notifications.rs | 3 - 4 files changed, 453 deletions(-) delete mode 100644 crates/notifications/src/notification_store.rs diff --git a/Cargo.lock b/Cargo.lock index 4caa8c204f..744905049a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11798,18 +11798,10 @@ dependencies = [ name = "notifications" version = "0.1.0" dependencies = [ - "anyhow", - "channel", - "client", "component", - "futures-lite 1.13.0", "gpui", "paddleboard_actions", - "rpc", - "sum_tree", - "time", "ui", - "util", "workspace", ] diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml index 99d97fdf2f..96a832cc2c 100644 --- a/crates/notifications/Cargo.toml +++ b/crates/notifications/Cargo.toml @@ -14,29 +14,15 @@ doctest = false [features] test-support = [ - "channel/test-support", - "gpui/test-support", - "rpc/test-support", ] [dependencies] -anyhow.workspace = true -channel.workspace = true -client.workspace = true -futures-lite.workspace = true component.workspace = true gpui.workspace = true -rpc.workspace = true -sum_tree.workspace = true -time.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true paddleboard_actions.workspace = true [dev-dependencies] -client = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } -rpc = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs deleted file mode 100644 index 2e23b945a6..0000000000 --- a/crates/notifications/src/notification_store.rs +++ /dev/null @@ -1,428 +0,0 @@ -use anyhow::{Context as _, Result}; -use channel::ChannelStore; -use client::{ChannelId, Client, UserStore}; -use futures_lite::stream::StreamExt; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task}; -use rpc::{Notification, TypedEnvelope, proto}; -use std::{ops::Range, sync::Arc}; -use sum_tree::{Bias, Dimensions, SumTree}; -use time::OffsetDateTime; -use util::ResultExt; - -pub fn init(client: Arc, user_store: Entity, cx: &mut App) { - let notification_store = cx.new(|cx| NotificationStore::new(client, user_store, cx)); - cx.set_global(GlobalNotificationStore(notification_store)); -} - -struct GlobalNotificationStore(Entity); - -impl Global for GlobalNotificationStore {} - -pub struct NotificationStore { - client: Arc, - user_store: Entity, - channel_store: Entity, - notifications: SumTree, - loaded_all_notifications: bool, - _watch_connection_status: Task>, - _subscriptions: Vec, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum NotificationEvent { - NotificationsUpdated { - old_range: Range, - new_count: usize, - }, - NewNotification { - entry: NotificationEntry, - }, - NotificationRemoved { - entry: NotificationEntry, - }, - NotificationRead { - entry: NotificationEntry, - }, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct NotificationEntry { - pub id: u64, - pub notification: Notification, - pub timestamp: OffsetDateTime, - pub is_read: bool, - pub response: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct NotificationSummary { - max_id: u64, - count: usize, - unread_count: usize, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct Count(usize); - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct NotificationId(u64); - -impl NotificationStore { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { - let mut connection_status = client.status(); - let watch_connection_status = cx.spawn(async move |this, cx| { - while let Some(status) = connection_status.next().await { - let this = this.upgrade()?; - match status { - client::Status::Connected { .. } => { - if let Some(task) = this.update(cx, |this, cx| this.handle_connect(cx)) { - task.await.log_err()?; - } - } - _ => { - this.update(cx, |this, cx| this.handle_disconnect(cx)); - } - } - } - Some(()) - }); - - Self { - channel_store: ChannelStore::global(cx), - notifications: Default::default(), - loaded_all_notifications: false, - _watch_connection_status: watch_connection_status, - _subscriptions: vec![ - client.add_message_handler(cx.weak_entity(), Self::handle_new_notification), - client.add_message_handler(cx.weak_entity(), Self::handle_delete_notification), - ], - user_store, - client, - } - } - - pub fn notification_count(&self) -> usize { - self.notifications.summary().count - } - - pub fn unread_notification_count(&self) -> usize { - self.notifications.summary().unread_count - } - - // Get the nth newest notification. - pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { - let count = self.notifications.summary().count; - if ix >= count { - return None; - } - let ix = count - 1 - ix; - let (.., item) = self - .notifications - .find::((), &Count(ix), Bias::Right); - item - } - pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { - let (.., item) = - self.notifications - .find::((), &NotificationId(id), Bias::Left); - if let Some(item) = item - && item.id == id - { - return Some(item); - } - None - } - - pub fn load_more_notifications( - &self, - clear_old: bool, - cx: &mut Context, - ) -> Option>> { - if self.loaded_all_notifications && !clear_old { - return None; - } - - let before_id = if clear_old { - None - } else { - self.notifications.first().map(|entry| entry.id) - }; - let request = self.client.request(proto::GetNotifications { before_id }); - Some(cx.spawn(async move |this, cx| { - let this = this - .upgrade() - .context("Notification store was dropped while loading notifications")?; - - let response = request.await?; - this.update(cx, |this, _| this.loaded_all_notifications = response.done); - Self::add_notifications( - this, - response.notifications, - AddNotificationsOptions { - is_new: false, - clear_old, - includes_first: response.done, - }, - cx, - ) - .await?; - Ok(()) - })) - } - - fn handle_connect(&mut self, cx: &mut Context) -> Option>> { - self.notifications = Default::default(); - cx.notify(); - self.load_more_notifications(true, cx) - } - - fn handle_disconnect(&mut self, cx: &mut Context) { - cx.notify() - } - - async fn handle_new_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - Self::add_notifications( - this, - envelope.payload.notification.into_iter().collect(), - AddNotificationsOptions { - is_new: true, - clear_old: false, - includes_first: false, - }, - &mut cx, - ) - .await - } - - async fn handle_delete_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.splice_notifications([(envelope.payload.notification_id, None)], false, cx); - }); - Ok(()) - } - - async fn add_notifications( - this: Entity, - notifications: Vec, - options: AddNotificationsOptions, - cx: &mut AsyncApp, - ) -> Result<()> { - let mut user_ids = Vec::new(); - - let notifications = notifications - .into_iter() - .filter_map(|message| { - Some(NotificationEntry { - id: message.id, - is_read: message.is_read, - timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) - .ok()?, - notification: Notification::from_proto(&message)?, - response: message.response, - }) - }) - .collect::>(); - if notifications.is_empty() { - return Ok(()); - } - - for entry in ¬ifications { - match entry.notification { - Notification::ChannelInvitation { inviter_id, .. } => { - user_ids.push(inviter_id); - } - Notification::ContactRequest { - sender_id: requester_id, - } => { - user_ids.push(requester_id); - } - Notification::ContactRequestAccepted { - responder_id: contact_id, - } => { - user_ids.push(contact_id); - } - } - } - - let user_store = this.read_with(cx, |this, _| this.user_store.clone()); - - user_store - .update(cx, |store, cx| store.get_users(user_ids, cx)) - .await?; - this.update(cx, |this, cx| { - if options.clear_old { - cx.emit(NotificationEvent::NotificationsUpdated { - old_range: 0..this.notifications.summary().count, - new_count: 0, - }); - this.notifications = SumTree::default(); - this.loaded_all_notifications = false; - } - - if options.includes_first { - this.loaded_all_notifications = true; - } - - this.splice_notifications( - notifications - .into_iter() - .map(|notification| (notification.id, Some(notification))), - options.is_new, - cx, - ); - }); - - Ok(()) - } - - fn splice_notifications( - &mut self, - notifications: impl IntoIterator)>, - is_new: bool, - cx: &mut Context, - ) { - let mut cursor = self - .notifications - .cursor::>(()); - let mut new_notifications = SumTree::default(); - let mut old_range = 0..0; - - for (i, (id, new_notification)) in notifications.into_iter().enumerate() { - new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left), ()); - - if i == 0 { - old_range.start = cursor.start().1.0; - } - - let old_notification = cursor.item(); - if let Some(old_notification) = old_notification { - if old_notification.id == id { - cursor.next(); - - if let Some(new_notification) = &new_notification { - if new_notification.is_read { - cx.emit(NotificationEvent::NotificationRead { - entry: new_notification.clone(), - }); - } - } else { - cx.emit(NotificationEvent::NotificationRemoved { - entry: old_notification.clone(), - }); - } - } - } else if let Some(new_notification) = &new_notification - && is_new - { - cx.emit(NotificationEvent::NewNotification { - entry: new_notification.clone(), - }); - } - - if let Some(notification) = new_notification { - new_notifications.push(notification, ()); - } - } - - old_range.end = cursor.start().1.0; - let new_count = new_notifications.summary().count - old_range.start; - new_notifications.append(cursor.suffix(), ()); - drop(cursor); - - self.notifications = new_notifications; - cx.emit(NotificationEvent::NotificationsUpdated { - old_range, - new_count, - }); - } - - pub fn respond_to_notification( - &mut self, - notification: Notification, - response: bool, - cx: &mut Context, - ) { - match notification { - Notification::ContactRequest { sender_id } => { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(sender_id, response, cx) - }) - .detach(); - } - Notification::ChannelInvitation { channel_id, .. } => { - self.channel_store - .update(cx, |store, cx| { - store.respond_to_channel_invite(ChannelId(channel_id), response, cx) - }) - .detach(); - } - _ => {} - } - } -} - -impl EventEmitter for NotificationStore {} - -impl sum_tree::Item for NotificationEntry { - type Summary = NotificationSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - NotificationSummary { - max_id: self.id, - count: 1, - unread_count: if self.is_read { 0 } else { 1 }, - } - } -} - -impl sum_tree::ContextLessSummary for NotificationSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.max_id = self.max_id.max(summary.max_id); - self.count += summary.count; - self.unread_count += summary.unread_count; - } -} - -impl sum_tree::Dimension<'_, NotificationSummary> for NotificationId { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &NotificationSummary, _: ()) { - debug_assert!(summary.max_id > self.0); - self.0 = summary.max_id; - } -} - -impl sum_tree::Dimension<'_, NotificationSummary> for Count { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &NotificationSummary, _: ()) { - self.0 += summary.count; - } -} - -struct AddNotificationsOptions { - is_new: bool, - clear_old: bool, - includes_first: bool, -} diff --git a/crates/notifications/src/notifications.rs b/crates/notifications/src/notifications.rs index ee952555eb..3634788cad 100644 --- a/crates/notifications/src/notifications.rs +++ b/crates/notifications/src/notifications.rs @@ -1,4 +1 @@ -mod notification_store; - -pub use notification_store::*; pub mod status_toast; From 544df497f0e4f8c28e6b512f538a5db577f0d911 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 16:57:03 -0700 Subject: [PATCH 4/8] git_ui: drop co-authors-from-collab-room integration Step 4 of removing Zed's hosted collab feature from PaddleBoard. When committing inside a Zed Cloud collab room, the git panel suggested everyone in the room as a "Co-authored-by:" line on the commit message. Without the call/channel crates there is no room to read from, so collapse the entire feature. - potential_co_authors becomes a no-op returning Vec::default(). - Delete local_committer(&self, room: &call::Room, ...) which read the local participant from the room. - Delete pub fn load_local_committer and its commit_modal caller; it cached the local git committer for the room-suggestion path, which no longer exists. - Remove the local_committer and local_committer_task fields and their constructor inits. - In Render::render, drop the `let room = ...` binding and replace has_co_authors with `false`. The ToggleFillCoAuthors action is still defined but the action handler is gated on has_write_access && has_co_authors so it stays disabled. - Drop GitCommitter / get_git_committer imports (only used by the removed code path) and call from git_ui/Cargo.toml. cargo check -p git_ui is clean. The add_coauthors field, toggle_fill_co_authors action handler, render_co_authors helper, and fill_co_authors method are intentionally preserved: they're gated on potential_co_authors being non-empty, so they short- circuit safely. Keeping them minimizes the upstream-file diff. Release Notes: - Removed the git panel's "suggest co-authors from collab room" feature (Zed Cloud collab feature, never functional in PaddleBoard). Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 1 - crates/git_ui/Cargo.toml | 1 - crates/git_ui/src/commit_modal.rs | 1 - crates/git_ui/src/git_panel.rs | 85 ++----------------------------- 4 files changed, 5 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 744905049a..cb761f4e1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7689,7 +7689,6 @@ dependencies = [ "anyhow", "askpass", "buffer_diff", - "call", "collections", "component", "ctor", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 34dd60604e..6a23f10c41 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -21,7 +21,6 @@ alacritty_terminal.workspace = true anyhow.workspace = true askpass.workspace = true buffer_diff.workspace = true -call.workspace = true collections.workspace = true component.workspace = true db.workspace = true diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 8da934a274..eeaf8295de 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -152,7 +152,6 @@ impl CommitModal { } } git_panel.set_modal_open(true, cx); - git_panel.load_local_committer(cx); }); let dock = workspace.dock_at_position(git_panel.position(window, cx)); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 5c656cf18d..26684a18c7 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -27,8 +27,8 @@ use git::Oid; use git::commit::ParsedCommitMessage; use git::repository::{ Branch, CommitData, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, - GitCommitTemplate, GitCommitter, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput, - ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer, + GitCommitTemplate, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput, + ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, }; use git::stash::GitStash; use git::status::{DiffStat, StageStatus}; @@ -689,8 +689,6 @@ pub struct GitPanel { context_menu: Option<(Entity, Point, Subscription)>, modal_open: bool, show_placeholders: bool, - local_committer: Option, - local_committer_task: Option>, commit_template: Option, bulk_staging: Option, stash_entries: GitStash, @@ -882,8 +880,6 @@ impl GitPanel { tracked_staged_count: 0, update_visible_entries_task: Task::ready(()), show_placeholders: false, - local_committer: None, - local_committer_task: None, commit_template: None, context_menu: None, workspace: workspace.weak_handle(), @@ -3348,70 +3344,8 @@ impl GitPanel { } } - pub fn load_local_committer(&mut self, cx: &Context) { - if self.local_committer_task.is_none() { - self.local_committer_task = Some(cx.spawn(async move |this, cx| { - let committer = get_git_committer(cx).await; - this.update(cx, |this, cx| { - this.local_committer = Some(committer); - cx.notify() - }) - .ok(); - })); - } - } - - fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> { - let mut new_co_authors = Vec::new(); - let project = self.project.read(cx); - - let Some(room) = - call::ActiveCall::try_global(cx).and_then(|call| call.read(cx).room().cloned()) - else { - return Vec::default(); - }; - - let room = room.read(cx); - - for (peer_id, collaborator) in project.collaborators() { - if collaborator.is_host { - continue; - } - - let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else { - continue; - }; - if !participant.can_write() { - continue; - } - if let Some(email) = &collaborator.committer_email { - let name = collaborator - .committer_name - .clone() - .or_else(|| participant.user.name.clone()) - .unwrap_or_else(|| participant.user.github_login.clone().to_string()); - new_co_authors.push((name.clone(), email.clone())) - } - } - if !project.is_local() - && !project.is_read_only(cx) - && let Some(local_committer) = self.local_committer(room, cx) - { - new_co_authors.push(local_committer); - } - new_co_authors - } - - fn local_committer(&self, room: &call::Room, cx: &App) -> Option<(String, String)> { - let user = room.local_participant_user(cx)?; - let committer = self.local_committer.as_ref()?; - let email = committer.email.clone()?; - let name = committer - .name - .clone() - .or_else(|| user.name.clone()) - .unwrap_or_else(|| user.github_login.clone().to_string()); - Some((name, email)) + fn potential_co_authors(&self, _cx: &App) -> Vec<(String, String)> { + Vec::default() } fn toggle_fill_co_authors( @@ -6548,19 +6482,10 @@ impl Render for GitPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let project = self.project.read(cx); let has_entries = !self.entries.is_empty(); - let room = self.workspace.upgrade().and_then(|_workspace| { - call::ActiveCall::try_global(cx).and_then(|call| call.read(cx).room().cloned()) - }); let has_write_access = self.has_write_access(cx); - let has_co_authors = room.is_some_and(|room| { - self.load_local_committer(cx); - let room = room.read(cx); - room.remote_participants() - .values() - .any(|remote_participant| remote_participant.can_write()) - }); + let has_co_authors = false; v_flex() .id("git_panel") From ef44a0298c79565ad5d9786cfe10911861dfd598 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Sat, 23 May 2026 09:12:34 -0700 Subject: [PATCH 5/8] paddleboard: unwire collab_ui from the binary Drops the collab_ui crate as a paddleboard dependency and removes every call site that pulled CollabPanel / ChannelView / collab_ui::init into the running app: main.rs init block + zed-link URL handling + zed.rs panel registration and ToggleFocus action + View > Collab Panel menu item + the test bootstrap. Also drops two notifications::init(...) calls that were left behind when the Zed Cloud NotificationStore was deleted. The collab_ui crate stays in the workspace tree (still has compile errors from prior surface rips); it just is not compiled into the paddleboard binary anymore. This makes the workspace.rs AutoWatch / screen-share / leader-routing surface truly dead, which a follow-up commit can then strip without the collab_ui consumer ghost-pinning it. Also drops a stale call::RemoteParticipant doc comment from the workspace shim that it abstracts away. --- Cargo.lock | 1 - crates/paddleboard/Cargo.toml | 1 - crates/paddleboard/src/main.rs | 58 +-------------------- crates/paddleboard/src/zed.rs | 13 ----- crates/paddleboard/src/zed/app_menus.rs | 2 - crates/paddleboard/src/zed/open_listener.rs | 17 ------ crates/workspace/src/workspace.rs | 1 - 7 files changed, 2 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb761f4e1f..142646b223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12781,7 +12781,6 @@ dependencies = [ "client", "clock", "codestral", - "collab_ui", "collections", "command_palette", "component", diff --git a/crates/paddleboard/Cargo.toml b/crates/paddleboard/Cargo.toml index e4fac8708e..2c71458d30 100644 --- a/crates/paddleboard/Cargo.toml +++ b/crates/paddleboard/Cargo.toml @@ -85,7 +85,6 @@ clap.workspace = true cli.workspace = true client.workspace = true codestral.workspace = true -collab_ui.workspace = true collections.workspace = true command_palette.workspace = true component.workspace = true diff --git a/crates/paddleboard/src/main.rs b/crates/paddleboard/src/main.rs index c0187962df..af5bae976f 100644 --- a/crates/paddleboard/src/main.rs +++ b/crates/paddleboard/src/main.rs @@ -21,7 +21,6 @@ use anyhow::{Context as _, Error, Result}; use clap::Parser; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, RefreshLlmTokenListener, UserStore, parse_zed_link}; -use collab_ui::channel_view::ChannelView; use collections::HashMap; use crashes::InitCrashHandler; use db::kvp::{GlobalKeyValueStore, KeyValueStore}; @@ -68,7 +67,7 @@ use std::{ }; use theme::{ActiveTheme, GlobalTheme, ThemeRegistry}; use theme_settings::load_user_theme; -use util::{ResultExt, TryFutureExt, maybe}; +use util::ResultExt; use uuid::Uuid; use workspace::{ AppState, MultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace, Toast, @@ -779,8 +778,6 @@ fn main() { settings_profile_selector::init(cx); language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); - collab_ui::init(&app_state, cx); git_ui::init(cx); git_graph::init(cx); feedback::init(cx); @@ -1432,58 +1429,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut })); } - if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { - cx.spawn(async move |cx| { - let result = maybe!(async { - if let Some(task) = task { - task.await?; - } - let client = app_state.client.clone(); - // we continue even if authentication fails as join_channel/ open channel notes will - // show a visible error message. - authenticate(client, cx).await.log_err(); - - if let Some(channel_id) = request.join_channel { - cx.update(|cx| { - workspace::join_channel( - client::ChannelId(channel_id), - app_state.clone(), - None, - None, - cx, - ) - }) - .await?; - } - - let workspace_window = - workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?; - - let workspace = workspace_window.read_with(cx, |mw, _| mw.workspace().clone())?; - - let mut promises = Vec::new(); - for (channel_id, heading) in request.open_channel_notes { - promises.push(cx.update_window(workspace_window.into(), |_, window, cx| { - ChannelView::open( - client::ChannelId(channel_id), - heading, - workspace.clone(), - window, - cx, - ) - .log_err() - })?) - } - future::join_all(promises).await; - anyhow::Ok(()) - }) - .await; - if let Err(err) = result { - fail_to_open_window_async(err, cx); - } - }) - .detach() - } else if let Some(task) = task { + if let Some(task) = task { cx.spawn(async move |cx| { if let Err(err) = task.await { fail_to_open_window_async(err, cx); diff --git a/crates/paddleboard/src/zed.rs b/crates/paddleboard/src/zed.rs index e6b1edd7f9..7ddf900901 100644 --- a/crates/paddleboard/src/zed.rs +++ b/crates/paddleboard/src/zed.rs @@ -732,8 +732,6 @@ fn initialize_panels(window: &mut Window, cx: &mut Context) -> Task) -> Task(window, cx); }, ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::collab_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) .register_action( |workspace: &mut Workspace, _: &terminal_panel::ToggleFocus, @@ -5506,12 +5495,10 @@ mod tests { audio::init(cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); release_channel::init(Version::new(0, 0, 0), cx); command_palette::init(cx); editor::init(cx); - collab_ui::init(&app_state, cx); git_ui::init(cx); project_panel::init(cx); outline_panel::init(cx); diff --git a/crates/paddleboard/src/zed/app_menus.rs b/crates/paddleboard/src/zed/app_menus.rs index 695b090823..ef87950010 100644 --- a/crates/paddleboard/src/zed/app_menus.rs +++ b/crates/paddleboard/src/zed/app_menus.rs @@ -1,4 +1,3 @@ -use collab_ui::collab_panel; use gpui::{App, Menu, MenuItem, OsAction}; use release_channel::ReleaseChannel; use terminal_view::terminal_panel; @@ -42,7 +41,6 @@ pub fn app_menus(cx: &mut App) -> Vec { MenuItem::separator(), MenuItem::action("Project Panel", paddleboard_actions::project_panel::ToggleFocus), MenuItem::action("Outline Panel", outline_panel::ToggleFocus), - MenuItem::action("Collab Panel", collab_panel::ToggleFocus), MenuItem::action("Terminal Panel", terminal_panel::ToggleFocus), MenuItem::action("Debugger Panel", debug_panel::ToggleFocus), MenuItem::separator(), diff --git a/crates/paddleboard/src/zed/open_listener.rs b/crates/paddleboard/src/zed/open_listener.rs index 419bbf412b..ffae0c758d 100644 --- a/crates/paddleboard/src/zed/open_listener.rs +++ b/crates/paddleboard/src/zed/open_listener.rs @@ -4,7 +4,6 @@ use agent_ui::ExternalSourcePrompt; use anyhow::{Context as _, Result, anyhow}; use cli::{CliRequest, CliResponse, CliResponseSink}; use cli::{IpcHandshake, ipc}; -use client::{ZedLink, parse_zed_link}; use db::kvp::KeyValueStore; use fs::Fs; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -38,8 +37,6 @@ pub struct OpenRequest { pub diff_paths: Vec<[String; 2]>, pub diff_all: bool, pub dev_container: bool, - pub open_channel_notes: Vec<(u64, Option)>, - pub join_channel: Option, pub remote_connection: Option, } @@ -124,8 +121,6 @@ impl OpenRequest { && self.open_paths.is_empty() && self.diff_paths.is_empty() && self.remote_connection.is_none() - && self.join_channel.is_none() - && self.open_channel_notes.is_empty() } pub fn parse(request: RawOpenRequest, cx: &App) -> Result { @@ -198,18 +193,6 @@ impl OpenRequest { this.parse_git_commit_url(commit_path)? } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? - } else if let Some(zed_link) = parse_zed_link(&url, cx) { - match zed_link { - ZedLink::Channel { channel_id } => { - this.join_channel = Some(channel_id); - } - ZedLink::ChannelNotes { - channel_id, - heading, - } => { - this.open_channel_notes.push((channel_id, heading)); - } - } } else { log::error!("unhandled url: {}", url); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e7c62b7763..f548da26c8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -8147,7 +8147,6 @@ impl ParticipantLocation { } } /// Workspace-local view of a remote collaborator's state. -/// This is the subset of `call::RemoteParticipant` that workspace needs. #[derive(Clone)] pub struct RemoteCollaborator { pub user: Arc, From fa896dbd7bb6c6c077e4d87ee8eaac82bf3d618c Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Sat, 23 May 2026 09:13:53 -0700 Subject: [PATCH 6/8] RECAPS: unwire collab_ui session Co-Authored-By: Claude Opus 4.7 --- RECAPS.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/RECAPS.md b/RECAPS.md index 677e43756e..6001773660 100644 --- a/RECAPS.md +++ b/RECAPS.md @@ -4,6 +4,33 @@ Running log of completed work sessions, newest first. Each entry summarizes a co --- +## 2026-05-23 + +### Unwire collab_ui from the paddleboard binary +- Continuation of the `rip-collab-livekit` branch. Previous commits stripped per-surface consumers (`title_bar`, `file_finder`, `notifications`, `git_ui`); this session targeted the next layer: the `collab_ui` crate itself. Resumed from a session that had been cut off mid-flight — only artifact in the worktree was a one-line stale doc-comment removal in `workspace.rs:8150` referring to `call::RemoteParticipant`. +- **Initial plan was wrong, course-corrected.** First attempt was "surgical inside workspace.rs only" — strip `AutoWatch` / `open_shared_screen` / `shared_screen_for_peer` / `ScreenShare` action / `LocalScreenShare*` & `RemoteVideoTracksChanged` event arms + the matching `AnyActiveCall` trait methods. Audit revealed those all still have live external consumers in `collab_ui::collab_panel` (3 sites: `auto_watch_state()`, `toggle_auto_watch`, `open_shared_screen`) plus the `collab/tests/integration/auto_watch_tests.rs` and `following_tests.rs` test files. And `collab_ui::init` was still being called at boot from `paddleboard/src/main.rs:783` and `crates/paddleboard/src/zed.rs:5514`, with `CollabPanel::load` registering the panel in `paddleboard/src/zed.rs:736`. So `workspace.rs` looked dead but actually had compile-time consumers ghost-pinning it. Re-scoped to unwire `collab_ui` first; the workspace surgery becomes truly self-contained in a follow-up. +- **What landed (commit `ef44a0298c`):** + - Dropped `collab_ui.workspace = true` from `crates/paddleboard/Cargo.toml`. + - `main.rs`: removed `use collab_ui::channel_view::ChannelView;`, the `collab_ui::init(&app_state, cx);` boot line, and the entire ~60-line `if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { ... }` block that handled zed:// channel URLs via `workspace::join_channel` and `ChannelView::open`. Simplified the trailing `else if let Some(task) = task` to `if let Some(task) = task`. + - `zed.rs`: removed `collab_ui::collab_panel::CollabPanel::load(...)` from `initialize_panels` (and its `channels_panel` slot in the `futures::join!`), the `ToggleFocus` `register_action` block at line 1199, and the test-bootstrap `collab_ui::init(&app_state, cx);` at line 5514. + - `zed/app_menus.rs`: dropped `use collab_ui::collab_panel;` and the `MenuItem::action("Collab Panel", collab_panel::ToggleFocus)` entry in View. + - `zed/open_listener.rs`: dropped the `ZedLink` / `parse_zed_link` import, the `open_channel_notes` and `join_channel` fields on `OpenRequest` (and their checks in `is_focus_app_only`), and the whole `else if let Some(zed_link) = parse_zed_link(&url, cx)` URL arm. zed:// channel URLs now fall through to the existing "unhandled url" log. + - Cleaned two `notifications::init(...)` calls in `main.rs:782` and `zed.rs:5498` that were already broken (function was deleted in commit `9099e043e4`) — apparently nothing built end-to-end since that commit landed, or the failures were ignored. + - Bonus cleanup: removed `maybe` from the `use util::{ResultExt, TryFutureExt, maybe};` import (no longer used) and then `TryFutureExt` (also unused after the channel block went). + - Kept the stale doc-comment removal in `workspace.rs:8150` rolled into the same commit since it points at the same surface being torn out. +- **Intentionally preserved:** + - The `collab_ui` crate itself stays in the workspace tree. It already had pre-existing compile errors (unresolved `notifications::Notification*` imports from the prior NotificationStore deletion, missing `title_bar::collab` after the title-bar strip, and four `u64` deref errors); none of those are my problem to fix here. Crate-level deletion is the natural next step after the workspace.rs surgery, not before. + - Workspace's `AnyActiveCall` trait, `GlobalAnyActiveCall`, `ActiveCallEvent` enum, `RemoteCollaborator` struct, the entire `follower_states` + leader-following protocol, `WorkspaceStore`'s `handle_follow` / `handle_update_followers` request handlers, the `join_channel` / `join_channel_internal` / `get_any_active_multi_workspace` flow, `prepare_to_close`'s "leave the call" prompt, and the namespace allowlist entries for `channel_modal` / `collab_panel` / `collab` in `zed.rs` keybind validation. All of these still have either intra-`workspace.rs` consumers or live `call` / `channel` crate consumers (`call::init` is still wired at boot at `main.rs:781` and `zed.rs:5497`). Leaving them costs nothing and gives the next commit a coherent rip target. + - `parse_zed_link` import in `main.rs` — still used at line 2005 by `parse_url_arg` to test which CLI args are URL-shaped. +- **Verified:** `cargo check -p paddleboard` clean (only inactive-code diagnostics from cross-platform `#[cfg]` blocks, no errors). `./script/clippy -p paddleboard` (release, all targets, deny warnings) clean — no warnings, `cargo-machete` happy. Did not run the full workspace check (`cargo check --workspace` would still fail on `collab_ui` and `collab` test files, which are now expected and unblocking). +- **Open follow-ups:** + - **workspace.rs surgical rip is now possible.** With `collab_ui` un-linked, `AutoWatch` / `auto_watch_state` / `toggle_auto_watch` / `open_shared_screen` / `shared_screen_for_peer` / `next_watched_peer` / `handle_auto_watch_*` / `ScreenShare` action / `LocalScreenShare*` & `RemoteVideoTracksChanged` event variants + the 3 trait methods only they used (`create_shared_screen`, `peer_ids_with_video_tracks`, `is_sharing_screen`) can come out in one commit. The matching `impl AnyActiveCall for ActiveCallEntity` methods in `crates/call/src/call_impl/mod.rs:67+` need to be trimmed in lockstep to keep that crate building. + - **collab_ui crate deletion.** The crate is unreferenced; ripping the directory + its workspace member entry is a tight follow-up after the workspace.rs cut. + - **collab / livekit / call crate deletion.** Bigger swing — `call::init` is still wired and `WorkspaceStore`'s `update_followers` calls `GlobalAnyActiveCall::try_global`. Order: rip workspace's follower protocol → rip `call` crate → rip `livekit` deps → rip `collab` tests. + - **Namespace allowlist tuning.** `zed.rs:5259/5263` keybind validation still expects `channel_modal` and `collab_panel` namespaces. Removing them when those actions stop being declared is a one-line touch but tied to the action-definition deletions in `workspace.rs`. + +--- + ## 2026-05-22 ### Auto-update: stop pointing remaining zed.dev surfaces at Zed From 7835e92d5b69a9b1aa362a93c0260cfd23952ea5 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Sun, 24 May 2026 08:39:52 -0700 Subject: [PATCH 7/8] workspace: rip AutoWatch, ScreenShare, and video-track plumbing Remove the screen-sharing / auto-watch feature surface from workspace, call, and feature_flags: - workspace.rs: delete AutoWatch enum, open_shared_screen, toggle_auto_watch, shared_screen_for_peer, and the three handle_auto_watch_* methods. Remove is_sharing_screen, create_shared_screen, and peer_ids_with_video_tracks from the AnyActiveCall trait. Drop RemoteVideoTracksChanged, LocalScreenShareStarted, LocalScreenShareStopped from ActiveCallEvent. Remove ScreenShare action. - call_impl/mod.rs: remove the matching trait impl methods and event-mapping arms. Clean up now-unused imports. - feature_flags: delete AutoWatchFeatureFlag (only consumer was the already-unlinked collab_ui). Co-Authored-By: Claude Opus 4.7 --- crates/call/src/call_impl/mod.rs | 117 +--------------------- crates/feature_flags/src/flags.rs | 8 -- crates/workspace/src/workspace.rs | 160 +----------------------------- 3 files changed, 6 insertions(+), 279 deletions(-) diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 04f47fcd3a..732651661a 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -8,8 +8,8 @@ use client::{ChannelId, Client, TypedEnvelope, User, UserStore, PADDLEBOARD_ALWA use collections::HashSet; use futures::{Future, FutureExt, channel::oneshot, future::Shared}; use gpui::{ - AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, - TaskExt, WeakEntity, Window, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, TaskExt, + WeakEntity, Window, }; use postage::watch; use project::Project; @@ -17,8 +17,8 @@ use room::Event; use settings::Settings; use std::sync::Arc; use workspace::{ - ActiveCallEvent, AnyActiveCall, GlobalAnyActiveCall, MultiWorkspace, MultiWorkspaceEvent, Pane, - RemoteCollaborator, SharedScreen, Workspace, + ActiveCallEvent, AnyActiveCall, GlobalAnyActiveCall, MultiWorkspace, MultiWorkspaceEvent, + RemoteCollaborator, Workspace, }; pub use livekit_client::{RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent}; @@ -112,13 +112,6 @@ impl AnyActiveCall for ActiveCallEntity { .map_or(false, |room| room.read(cx).is_sharing_project()) } - fn is_sharing_screen(&self, cx: &App) -> bool { - self.0 - .read(cx) - .room() - .map_or(false, |room| room.read(cx).is_sharing_screen()) - } - fn has_remote_participants(&self, cx: &App) -> bool { self.0.read(cx).room().map_or(false, |room| { !room.read(cx).remote_participants().is_empty() @@ -211,17 +204,6 @@ impl AnyActiveCall for ActiveCallEntity { participant_id: *participant_id, }) } - room::Event::RemoteVideoTracksChanged { participant_id } => { - Some(ActiveCallEvent::RemoteVideoTracksChanged { - participant_id: *participant_id, - }) - } - room::Event::LocalScreenShareStarted => { - Some(ActiveCallEvent::LocalScreenShareStarted) - } - room::Event::LocalScreenShareStopped => { - Some(ActiveCallEvent::LocalScreenShareStopped) - } _ => None, }; if let Some(event) = mapped { @@ -231,97 +213,6 @@ impl AnyActiveCall for ActiveCallEntity { ) } - fn create_shared_screen( - &self, - peer_id: client::proto::PeerId, - pane: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Option> { - let room = self.0.read(cx).room()?.clone(); - let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?; - let track = participant.video_tracks.values().next()?.clone(); - let user = participant.user.clone(); - - for item in pane.read(cx).items_of_type::() { - if item.read(cx).peer_id == peer_id { - return Some(item); - } - } - - Some(cx.new(|cx: &mut Context| { - let my_sid = track.sid(); - cx.subscribe( - &room, - move |_: &mut SharedScreen, - _: Entity, - ev: &room::Event, - cx: &mut Context| { - if let room::Event::RemoteVideoTrackUnsubscribed { sid } = ev - && *sid == my_sid - { - cx.emit(workspace::shared_screen::Event::Close); - } - }, - ) - .detach(); - - cx.observe_release( - &room, - |_: &mut SharedScreen, _: &mut Room, cx: &mut Context| { - cx.emit(workspace::shared_screen::Event::Close); - }, - ) - .detach(); - - let view = cx.new(|cx| RemoteVideoTrackView::new(track.clone(), window, cx)); - cx.subscribe( - &view, - |_: &mut SharedScreen, - _: Entity, - ev: &RemoteVideoTrackViewEvent, - cx: &mut Context| match ev { - RemoteVideoTrackViewEvent::Close => { - cx.emit(workspace::shared_screen::Event::Close); - } - }, - ) - .detach(); - - pub(super) fn clone_remote_video_track_view( - view: &AnyView, - window: &mut Window, - cx: &mut App, - ) -> AnyView { - let view = view - .clone() - .downcast::() - .expect("SharedScreen view must be a RemoteVideoTrackView"); - let cloned = view.update(cx, |view, cx| view.clone(window, cx)); - AnyView::from(cloned) - } - - SharedScreen::new( - peer_id, - user, - AnyView::from(view), - clone_remote_video_track_view, - cx, - ) - })) - } - - fn peer_ids_with_video_tracks(&self, cx: &App) -> Vec { - let Some(room) = self.0.read(cx).room() else { - return Vec::new(); - }; - room.read(cx) - .remote_participants() - .values() - .filter(|p| p.has_video_tracks()) - .map(|p| p.peer_id) - .collect() - } } pub struct OneAtATime { diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index bfae995f9f..cd09845372 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -116,14 +116,6 @@ impl FeatureFlag for AgentThreadWorktreeLabelFlag { } register_feature_flag!(AgentThreadWorktreeLabelFlag); -pub struct AutoWatchFeatureFlag; - -impl FeatureFlag for AutoWatchFeatureFlag { - const NAME: &'static str = "auto-watch-screens"; - type Value = PresenceFlag; -} -register_feature_flag!(AutoWatchFeatureFlag); - pub struct SkillsFeatureFlag; impl FeatureFlag for SkillsFeatureFlag { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f548da26c8..dd87e130f0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1396,7 +1396,7 @@ pub struct Workspace { project: Entity, follower_states: HashMap, last_leaders_by_pane: HashMap, CollaboratorId>, - auto_watch: AutoWatch, + window_edited: bool, last_window_title: Option, dirty_items: HashMap, @@ -1449,18 +1449,6 @@ pub struct FollowerState { items_by_leader_view_id: HashMap, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AutoWatch { - Off, - Active { watched_peer: Option }, - Paused, -} - -impl AutoWatch { - pub fn enabled(&self) -> bool { - matches!(self, AutoWatch::Active { .. } | AutoWatch::Paused) - } -} struct FollowerView { view: Box, @@ -1842,7 +1830,7 @@ impl Workspace { project: project.clone(), follower_states: Default::default(), last_leaders_by_pane: Default::default(), - auto_watch: AutoWatch::Off, + dispatching_keystrokes: Default::default(), window_edited: false, last_window_title: None, @@ -4854,108 +4842,6 @@ impl Workspace { item } - pub fn open_shared_screen( - &mut self, - peer_id: PeerId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(shared_screen) = - self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx) - { - self.active_pane.update(cx, |pane, cx| { - pane.add_item(Box::new(shared_screen), false, true, None, window, cx) - }); - } - } - - pub fn auto_watch_state(&self) -> &AutoWatch { - &self.auto_watch - } - - fn next_watched_peer(&self, cx: &App) -> Option { - self.active_call() - .and_then(|call| call.peer_ids_with_video_tracks(cx).first().copied()) - } - - pub fn toggle_auto_watch(&mut self, window: &mut Window, cx: &mut Context) { - if self.auto_watch.enabled() { - self.auto_watch = AutoWatch::Off; - cx.notify(); - return; - } - - let active_pane = self.active_pane.clone(); - self.unfollow_in_pane(&active_pane, window, cx); - - let local_is_sharing = self - .active_call() - .map_or(false, |call| call.is_sharing_screen(cx)); - - if local_is_sharing { - self.auto_watch = AutoWatch::Paused; - } else { - let watched_peer = self.next_watched_peer(cx); - self.auto_watch = AutoWatch::Active { watched_peer }; - - if let Some(peer_id) = watched_peer { - self.open_shared_screen(peer_id, window, cx); - } - } - - cx.notify(); - } - - fn handle_auto_watch_video_tracks_changed( - &mut self, - peer_id: PeerId, - window: &mut Window, - cx: &mut Context, - ) { - let AutoWatch::Active { watched_peer } = self.auto_watch else { - return; - }; - - let peer_is_sharing = self.active_call().map_or(false, |call| { - call.peer_ids_with_video_tracks(cx).contains(&peer_id) - }); - let should_watch_peer = peer_is_sharing && watched_peer.is_none(); - let watched_peer_stopped_sharing = watched_peer == Some(peer_id) && !peer_is_sharing; - - if should_watch_peer || watched_peer_stopped_sharing { - let next_watched_peer = if should_watch_peer { - Some(peer_id) - } else { - self.next_watched_peer(cx) - }; - - self.auto_watch = AutoWatch::Active { - watched_peer: next_watched_peer, - }; - - if let Some(next_watched_peer) = next_watched_peer { - self.open_shared_screen(next_watched_peer, window, cx); - } - } - } - - fn handle_auto_watch_local_share_stopped( - &mut self, - window: &mut Window, - cx: &mut Context, - ) { - let AutoWatch::Paused = self.auto_watch else { - return; - }; - - let watched_peer = self.next_watched_peer(cx); - self.auto_watch = AutoWatch::Active { watched_peer }; - - if let Some(peer_id) = watched_peer { - self.open_shared_screen(peer_id, window, cx); - } - } - pub fn activate_item( &mut self, item: &dyn ItemHandle, @@ -5786,7 +5672,6 @@ impl Workspace { .insert(pane.downgrade(), leader_id); self.unfollow(leader_id, window, cx); self.unfollow_in_pane(&pane, window, cx); - self.auto_watch = AutoWatch::Off; self.follower_states.insert( leader_id, FollowerState { @@ -6629,25 +6514,10 @@ impl Workspace { { item_to_activate = Some((item.location, item.view.boxed_clone())); } - } else if let Some(shared_screen) = - self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx) - { - item_to_activate = Some((None, Box::new(shared_screen))); } item_to_activate } - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Option> { - self.active_call()? - .create_shared_screen(peer_id, pane, window, cx) - } - pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { if window.is_window_active() { self.update_active_view_for_followers(window, cx); @@ -6697,19 +6567,6 @@ impl Workspace { ActiveCallEvent::ParticipantLocationChanged { participant_id } => { self.leader_updated(participant_id, window, cx); } - ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => { - self.leader_updated(participant_id, window, cx); - self.handle_auto_watch_video_tracks_changed(*participant_id, window, cx); - } - ActiveCallEvent::LocalScreenShareStarted => { - if let AutoWatch::Active { .. } = self.auto_watch { - self.auto_watch = AutoWatch::Paused; - cx.notify(); - } - } - ActiveCallEvent::LocalScreenShareStopped => { - self.handle_auto_watch_local_share_stopped(window, cx); - } } } @@ -8075,7 +7932,6 @@ pub trait AnyActiveCall { fn unshare_project(&self, _: Entity, _: &mut App) -> Result<()>; fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option; fn is_sharing_project(&self, _: &App) -> bool; - fn is_sharing_screen(&self, _: &App) -> bool; fn has_remote_participants(&self, _: &App) -> bool; fn local_participant_is_guest(&self, _: &App) -> bool; fn client(&self, _: &App) -> Arc; @@ -8098,14 +7954,6 @@ pub trait AnyActiveCall { _: &mut Context, _: Box)>, ) -> Subscription; - fn create_shared_screen( - &self, - _: PeerId, - _: &Entity, - _: &mut Window, - _: &mut App, - ) -> Option>; - fn peer_ids_with_video_tracks(&self, _: &App) -> Vec; } #[derive(Clone)] @@ -8157,9 +8005,6 @@ pub struct RemoteCollaborator { pub enum ActiveCallEvent { ParticipantLocationChanged { participant_id: PeerId }, - RemoteVideoTracksChanged { participant_id: PeerId }, - LocalScreenShareStarted, - LocalScreenShareStopped, } fn leader_border_for_pane( @@ -9212,7 +9057,6 @@ actions!( /// Shares the current project with collaborators. ShareProject, /// Shares your screen with collaborators. - ScreenShare, /// Copies the current room name and session id for debugging purposes. CopyRoomId, ] From fd62b60274dde19b7e22d72eab2065be6367b274 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Sun, 24 May 2026 08:40:45 -0700 Subject: [PATCH 8/8] RECAPS: workspace surgical rip session Co-Authored-By: Claude Opus 4.7 --- RECAPS.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/RECAPS.md b/RECAPS.md index 6001773660..82e8acd109 100644 --- a/RECAPS.md +++ b/RECAPS.md @@ -4,6 +4,31 @@ Running log of completed work sessions, newest first. Each entry summarizes a co --- +## 2026-05-24 + +### PR #41 + workspace surgical rip of AutoWatch / ScreenShare / video-track plumbing +- Opened PR #41 (`rip-collab-livekit`) covering the 6 commits from the 2026-05-23 session that unwired `collab_ui`, `notifications`, `title_bar`, `file_finder`, and `git_ui` from the paddleboard binary. Net -1,549 lines across 20 files. +- **Workspace surgical rip (commit `7835e92d5b`):** With `collab_ui` unlinked from the binary, the workspace.rs screen-sharing surface was now self-contained. Removed: + - `AutoWatch` enum + impl, `auto_watch` field on `Workspace`, all auto-watch methods (`toggle_auto_watch`, `handle_auto_watch_video_tracks_changed`, `handle_auto_watch_local_share_stopped`, `next_watched_peer`, `auto_watch_state`). + - `open_shared_screen`, `shared_screen_for_peer` methods. + - `ScreenShare` action from the `collab` namespace actions macro. + - `is_sharing_screen`, `create_shared_screen`, `peer_ids_with_video_tracks` from the `AnyActiveCall` trait definition. + - `RemoteVideoTracksChanged`, `LocalScreenShareStarted`, `LocalScreenShareStopped` variants from `ActiveCallEvent`. + - Matching trait impl methods and event-mapping arms in `crates/call/src/call_impl/mod.rs` (~110 LOC including the `create_shared_screen` impl with its livekit `RemoteVideoTrackView` construction). + - `AutoWatchFeatureFlag` from `crates/feature_flags/src/flags.rs` (only consumer was the already-unlinked `collab_ui`). + - Cleaned unused imports (`AnyView`, `Pane`, `SharedScreen`) from call_impl. +- **Intentionally preserved:** `shared_screen.rs` module and its `pub use` re-export in workspace.rs (harmless, and removing it is a separate concern). The `follower_states` / leader-following protocol, `WorkspaceStore`'s follow/update_followers handlers, `call::init` wiring, and the `collab`/`livekit`/`call` crates themselves — all still have intra-workspace or cross-crate consumers. The `pub use livekit_client::{RemoteVideoTrack, ...}` re-exports in call_impl stayed (no external consumers, but they're re-exports from a crate we haven't ripped yet). +- **Verified:** `cargo check -p paddleboard -p call -p workspace -p feature_flags` clean. `./script/clippy -p paddleboard -p call -p workspace -p feature_flags` (release, all targets, deny warnings) clean. Net -279 lines across 3 files. +- **Open follow-ups:** + - **Follower protocol rip.** `follower_states`, `last_leaders_by_pane`, `leader_updates_tx`, `_apply_leader_updates`, the entire leader/follower negotiation (`start_following`, `follow_next_collaborator`, `unfollow`, `leader_updated`, `update_followers`, etc.), `WorkspaceStore`'s `handle_follow`/`handle_update_followers` request handlers. This is the heaviest remaining workspace.rs surgery (~500+ LOC). + - **`call::init` unwiring.** Still wired at boot in `main.rs` and `zed.rs`. Prerequisite: rip the follower protocol first (it calls `GlobalAnyActiveCall::try_global`). + - **`collab_ui` crate deletion.** Directory + workspace member entry — trivial once the above are done. + - **`collab` / `livekit_*` / `call` crate deletion.** Biggest swing, deferred until the protocol is fully excised from workspace. + - **`shared_screen.rs` module removal.** Can be done any time after `create_shared_screen` is gone from call_impl (already done this session) — just needs the `pub mod` + `pub use` lines dropped from workspace.rs. + - **Namespace allowlist tuning.** `zed.rs` keybind validation still expects `collab_panel` / `channel_modal` / `collab` namespaces — tied to action-definition deletions. + +--- + ## 2026-05-23 ### Unwire collab_ui from the paddleboard binary