From b96f1c4738954dba100482c32351ce85721f6664 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 2 Feb 2026 16:33:14 -0600 Subject: [PATCH] Add `sweep_ai` privacy mode setting (#48220) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- 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 ++++++ .../pages/edit_prediction_provider_setup.rs | 45 +++++++++++++++++- crates/settings_ui/src/settings_ui.rs | 47 ++++++++++++------- 6 files changed, 118 insertions(+), 21 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 28e7a45248510a94c10a7669879a1e5a7ab8a39b..65bd40ab6939f04036772d932ecc4cb59adb2905 100644 --- a/assets/settings/default.json +++ b/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 diff --git a/crates/edit_prediction/src/sweep_ai.rs b/crates/edit_prediction/src/sweep_ai.rs index d781d175a247a0ee7c92565cb9becc7446d34df0..b42f54b7a89ea3f858501529d785c9013d490c99 100644 --- a/crates/edit_prediction/src/sweep_ai.rs +++ b/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>> { + 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 = Vec::new(); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 827a6389ac35940319df49d5b89843b72b3e232a..a7e678e6ee43a3cac0a4ea388923f8341adbb86e 100644 --- a/crates/language/src/language_settings.rs +++ b/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, } +#[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, (GlobSet, Vec)> = 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, diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index 5e0c553f3a9bd25b85c0deabe6463f556b6d5cdf..aad7a27879de3219ce5d7734068ec6feb1796983 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -203,6 +203,8 @@ pub struct EditPredictionSettingsContent { pub copilot: Option, /// Settings specific to Codestral. pub codestral: Option, + /// Settings specific to Sweep. + pub sweep: Option, /// Whether edit predictions are enabled in the assistant prompt editor. /// This has no effect if globally disabled. pub enabled_in_text_threads: Option, @@ -250,6 +252,18 @@ pub struct CodestralSettingsContent { pub api_url: Option, } +#[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, +} + /// The mode in which edit predictions should be displayed. #[derive( Copy, diff --git a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs index 3c180d46cdcdaec45417a5b60a58c26e93bd3156..64fd4e376352f4ccc2fe8978c02b8c7fe3dc0c3e 100644 --- a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs +++ b/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 { diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 6e4ce68a1622785efb5cd052ec79172bcbe66b07..d43b0a6853242294f1219b70f13660491f93767a 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/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, ) -> AnyElement { @@ -854,7 +855,7 @@ impl SettingsPageItem { let apply_padding = |element: Stateful
| -> Stateful
{ 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, ) -> impl IntoElement @@ -3000,13 +3009,14 @@ impl SettingsWindow { Items: Iterator, { 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
, items: Items, + is_inline_section: bool, window: &mut Window, cx: &mut Context, ) -> 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, ))