Add a setting to prevent sharing projects in public channels (#41395)

Marshall Bowers created

This PR adds a setting to prevent projects from being shared in public
channels.

This can be enabled by adding the following to the project settings
(`.zed/settings.json`):

```json
{
  "prevent_sharing_in_public_channels": true
}
```

This will then disable the "Share" button when not in a private channel:

<img width="380" height="115" alt="Screenshot 2025-10-28 at 2 28 10 PM"
src="https://github.com/user-attachments/assets/6761ac34-c0d5-4451-a443-adf7a1c42bcd"
/>

Release Notes:

- collaboration: Added a `prevent_sharing_in_public_channels` project
setting for preventing projects from being shared in public channels.

Change summary

Cargo.lock                                      |  1 
crates/settings/src/settings_content/project.rs |  6 ++++
crates/settings/src/vscode_import.rs            |  1 
crates/title_bar/Cargo.toml                     |  1 
crates/title_bar/src/collab.rs                  | 28 +++++++++++++++++++
crates/worktree/src/worktree_settings.rs        |  3 ++
6 files changed, 40 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -17439,6 +17439,7 @@ dependencies = [
  "anyhow",
  "auto_update",
  "call",
+ "channel",
  "chrono",
  "client",
  "cloud_llm_client",

crates/settings/src/settings_content/project.rs 🔗

@@ -64,6 +64,12 @@ pub struct WorktreeSettingsContent {
     #[serde(skip_serializing_if = "Maybe::is_unset")]
     pub project_name: Maybe<String>,
 
+    /// Whether to prevent this project from being shared in public channels.
+    ///
+    /// Default: false
+    #[serde(default)]
+    pub prevent_sharing_in_public_channels: bool,
+
     /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
     /// `file_scan_inclusions`.
     ///

crates/settings/src/vscode_import.rs 🔗

@@ -855,6 +855,7 @@ impl VsCodeSettings {
     fn worktree_settings_content(&self) -> WorktreeSettingsContent {
         WorktreeSettingsContent {
             project_name: crate::Maybe::Unset,
+            prevent_sharing_in_public_channels: false,
             file_scan_exclusions: self
                 .read_value("files.watcherExclude")
                 .and_then(|v| v.as_array())

crates/title_bar/Cargo.toml 🔗

@@ -30,6 +30,7 @@ test-support = [
 anyhow.workspace = true
 auto_update.workspace = true
 call.workspace = true
+channel.workspace = true
 chrono.workspace = true
 client.workspace = true
 cloud_llm_client.workspace = true

crates/title_bar/src/collab.rs 🔗

@@ -2,18 +2,22 @@ use std::rc::Rc;
 use std::sync::Arc;
 
 use call::{ActiveCall, ParticipantLocation, Room};
+use channel::ChannelStore;
 use client::{User, proto::PeerId};
 use gpui::{
     AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, WeakEntity,
     canvas, point,
 };
 use gpui::{App, Task, Window, actions};
+use project::WorktreeSettings;
 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::notifications::DetachAndPromptErr;
 
 use crate::TitleBar;
@@ -347,6 +351,11 @@ impl TitleBar {
         let can_share_projects = room.can_share_projects();
         let screen_sharing_supported = cx.is_screen_capture_supported();
 
+        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();
 
         children.push(
@@ -368,6 +377,20 @@ impl TitleBar {
         );
 
         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",
@@ -382,6 +405,11 @@ impl TitleBar {
                 .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);

crates/worktree/src/worktree_settings.rs 🔗

@@ -11,6 +11,8 @@ use util::{
 #[derive(Clone, PartialEq, Eq)]
 pub struct WorktreeSettings {
     pub project_name: Option<String>,
+    /// Whether to prevent this project from being shared in public channels.
+    pub prevent_sharing_in_public_channels: bool,
     pub file_scan_inclusions: PathMatcher,
     pub file_scan_exclusions: PathMatcher,
     pub private_files: PathMatcher,
@@ -51,6 +53,7 @@ impl Settings for WorktreeSettings {
 
         Self {
             project_name: worktree.project_name.into_inner(),
+            prevent_sharing_in_public_channels: worktree.prevent_sharing_in_public_channels,
             file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions")
                 .log_err()
                 .unwrap_or_default(),