Add `sweep_ai` privacy mode setting (#48220)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

assets/settings/default.json                                   |  8 
crates/edit_prediction/src/sweep_ai.rs                         |  8 
crates/language/src/language_settings.rs                       | 17 +
crates/settings_content/src/language.rs                        | 14 +
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs | 45 +++
crates/settings_ui/src/settings_ui.rs                          | 47 ++-
6 files changed, 118 insertions(+), 21 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1545,6 +1545,13 @@
       "model": "codestral-latest",
       "max_tokens": 150,
     },
+    "sweep": {
+      // When enabled, Sweep will not store edit prediction inputs or outputs.
+      // When disabled, Sweep may collect data including buffer contents,
+      // diagnostics, file paths, repository names, and generated predictions
+      // to improve the service.
+      "privacy_mode": false,
+    },
     // Whether edit predictions are enabled when editing text threads in the agent panel.
     // This setting has no effect if globally disabled.
     "enabled_in_text_threads": true,
@@ -2346,7 +2353,6 @@
   "line_indicator_format": "long",
   // Set a proxy to use. The proxy protocol is specified by the URI scheme.
   //
-  // Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`,
   // `socks5h`. `http` will be used when no scheme is specified.
   //
   // By default no proxy will be used, or Zed will try get proxy settings from

crates/edit_prediction/src/sweep_ai.rs 🔗

@@ -11,6 +11,7 @@ use gpui::{
     App, AppContext as _, Entity, Global, SharedString, Task,
     http_client::{self, AsyncBody, Method},
 };
+use language::language_settings::all_language_settings;
 use language::{Anchor, Buffer, BufferSnapshot, Point, ToOffset as _};
 use language_model::{ApiKeyState, EnvVar, env_var};
 use lsp::DiagnosticSeverity;
@@ -44,6 +45,10 @@ impl SweepAi {
         inputs: EditPredictionModelInput,
         cx: &mut App,
     ) -> Task<Result<Option<EditPredictionResult>>> {
+        let privacy_mode_enabled = all_language_settings(None, cx)
+            .edit_predictions
+            .sweep
+            .privacy_mode;
         let debug_info = self.debug_info.clone();
         self.api_token.update(cx, |key_state, cx| {
             _ = key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx);
@@ -197,8 +202,7 @@ impl SweepAi {
                 retrieval_chunks,
                 recent_user_actions,
                 use_bytes: true,
-                // TODO
-                privacy_mode_enabled: false,
+                privacy_mode_enabled,
             };
 
             let mut buf: Vec<u8> = Vec::new();

crates/language/src/language_settings.rs 🔗

@@ -388,6 +388,8 @@ pub struct EditPredictionSettings {
     pub copilot: CopilotSettings,
     /// Settings specific to Codestral.
     pub codestral: CodestralSettings,
+    /// Settings specific to Sweep.
+    pub sweep: SweepSettings,
     /// Whether edit predictions are enabled in the assistant panel.
     /// This setting has no effect if globally disabled.
     pub enabled_in_text_threads: bool,
@@ -437,6 +439,15 @@ pub struct CodestralSettings {
     pub api_url: Option<String>,
 }
 
+#[derive(Clone, Debug, Default)]
+pub struct SweepSettings {
+    /// When enabled, Sweep will not store edit prediction inputs or outputs.
+    /// When disabled, Sweep may collect data including buffer contents,
+    /// diagnostics, file paths, repository names, and generated predictions
+    /// to improve the service.
+    pub privacy_mode: bool,
+}
+
 impl AllLanguageSettings {
     /// Returns the [`LanguageSettings`] for the language with the specified name.
     pub fn language<'a>(
@@ -663,6 +674,11 @@ impl settings::Settings for AllLanguageSettings {
             api_url: codestral.api_url,
         };
 
+        let sweep = edit_predictions.sweep.unwrap();
+        let sweep_settings = SweepSettings {
+            privacy_mode: sweep.privacy_mode.unwrap(),
+        };
+
         let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
 
         let mut file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)> = FxHashMap::default();
@@ -700,6 +716,7 @@ impl settings::Settings for AllLanguageSettings {
                 mode: edit_predictions_mode,
                 copilot: copilot_settings,
                 codestral: codestral_settings,
+                sweep: sweep_settings,
                 enabled_in_text_threads,
                 examples_dir: edit_predictions.examples_dir,
                 example_capture_rate: edit_predictions.example_capture_rate,

crates/settings_content/src/language.rs 🔗

@@ -203,6 +203,8 @@ pub struct EditPredictionSettingsContent {
     pub copilot: Option<CopilotSettingsContent>,
     /// Settings specific to Codestral.
     pub codestral: Option<CodestralSettingsContent>,
+    /// Settings specific to Sweep.
+    pub sweep: Option<SweepSettingsContent>,
     /// Whether edit predictions are enabled in the assistant prompt editor.
     /// This has no effect if globally disabled.
     pub enabled_in_text_threads: Option<bool>,
@@ -250,6 +252,18 @@ pub struct CodestralSettingsContent {
     pub api_url: Option<String>,
 }
 
+#[with_fallible_options]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
+pub struct SweepSettingsContent {
+    /// When enabled, Sweep will not store edit prediction inputs or outputs.
+    /// When disabled, Sweep may collect data including buffer contents,
+    /// diagnostics, file paths, repository names, and generated predictions
+    /// to improve the service.
+    ///
+    /// Default: false
+    pub privacy_mode: Option<bool>,
+}
+
 /// The mode in which edit predictions should be displayed.
 #[derive(
     Copy,

crates/settings_ui/src/pages/edit_prediction_provider_setup.rs 🔗

@@ -46,7 +46,16 @@ pub(crate) fn render_edit_prediction_setup_page(
                 "https://app.sweep.dev/".into(),
                 sweep_api_token(cx),
                 |_cx| SWEEP_CREDENTIALS_URL,
-                None,
+                Some(
+                    settings_window
+                        .render_sub_page_items_section(
+                            sweep_settings().iter().enumerate(),
+                            true,
+                            window,
+                            cx,
+                        )
+                        .into_any_element(),
+                ),
                 window,
                 cx,
             )
@@ -63,6 +72,7 @@ pub(crate) fn render_edit_prediction_setup_page(
                     settings_window
                         .render_sub_page_items_section(
                             codestral_settings().iter().enumerate(),
+                            true,
                             window,
                             cx,
                         )
@@ -285,6 +295,39 @@ fn render_api_key_provider(
     })
 }
 
+fn sweep_settings() -> Box<[SettingsPageItem]> {
+    Box::new([SettingsPageItem::SettingItem(SettingItem {
+        title: "Privacy Mode",
+        description: "When enabled, Sweep will not store edit prediction inputs or outputs. When disabled, Sweep may collect data including buffer contents, diagnostics, file paths, and generated predictions to improve the service.",
+        field: Box::new(SettingField {
+            pick: |settings| {
+                settings
+                    .project
+                    .all_languages
+                    .edit_predictions
+                    .as_ref()?
+                    .sweep
+                    .as_ref()?
+                    .privacy_mode
+                    .as_ref()
+            },
+            write: |settings, value| {
+                settings
+                    .project
+                    .all_languages
+                    .edit_predictions
+                    .get_or_insert_default()
+                    .sweep
+                    .get_or_insert_default()
+                    .privacy_mode = value;
+            },
+            json_path: Some("edit_predictions.sweep.privacy_mode"),
+        }),
+        metadata: None,
+        files: USER,
+    })])
+}
+
 fn codestral_settings() -> Box<[SettingsPageItem]> {
     Box::new([
         SettingsPageItem::SettingItem(SettingItem {

crates/settings_ui/src/settings_ui.rs 🔗

@@ -846,7 +846,8 @@ impl SettingsPageItem {
         &self,
         settings_window: &SettingsWindow,
         item_index: usize,
-        is_last: bool,
+        bottom_border: bool,
+        extra_bottom_padding: bool,
         window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) -> AnyElement {
@@ -854,7 +855,7 @@ impl SettingsPageItem {
 
         let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
             let element = element.pt_4();
-            if is_last {
+            if extra_bottom_padding {
                 element.pb_10()
             } else {
                 element.pb_4()
@@ -933,7 +934,7 @@ impl SettingsPageItem {
                     .group("setting-item")
                     .px_8()
                     .child(field_with_padding)
-                    .when(!is_last, |this| this.child(Divider::horizontal()))
+                    .when(bottom_border, |this| this.child(Divider::horizontal()))
                     .into_any_element()
             }
             SettingsPageItem::SubPageLink(sub_page_link) => v_flex()
@@ -1010,7 +1011,7 @@ impl SettingsPageItem {
                             cx,
                         )),
                 )
-                .when(!is_last, |this| this.child(Divider::horizontal()))
+                .when(bottom_border, |this| this.child(Divider::horizontal()))
                 .into_any_element(),
             SettingsPageItem::DynamicItem(DynamicItem {
                 discriminant: discriminant_setting_item,
@@ -1036,7 +1037,7 @@ impl SettingsPageItem {
                             .px_8()
                             .child(discriminant_element.when(has_sub_fields, |this| this.pb_4())),
                     )
-                    .when(!has_sub_fields && !is_last, |this| {
+                    .when(!has_sub_fields && bottom_border, |this| {
                         this.child(h_flex().px_8().child(Divider::horizontal()))
                     });
 
@@ -1057,7 +1058,9 @@ impl SettingsPageItem {
                                 .p_4()
                                 .border_t_1()
                                 .when(is_last_sub_field, |this| this.border_b_1())
-                                .when(is_last_sub_field && is_last, |this| this.mb_8())
+                                .when(is_last_sub_field && extra_bottom_padding, |this| {
+                                    this.mb_8()
+                                })
                                 .border_dashed()
                                 .border_color(cx.theme().colors().border_variant)
                                 .bg(cx.theme().colors().element_background.opacity(0.2)),
@@ -1114,7 +1117,7 @@ impl SettingsPageItem {
                             }),
                         ),
                 )
-                .when(!is_last, |this| this.child(Divider::horizontal()))
+                .when(bottom_border, |this| this.child(Divider::horizontal()))
                 .into_any_element(),
         }
     }
@@ -2940,12 +2943,16 @@ impl SettingsWindow {
                         return gpui::Empty.into_any_element();
                     };
 
-                    let no_bottom_border = visible_items
+                    let next_is_header = visible_items
                         .next()
                         .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_)))
                         .unwrap_or(false);
 
                     let is_last = Some(actual_item_index) == last_non_header_index;
+                    let is_last_in_section = next_is_header || is_last;
+
+                    let bottom_border = !is_last_in_section;
+                    let extra_bottom_padding = is_last_in_section;
 
                     let item_focus_handle = this.content_handles[current_page_index]
                         [actual_item_index]
@@ -2959,7 +2966,8 @@ impl SettingsWindow {
                         .child(item.render(
                             this,
                             actual_item_index,
-                            no_bottom_border || is_last,
+                            bottom_border,
+                            extra_bottom_padding,
                             window,
                             cx,
                         ))
@@ -2987,12 +2995,13 @@ impl SettingsWindow {
             .size_full()
             .overflow_y_scroll()
             .track_scroll(scroll_handle);
-        self.render_sub_page_items_in(page_content, items, window, cx)
+        self.render_sub_page_items_in(page_content, items, false, window, cx)
     }
 
     fn render_sub_page_items_section<'a, Items>(
         &self,
         items: Items,
+        is_inline_section: bool,
         window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) -> impl IntoElement
@@ -3000,13 +3009,14 @@ impl SettingsWindow {
         Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
     {
         let page_content = v_flex().id("settings-ui-sub-page-section").size_full();
-        self.render_sub_page_items_in(page_content, items, window, cx)
+        self.render_sub_page_items_in(page_content, items, is_inline_section, window, cx)
     }
 
     fn render_sub_page_items_in<'a, Items>(
         &self,
         page_content: Stateful<Div>,
         items: Items,
+        is_inline_section: bool,
         window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) -> impl IntoElement
@@ -3043,12 +3053,14 @@ impl SettingsWindow {
                 })
                 .children(items.clone().into_iter().enumerate().map(
                     |(index, (actual_item_index, item))| {
-                        let no_bottom_border =
-                            items.get(index + 1).is_some_and(|(_, next_item)| {
-                                matches!(next_item, SettingsPageItem::SectionHeader(_))
-                            });
+                        let is_last_item = Some(index) == last_non_header_index;
+                        let next_is_header = items.get(index + 1).is_some_and(|(_, next_item)| {
+                            matches!(next_item, SettingsPageItem::SectionHeader(_))
+                        });
+                        let bottom_border = !is_inline_section && !next_is_header && !is_last_item;
 
-                        let is_last = Some(index) == last_non_header_index;
+                        let extra_bottom_padding =
+                            !is_inline_section && (next_is_header || is_last_item);
 
                         v_flex()
                             .w_full()
@@ -3057,7 +3069,8 @@ impl SettingsWindow {
                             .child(item.render(
                                 self,
                                 actual_item_index,
-                                no_bottom_border || is_last,
+                                bottom_border,
+                                extra_bottom_padding,
                                 window,
                                 cx,
                             ))