From 4e85f31dacd6bf504f81ad134cdad3cd8e730a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 04:47:45 -0400 Subject: [PATCH] Add diagnostics privacy controls --- .../shell/internal_pages/privacy_security.rs | 149 +++++++++++++++++- crates/ely_app/src/shell/settings_actions.rs | 16 +- crates/ely_browser_core/src/navigation.rs | 4 +- crates/ely_browser_core/src/state.rs | 24 ++- .../tests/settings_defaults.rs | 13 +- crates/ely_domain/src/lib.rs | 2 +- crates/ely_domain/src/privacy.rs | 42 +++++ 7 files changed, 230 insertions(+), 20 deletions(-) diff --git a/crates/ely_app/src/shell/internal_pages/privacy_security.rs b/crates/ely_app/src/shell/internal_pages/privacy_security.rs index d997341..82dadf5 100644 --- a/crates/ely_app/src/shell/internal_pages/privacy_security.rs +++ b/crates/ely_app/src/shell/internal_pages/privacy_security.rs @@ -1,6 +1,6 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; -use ely_domain::HistoryRecordingPolicy; +use ely_domain::{DiagnosticsReportingPolicy, HistoryRecordingPolicy}; use gpui::prelude::FluentBuilder; use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; use gpui_component::{ @@ -32,7 +32,7 @@ impl ElyShell { .when(snapshot.active_profile_history_entry_count > 0, |this| { this.child(render_history_clear_controls(confirming_clear, cx)) }) - .child(render_history_policy_rows(snapshot.history_recording_policy, cx)), + .child(render_privacy_settings_rows(snapshot, cx)), ) } } @@ -229,18 +229,43 @@ fn render_clear_history_confirmation(cx: &mut Context) -> AnyElement { .into_any_element() } -fn render_history_policy_rows( - active_policy: HistoryRecordingPolicy, +fn render_privacy_settings_rows( + snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { div() .flex_1() .min_h_0() - .flex() - .flex_col() .overflow_y_scrollbar() .border_t_1() .border_color(rgb(colors::HAIRLINE)) + .flex() + .flex_col() + .child(section_label("History")) + .child(render_history_policy_rows(snapshot.history_recording_policy, cx)) + .child(section_label("Diagnostics")) + .child(render_diagnostics_policy_rows(snapshot.diagnostics_reporting_policy, cx)) + .into_any_element() +} + +fn section_label(label: &'static str) -> AnyElement { + div() + .pt_4() + .pb_2() + .text_xs() + .font_semibold() + .text_color(rgb(colors::MUTED)) + .child(label) + .into_any_element() +} + +fn render_history_policy_rows( + active_policy: HistoryRecordingPolicy, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .flex_col() .children( HistoryRecordingPolicy::ALL .iter() @@ -251,6 +276,85 @@ fn render_history_policy_rows( .into_any_element() } +fn render_diagnostics_policy_rows( + active_policy: DiagnosticsReportingPolicy, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .flex_col() + .children( + DiagnosticsReportingPolicy::ALL.iter().copied().enumerate().map(|(index, policy)| { + render_diagnostics_policy_row(index, policy, active_policy, cx) + }), + ) + .into_any_element() +} + +fn render_diagnostics_policy_row( + index: usize, + policy: DiagnosticsReportingPolicy, + active_policy: DiagnosticsReportingPolicy, + cx: &mut Context, +) -> AnyElement { + let selected = policy == active_policy; + + div() + .py_3() + .border_b_1() + .border_color(rgb(colors::HAIRLINE)) + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child( + div() + .text_color(rgb(diagnostics_policy_icon_color(policy, selected))) + .child(diagnostics_policy_icon(policy, selected)), + ) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(policy.name()), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(policy.detail()), + ), + ), + ) + .child( + Button::new(("diagnostics-reporting-policy", index)) + .ghost() + .xsmall() + .selected(selected) + .label(diagnostics_policy_button_label(policy, selected)) + .tooltip(policy.name()) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.set_diagnostics_reporting_policy(policy, cx); + })), + ) + .into_any_element() +} + fn render_history_policy_row( index: usize, policy: HistoryRecordingPolicy, @@ -322,6 +426,13 @@ fn privacy_icon(policy: HistoryRecordingPolicy) -> IconName { } } +fn diagnostics_icon(policy: DiagnosticsReportingPolicy) -> IconName { + match policy { + DiagnosticsReportingPolicy::Minimal => IconName::Info, + DiagnosticsReportingPolicy::Paused => IconName::EyeOff, + } +} + fn policy_color(policy: HistoryRecordingPolicy) -> u32 { match policy { HistoryRecordingPolicy::Record => colors::SUCCESS, @@ -329,14 +440,29 @@ fn policy_color(policy: HistoryRecordingPolicy) -> u32 { } } +fn diagnostics_policy_color(policy: DiagnosticsReportingPolicy) -> u32 { + match policy { + DiagnosticsReportingPolicy::Minimal => colors::SUCCESS, + DiagnosticsReportingPolicy::Paused => colors::PRIMARY, + } +} + fn history_policy_icon(policy: HistoryRecordingPolicy, selected: bool) -> IconName { if selected { IconName::CircleCheck } else { privacy_icon(policy) } } +fn diagnostics_policy_icon(policy: DiagnosticsReportingPolicy, selected: bool) -> IconName { + if selected { IconName::CircleCheck } else { diagnostics_icon(policy) } +} + fn history_policy_icon_color(policy: HistoryRecordingPolicy, selected: bool) -> u32 { if selected { policy_color(policy) } else { colors::MUTED_SOFT } } +fn diagnostics_policy_icon_color(policy: DiagnosticsReportingPolicy, selected: bool) -> u32 { + if selected { diagnostics_policy_color(policy) } else { colors::MUTED_SOFT } +} + fn history_policy_button_label(policy: HistoryRecordingPolicy, selected: bool) -> &'static str { match (policy, selected) { (HistoryRecordingPolicy::Record, true) => "Default", @@ -344,3 +470,14 @@ fn history_policy_button_label(policy: HistoryRecordingPolicy, selected: bool) - _ => "Select", } } + +fn diagnostics_policy_button_label( + policy: DiagnosticsReportingPolicy, + selected: bool, +) -> &'static str { + match (policy, selected) { + (DiagnosticsReportingPolicy::Minimal, true) => "Default", + (_, true) => "Active", + _ => "Select", + } +} diff --git a/crates/ely_app/src/shell/settings_actions.rs b/crates/ely_app/src/shell/settings_actions.rs index 9f995d9..6c645b3 100644 --- a/crates/ely_app/src/shell/settings_actions.rs +++ b/crates/ely_app/src/shell/settings_actions.rs @@ -1,6 +1,7 @@ use ely_domain::{ - ArchivePolicy, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, - ProfileId, ProfileSyncPolicy, SearchEngine, SyncObjectKind, SyncObjectPolicy, UpdatePolicy, + ArchivePolicy, DiagnosticsReportingPolicy, DownloadPolicy, FavoriteLimit, + HistoryRecordingPolicy, NewTabDestination, ProfileId, ProfileSyncPolicy, SearchEngine, + SyncObjectKind, SyncObjectPolicy, UpdatePolicy, }; use gpui::Context; @@ -66,6 +67,17 @@ impl ElyShell { } } + pub(super) fn set_diagnostics_reporting_policy( + &mut self, + policy: DiagnosticsReportingPolicy, + cx: &mut Context, + ) { + if let ShellState::Ready(core) = &mut self.state { + core.set_diagnostics_reporting_policy(policy); + cx.notify(); + } + } + pub(super) fn reset_privacy_settings(&mut self, cx: &mut Context) { if let ShellState::Ready(core) = &mut self.state { core.reset_privacy_settings(); diff --git a/crates/ely_browser_core/src/navigation.rs b/crates/ely_browser_core/src/navigation.rs index 9df015f..467dcb4 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -343,10 +343,12 @@ const SETTINGS_ROUTE_MATCHES: &[SettingsRouteMatch] = &[ "privacy & security", "history", "history recording", + "diagnostics", + "diagnostic reporting", ], search_terms: &[ "Privacy & Security", - "History recording and profile-scoped privacy controls.", + "History recording, diagnostics, and profile-scoped privacy controls.", "profile privacy", "recording policy", ], diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 7cb50f4..23f4b74 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -1,11 +1,11 @@ use std::{collections::BTreeMap, time::SystemTime}; use ely_domain::{ - ArchivePolicy, ArchivedTab, BookmarkEntry, BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, DomainError, - DownloadEntry, DownloadPolicy, FavoriteLimit, HistoryEntry, HistoryRecordingPolicy, - NewTabDestination, NoteEntry, Profile, ProfileId, ProfileKind, ReadingListEntry, SearchEngine, - SitePermissionAuditEvent, SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, - TabGroup, TabId, UpdatePolicy, UrlText, + ArchivePolicy, ArchivedTab, BookmarkEntry, BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, + DiagnosticsReportingPolicy, DomainError, DownloadEntry, DownloadPolicy, FavoriteLimit, + HistoryEntry, HistoryRecordingPolicy, NewTabDestination, NoteEntry, Profile, ProfileId, + ProfileKind, ReadingListEntry, SearchEngine, SitePermissionAuditEvent, SitePermissionEntry, + Space, SpaceId, SplitLayout, SyncStatus, TabGroup, TabId, UpdatePolicy, UrlText, }; use crate::{CoreError, navigation::tab_title}; @@ -112,6 +112,7 @@ pub struct BrowserSnapshot { pub search_engine: SearchEngine, pub new_tab_destination: NewTabDestination, pub history_recording_policy: HistoryRecordingPolicy, + pub diagnostics_reporting_policy: DiagnosticsReportingPolicy, pub favorite_limit: FavoriteLimit, pub update_policy: UpdatePolicy, pub command_query: String, @@ -144,6 +145,7 @@ pub struct BrowserCore { search_engine: SearchEngine, new_tab_destination: NewTabDestination, history_recording_policy: HistoryRecordingPolicy, + diagnostics_reporting_policy: DiagnosticsReportingPolicy, favorite_limit: FavoriteLimit, update_policy: UpdatePolicy, sync_object_policies: SyncObjectPolicies, @@ -190,6 +192,7 @@ impl BrowserCore { search_engine: SearchEngine::default(), new_tab_destination, history_recording_policy: HistoryRecordingPolicy::default(), + diagnostics_reporting_policy: DiagnosticsReportingPolicy::default(), favorite_limit: FavoriteLimit::default(), update_policy: UpdatePolicy::default(), sync_object_policies: SyncObjectPolicies::default(), @@ -325,8 +328,13 @@ impl BrowserCore { self.history_recording_policy = policy; } + pub fn set_diagnostics_reporting_policy(&mut self, policy: DiagnosticsReportingPolicy) { + self.diagnostics_reporting_policy = policy; + } + pub fn reset_privacy_settings(&mut self) { self.set_history_recording_policy(HistoryRecordingPolicy::default()); + self.set_diagnostics_reporting_policy(DiagnosticsReportingPolicy::default()); } #[must_use] @@ -334,6 +342,11 @@ impl BrowserCore { self.history_recording_policy } + #[must_use] + pub fn diagnostics_reporting_policy(&self) -> DiagnosticsReportingPolicy { + self.diagnostics_reporting_policy + } + pub fn set_favorite_limit(&mut self, favorite_limit: FavoriteLimit) { self.favorite_limit = favorite_limit; } @@ -408,6 +421,7 @@ impl BrowserCore { search_engine: self.search_engine, new_tab_destination: self.new_tab_destination, history_recording_policy: self.history_recording_policy, + diagnostics_reporting_policy: self.diagnostics_reporting_policy, favorite_limit: self.favorite_limit, update_policy: self.update_policy, command_query: self.command_query.clone(), diff --git a/crates/ely_browser_core/tests/settings_defaults.rs b/crates/ely_browser_core/tests/settings_defaults.rs index e3b3eb8..61f3450 100644 --- a/crates/ely_browser_core/tests/settings_defaults.rs +++ b/crates/ely_browser_core/tests/settings_defaults.rs @@ -2,9 +2,9 @@ use std::{error::Error, path::PathBuf}; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; use ely_domain::{ - ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, - NewTabDestination, ProfileKind, ProfileSyncPolicy, SearchEngine, SyncObjectKind, - SyncObjectPolicy, UpdatePolicy, + ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, DiagnosticsReportingPolicy, DownloadPolicy, + FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, ProfileKind, ProfileSyncPolicy, + SearchEngine, SyncObjectKind, SyncObjectPolicy, UpdatePolicy, }; #[test] @@ -20,10 +20,13 @@ fn section_resets_restore_settings_defaults() -> Result<(), Box> { assert_eq!(core.snapshot()?.search_engine, SearchEngine::DuckDuckGo); core.set_history_recording_policy(HistoryRecordingPolicy::Pause); + core.set_diagnostics_reporting_policy(DiagnosticsReportingPolicy::Paused); core.reset_privacy_settings(); - assert_eq!(core.snapshot()?.history_recording_policy, HistoryRecordingPolicy::Record); + let snapshot = core.snapshot()?; + assert_eq!(snapshot.history_recording_policy, HistoryRecordingPolicy::Record); + assert_eq!(snapshot.diagnostics_reporting_policy, DiagnosticsReportingPolicy::Minimal); - let active_space_id = core.snapshot()?.active_space_id; + let active_space_id = snapshot.active_space_id; core.set_active_space_archive_policy(ArchivePolicy::IdleDays(30))?; core.set_space_sidebar_width(&active_space_id, 56)?; core.set_favorite_limit(FavoriteLimit::Six); diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index b72a4dd..2a058eb 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -42,7 +42,7 @@ pub use plugin::{ PluginContributionPoint, PluginId, PluginManifest, PluginPermission, PluginPermissionRisk, PluginSignature, PluginSignatureAlgorithm, }; -pub use privacy::HistoryRecordingPolicy; +pub use privacy::{DiagnosticsReportingPolicy, HistoryRecordingPolicy}; pub use profile::{Profile, ProfileKind, ProfileSyncPolicy}; pub use reading_list::{ReadingListEntry, ReadingProgress, ReadingProgressPercent}; pub use search::SearchEngine; diff --git a/crates/ely_domain/src/privacy.rs b/crates/ely_domain/src/privacy.rs index 9b84ee6..637e71d 100644 --- a/crates/ely_domain/src/privacy.rs +++ b/crates/ely_domain/src/privacy.rs @@ -37,3 +37,45 @@ impl HistoryRecordingPolicy { } } } + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum DiagnosticsReportingPolicy { + #[default] + Minimal, + Paused, +} + +impl DiagnosticsReportingPolicy { + pub const ALL: &[Self] = &[Self::Minimal, Self::Paused]; + + #[must_use] + pub fn reports_diagnostics(self) -> bool { + matches!(self, Self::Minimal) + } + + #[must_use] + pub fn name(self) -> &'static str { + match self { + Self::Minimal => "Minimal Diagnostics", + Self::Paused => "Paused Diagnostics", + } + } + + #[must_use] + pub fn detail(self) -> &'static str { + match self { + Self::Minimal => { + "Send startup, crash, WebView crash, sync error, plugin crash, and update result codes." + } + Self::Paused => "Keep diagnostics local for review and privacy exports.", + } + } + + #[must_use] + pub fn status(self) -> &'static str { + match self { + Self::Minimal => "Diagnostics reporting is on", + Self::Paused => "Diagnostics reporting is paused", + } + } +}