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.
This commit is contained in:
Danilo Leal 2026-05-18 13:18:59 -03:00 committed by GitHub
parent ea01b926ea
commit ec9ba5f069
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 178 additions and 40 deletions

View file

@ -252,17 +252,11 @@ fn maybe_propagate_worktree_trust(
if ProjectSettings::get_global(cx).session.trust_all_worktrees { if ProjectSettings::get_global(cx).session.trust_all_worktrees {
return; return;
} }
let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else {
return;
};
let source_is_trusted = source_workspace let source_is_trusted = source_workspace
.upgrade() .upgrade()
.map(|workspace| { .map(|workspace| {
let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store(); let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store();
!trusted_store !TrustedWorktrees::has_restricted_worktrees(&source_worktree_store, cx)
.read(cx)
.has_restricted_worktrees(&source_worktree_store, cx)
}) })
.unwrap_or(false); .unwrap_or(false);
@ -280,9 +274,11 @@ fn maybe_propagate_worktree_trust(
.collect(); .collect();
if !paths_to_trust.is_empty() { if !paths_to_trust.is_empty() {
trusted_store.update(cx, |store, cx| { if let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) {
store.trust(&worktree_store, paths_to_trust, cx); trusted_store.update(cx, |store, cx| {
}); store.trust(&worktree_store, paths_to_trust, cx);
});
}
} }
}) })
.ok(); .ok();

View file

@ -13,12 +13,12 @@ use language::language_settings::{EditPredictionProvider, all_language_settings}
use client::proto; use client::proto;
use collections::HashSet; use collections::HashSet;
use editor::{Editor, EditorEvent}; 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 language::{BinaryStatus, BufferId, ServerHealth};
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
use project::{ use project::{
LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore, LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
project_settings::ProjectSettings, project_settings::ProjectSettings, trusted_worktrees::TrustedWorktrees,
}; };
use settings::{Settings as _, SettingsStore}; use settings::{Settings as _, SettingsStore};
use ui::{ use ui::{
@ -26,7 +26,7 @@ use ui::{
}; };
use util::{ResultExt, paths::PathExt, rel_path::RelPath}; use util::{ResultExt, paths::PathExt, rel_path::RelPath};
use workspace::{StatusItemView, Workspace}; use workspace::{StatusItemView, ToggleWorktreeSecurity, Workspace};
use crate::lsp_log_view; use crate::lsp_log_view;
@ -221,6 +221,45 @@ impl LanguageServerState {
return menu; 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 let server_metadata = self
.lsp_store .lsp_store
.update(cx, |lsp_store, _| { .update(cx, |lsp_store, _| {
@ -832,12 +871,18 @@ impl LspButton {
lsp_menu_refresh: Task::ready(()), lsp_menu_refresh: Task::ready(()),
_subscriptions: vec![settings_subscription, lsp_store_subscription], _subscriptions: vec![settings_subscription, lsp_store_subscription],
}; };
if !lsp_button let is_restricted = TrustedWorktrees::has_restricted_worktrees(
.server_state &workspace.project().read(cx).worktree_store(),
.read(cx) cx,
.language_servers );
.binary_statuses
.is_empty() if is_restricted
|| !lsp_button
.server_state
.read(cx)
.language_servers
.binary_statuses
.is_empty()
{ {
lsp_button.refresh_lsp_menu(true, window, cx); lsp_button.refresh_lsp_menu(true, window, cx);
} }
@ -1258,7 +1303,20 @@ impl StatusItemView for LspButton {
impl Render for LspButton { impl Render for LspButton {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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(); 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)), Some(Indicator::dot().color(Color::Error)),
"Server with errors", "Server with errors",
@ -1333,6 +1396,7 @@ impl Render for LspButton {
IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined) IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
.when_some(indicator, IconButton::indicator) .when_some(indicator, IconButton::indicator)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.when(is_restricted, |s| s.icon_color(Color::Warning))
.indicator_border_color(Some(cx.theme().colors().status_bar_background)), .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
move |_window, cx| { move |_window, cx| {
Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx) Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)

View file

@ -113,6 +113,17 @@ impl TrustedWorktrees {
pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> { pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
cx.try_global::<Self>().map(|this| this.0.clone()) cx.try_global::<Self>().map(|this| this.0.clone())
} }
/// Whether the given project store has any restricted worktrees.
pub fn has_restricted_worktrees(worktree_store: &Entity<WorktreeStore>, 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. /// A collection of worktrees that are considered trusted and not trusted.

View file

@ -3350,6 +3350,65 @@ impl SettingsWindow {
.into_any_element() .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() v_flex()
.id("settings-ui-page") .id("settings-ui-page")
.on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
@ -3440,7 +3499,8 @@ impl SettingsWindow {
.px_8() .px_8()
.gap_2() .gap_2()
.child(page_header) .child(page_header)
.child(warning_banner), .child(warning_banner)
.child(restricted_banner),
) )
.child( .child(
div() div()

View file

@ -641,13 +641,8 @@ impl TitleBar {
} }
pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> { pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx) let has_restricted_worktrees =
.map(|trusted_worktrees| { TrustedWorktrees::has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx);
trusted_worktrees
.read(cx)
.has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
})
.unwrap_or(false);
if !has_restricted_worktrees { if !has_restricted_worktrees {
return None; return None;
} }

View file

@ -56,11 +56,17 @@ impl ModalView for SecurityModal {
fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context<Self>) -> DismissDecision { fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context<Self>) -> DismissDecision {
match self.trusted { match self.trusted {
Some(false) => telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"), Some(false) => {
Some(true) => telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"), telemetry::event!("Open in Restricted", source = "Worktree Trust Modal");
None => telemetry::event!("Dismissed", 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)
} }
} }

View file

@ -2122,6 +2122,15 @@ impl Workspace {
.log_err(); .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 { Ok(OpenResult {
window, window,
workspace, workspace,
@ -8014,13 +8023,10 @@ impl Workspace {
}); });
} }
} else { } else {
let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx) let has_restricted_worktrees = TrustedWorktrees::has_restricted_worktrees(
.map(|trusted_worktrees| { &self.project().read(cx).worktree_store(),
trusted_worktrees cx,
.read(cx) );
.has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
})
.unwrap_or(false);
if has_restricted_worktrees { if has_restricted_worktrees {
let project = self.project().read(cx); let project = self.project().read(cx);
let remote_host = project let remote_host = project