From ec9ba5f069f415713a2f2e3e8550c28479678dba Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 May 2026 13:18:59 -0300 Subject: [PATCH] Make restricted mode more obvious (#57056) Closes TRA-150 This PR makes the restricted mode more obvious by: - Immediately opening the restricted mode modal upon opening an untrusted project - Disabling dismissing the modal on escape or click away to force choosing one of the two options (and avoid accidentally staying in restricted mode by simply dismissing it) - Showing the LSP button but with communication about language servers being disabled for untrusted projects - Showing a banner in the project settings with the same communication The motivation for this change was that we tried to be minimal with how we communicate a project is untrusted, but it was so minimal that people were confused as to why language servers and other settings weren't working. It was easy to miss the title bar button, for some reason. The changes in this PR makes it so acting on this decision (trust or not a project) is mandatory in order to even start to interact with the project. I appreciate changes here are more aggressive, but I think it's better to make you think about this decision vs. letting you be confused as to why you don't see LS completions or formatting. Release Notes: - Made restricted mode more obvious, demanding immediate action when opening an untrusted project. --- crates/git_ui/src/worktree_service.rs | 16 ++--- crates/language_tools/src/lsp_button.rs | 86 +++++++++++++++++++++---- crates/project/src/trusted_worktrees.rs | 11 ++++ crates/settings_ui/src/settings_ui.rs | 62 +++++++++++++++++- crates/title_bar/src/title_bar.rs | 9 +-- crates/workspace/src/security_modal.rs | 14 ++-- crates/workspace/src/workspace.rs | 20 ++++-- 7 files changed, 178 insertions(+), 40 deletions(-) diff --git a/crates/git_ui/src/worktree_service.rs b/crates/git_ui/src/worktree_service.rs index 0ec34f3d915..1eda4219092 100644 --- a/crates/git_ui/src/worktree_service.rs +++ b/crates/git_ui/src/worktree_service.rs @@ -252,17 +252,11 @@ fn maybe_propagate_worktree_trust( if ProjectSettings::get_global(cx).session.trust_all_worktrees { return; } - let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else { - return; - }; - let source_is_trusted = source_workspace .upgrade() .map(|workspace| { let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store(); - !trusted_store - .read(cx) - .has_restricted_worktrees(&source_worktree_store, cx) + !TrustedWorktrees::has_restricted_worktrees(&source_worktree_store, cx) }) .unwrap_or(false); @@ -280,9 +274,11 @@ fn maybe_propagate_worktree_trust( .collect(); if !paths_to_trust.is_empty() { - trusted_store.update(cx, |store, cx| { - store.trust(&worktree_store, paths_to_trust, cx); - }); + if let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) { + trusted_store.update(cx, |store, cx| { + store.trust(&worktree_store, paths_to_trust, cx); + }); + } } }) .ok(); diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index 8b7088dc228..e7c6d5b2160 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -13,12 +13,12 @@ use language::language_settings::{EditPredictionProvider, all_language_settings} use client::proto; use collections::HashSet; use editor::{Editor, EditorEvent}; -use gpui::{Anchor, App, Entity, Subscription, Task, TaskExt, WeakEntity, actions}; +use gpui::{Action as _, Anchor, App, Entity, Subscription, Task, TaskExt, WeakEntity, actions}; use language::{BinaryStatus, BufferId, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; use project::{ LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore, - project_settings::ProjectSettings, + project_settings::ProjectSettings, trusted_worktrees::TrustedWorktrees, }; use settings::{Settings as _, SettingsStore}; use ui::{ @@ -26,7 +26,7 @@ use ui::{ }; use util::{ResultExt, paths::PathExt, rel_path::RelPath}; -use workspace::{StatusItemView, Workspace}; +use workspace::{StatusItemView, ToggleWorktreeSecurity, Workspace}; use crate::lsp_log_view; @@ -221,6 +221,45 @@ impl LanguageServerState { return menu; }; + let is_restricted = self + .workspace + .upgrade() + .map(|workspace| { + let worktree_store = workspace.read(cx).project().read(cx).worktree_store(); + TrustedWorktrees::has_restricted_worktrees(&worktree_store, cx) + }) + .unwrap_or(false); + + if is_restricted { + menu = menu.custom_entry( + move |_window, _cx| { + v_flex() + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall), + ) + .child( + Label::new("Project is in Restricted Mode") + .size(LabelSize::Small), + ), + ) + .child( + Label::new("Language Servers can't run until you trust this project.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + move |window, cx| { + window.dispatch_action(ToggleWorktreeSecurity.boxed_clone(), cx); + }, + ); + } + let server_metadata = self .lsp_store .update(cx, |lsp_store, _| { @@ -832,12 +871,18 @@ impl LspButton { lsp_menu_refresh: Task::ready(()), _subscriptions: vec![settings_subscription, lsp_store_subscription], }; - if !lsp_button - .server_state - .read(cx) - .language_servers - .binary_statuses - .is_empty() + let is_restricted = TrustedWorktrees::has_restricted_worktrees( + &workspace.project().read(cx).worktree_store(), + cx, + ); + + if is_restricted + || !lsp_button + .server_state + .read(cx) + .language_servers + .binary_statuses + .is_empty() { lsp_button.refresh_lsp_menu(true, window, cx); } @@ -1258,7 +1303,20 @@ impl StatusItemView for LspButton { impl Render for LspButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() { + let is_restricted = self + .server_state + .read(cx) + .workspace + .upgrade() + .map(|workspace| { + let worktree_store = workspace.read(cx).project().read(cx).worktree_store(); + TrustedWorktrees::has_restricted_worktrees(&worktree_store, cx) + }) + .unwrap_or(false); + + if !is_restricted + && (self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none()) + { return div().hidden(); } @@ -1288,7 +1346,12 @@ impl Render for LspButton { } } - let (indicator, description) = if has_errors { + let (indicator, description) = if is_restricted { + ( + Some(Indicator::dot().color(Color::Warning)), + "Restricted Mode", + ) + } else if has_errors { ( Some(Indicator::dot().color(Color::Error)), "Server with errors", @@ -1333,6 +1396,7 @@ impl Render for LspButton { IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined) .when_some(indicator, IconButton::indicator) .icon_size(IconSize::Small) + .when(is_restricted, |s| s.icon_color(Color::Warning)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), move |_window, cx| { Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx) diff --git a/crates/project/src/trusted_worktrees.rs b/crates/project/src/trusted_worktrees.rs index 69d410adc66..8d8804c3f97 100644 --- a/crates/project/src/trusted_worktrees.rs +++ b/crates/project/src/trusted_worktrees.rs @@ -113,6 +113,17 @@ impl TrustedWorktrees { pub fn try_get_global(cx: &App) -> Option> { cx.try_global::().map(|this| this.0.clone()) } + + /// Whether the given project store has any restricted worktrees. + pub fn has_restricted_worktrees(worktree_store: &Entity, cx: &App) -> bool { + Self::try_get_global(cx) + .map(|trusted| { + trusted + .read(cx) + .has_restricted_worktrees(worktree_store, cx) + }) + .unwrap_or(false) + } } /// A collection of worktrees that are considered trusted and not trusted. diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index f8d938e9eec..02bbacdfa30 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -3350,6 +3350,65 @@ impl SettingsWindow { .into_any_element() } + let mut restricted_banner = gpui::Empty.into_any_element(); + if let SettingsUiFile::Project((worktree_id, _)) = &self.current_file { + let worktree_id = *worktree_id; + let is_restricted = all_projects(self.original_window.as_ref(), cx) + .find(|project| project.read(cx).worktree_for_id(worktree_id, cx).is_some()) + .map(|project| { + let worktree_store = project.read(cx).worktree_store(); + project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees( + &worktree_store, + cx, + ) + }) + .unwrap_or(false); + + if is_restricted { + let original_window = self.original_window; + restricted_banner = Banner::new() + .severity(Severity::Warning) + .child( + v_flex() + .my_0p5() + .gap_0p5() + .child(Label::new("Restricted Mode")) + .child( + Label::new( + "This project is in restricted mode. Some project settings may not apply.", + ) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .action_slot( + div().pr_2().pb_1().child( + Button::new("manage-trust", "Manage Trust") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .on_click(cx.listener(move |_this, _, window, cx| { + if let Some(original_window) = original_window { + original_window + .update(cx, |multi_workspace, window, cx| { + multi_workspace + .workspace() + .update(cx, |workspace, cx| { + workspace + .show_worktree_trust_security_modal( + true, window, cx, + ); + }); + }) + .log_err(); + } + // Close the settings window + window.remove_window(); + })), + ), + ) + .into_any_element(); + } + } + v_flex() .id("settings-ui-page") .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { @@ -3440,7 +3499,8 @@ impl SettingsWindow { .px_8() .gap_2() .child(page_header) - .child(warning_banner), + .child(warning_banner) + .child(restricted_banner), ) .child( div() diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index c15f840e69d..3bc12a20748 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -641,13 +641,8 @@ impl TitleBar { } pub fn render_restricted_mode(&self, cx: &mut Context) -> Option { - let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx) - .map(|trusted_worktrees| { - trusted_worktrees - .read(cx) - .has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx) - }) - .unwrap_or(false); + let has_restricted_worktrees = + TrustedWorktrees::has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx); if !has_restricted_worktrees { return None; } diff --git a/crates/workspace/src/security_modal.rs b/crates/workspace/src/security_modal.rs index 2130a1d1eca..89ce2abfd66 100644 --- a/crates/workspace/src/security_modal.rs +++ b/crates/workspace/src/security_modal.rs @@ -56,11 +56,17 @@ impl ModalView for SecurityModal { fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context) -> DismissDecision { match self.trusted { - Some(false) => telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"), - Some(true) => telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"), - None => telemetry::event!("Dismissed", source = "Worktree Trust Modal"), + Some(false) => { + telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"); + DismissDecision::Dismiss(true) + } + Some(true) => { + telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"); + DismissDecision::Dismiss(true) + } + // Block dismiss via escape or clicking outside; user must pick an action + None => DismissDecision::Dismiss(false), } - DismissDecision::Dismiss(true) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index da8ffe972ee..599a2d23681 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2122,6 +2122,15 @@ impl Workspace { .log_err(); } + // Auto-show the security modal if the project has restricted worktrees + window + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.show_worktree_trust_security_modal(false, window, cx); + }); + }) + .log_err(); + Ok(OpenResult { window, workspace, @@ -8014,13 +8023,10 @@ impl Workspace { }); } } else { - let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx) - .map(|trusted_worktrees| { - trusted_worktrees - .read(cx) - .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx) - }) - .unwrap_or(false); + let has_restricted_worktrees = TrustedWorktrees::has_restricted_worktrees( + &self.project().read(cx).worktree_store(), + cx, + ); if has_restricted_worktrees { let project = self.project().read(cx); let remote_host = project