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 {
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();

View file

@ -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<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();
}
@ -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)

View file

@ -113,6 +113,17 @@ impl TrustedWorktrees {
pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
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.

View file

@ -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()

View file

@ -641,13 +641,8 @@ impl TitleBar {
}
pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
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;
}

View file

@ -56,11 +56,17 @@ impl ModalView for SecurityModal {
fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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)
}
}

View file

@ -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