Introduce global ai setting

This commit is contained in:
Nate Butler
2025-07-07 11:41:55 -04:00
parent d30664d4b1
commit 7e22e57e08
9 changed files with 188 additions and 29 deletions

2
Cargo.lock generated
View File

@@ -10826,6 +10826,7 @@ dependencies = [
"editor",
"feature_flags",
"gpui",
"language",
"menu",
"project",
"serde_json",
@@ -19999,6 +20000,7 @@ dependencies = [
"collab_ui",
"collections",
"command_palette",
"command_palette_hooks",
"component",
"copilot",
"dap",

View File

@@ -25,7 +25,11 @@
// Features that can be globally enabled or disabled
"features": {
// Which edit prediction provider to use.
"edit_prediction_provider": "zed"
"edit_prediction_provider": "zed",
// A globally enable or disable AI features.
//
// This setting supersedes all other settings related to AI features.
"ai_assistance": true
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono",

View File

@@ -33,6 +33,7 @@ use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
};
use language::language_settings;
use language::{Buffer, Point, Selection, TransactionId};
use language_model::{
ConfigurationError, ConfiguredModel, LanguageModelRegistry, report_assistant_event,
@@ -1768,7 +1769,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
_: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> {
if !AgentSettings::get_global(cx).enabled {
if !AgentSettings::get_global(cx).enabled || !language_settings::ai_enabled(cx) {
return Task::ready(Ok(Vec::new()));
}

View File

@@ -54,11 +54,18 @@ pub fn all_language_settings<'a>(
AllLanguageSettings::get(location, cx)
}
/// Returns whether AI assistance is globally enabled or disabled.
pub fn ai_enabled(cx: &App) -> bool {
all_language_settings(None, cx).ai_assistance
}
/// The settings for all languages.
#[derive(Debug, Clone)]
pub struct AllLanguageSettings {
/// The edit prediction settings.
pub edit_predictions: EditPredictionSettings,
/// Whether AI assistance is enabled.
pub ai_assistance: bool,
pub defaults: LanguageSettings,
languages: HashMap<LanguageName, LanguageSettings>,
pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
@@ -646,6 +653,8 @@ pub struct CopilotSettingsContent {
pub struct FeaturesContent {
/// Determines which edit prediction provider to use.
pub edit_prediction_provider: Option<EditPredictionProvider>,
/// Whether AI assistance is enabled.
pub ai_assistance: Option<bool>,
}
/// Controls the soft-wrapping behavior in the editor.
@@ -1122,6 +1131,11 @@ impl AllLanguageSettings {
pub fn edit_predictions_mode(&self) -> EditPredictionsMode {
self.edit_predictions.mode
}
/// Returns whether AI assistance is enabled.
pub fn is_ai_assistance_enabled(&self) -> bool {
self.ai_assistance
}
}
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
@@ -1247,6 +1261,12 @@ impl settings::Settings for AllLanguageSettings {
.map(|settings| settings.enabled_in_text_threads)
.unwrap_or(true);
let ai_assistance = default_value
.features
.as_ref()
.and_then(|f| f.ai_assistance)
.unwrap_or(true);
let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
for (language, patterns) in &default_value.file_types {
@@ -1359,6 +1379,7 @@ impl settings::Settings for AllLanguageSettings {
copilot: copilot_settings,
enabled_in_text_threads,
},
ai_assistance,
defaults,
languages,
file_types,

View File

@@ -23,6 +23,7 @@ component.workspace = true
db.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
project.workspace = true
serde_json.workspace = true

View File

@@ -10,20 +10,22 @@ use client::{Client, TelemetrySettings};
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use gpui::{
Action, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyBinding, Task,
UpdateGlobal, WeakEntity, actions, prelude::*, svg, transparent_black,
Action, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyBinding, Subscription,
Task, UpdateGlobal, WeakEntity, actions, prelude::*, svg, transparent_black,
};
use menu;
use persistence::ONBOARDING_DB;
use language::language_settings::{AllLanguageSettings, ai_enabled, all_language_settings};
use project::Project;
use serde_json;
use settings::{Settings, SettingsStore};
use settings::{Settings, SettingsStore, update_settings_file};
use settings_ui::SettingsUiFeatureFlag;
use std::collections::HashSet;
use std::sync::Arc;
use theme::{Theme, ThemeRegistry, ThemeSettings};
use ui::{
CheckboxWithLabel, ContentGroup, KeybindingHint, ListItem, Ring, ToggleState, Vector,
CheckboxWithLabel, ContentGroup, FocusOutline, KeybindingHint, ListItem, ToggleState, Vector,
VectorName, prelude::*,
};
use util::ResultExt;
@@ -87,7 +89,7 @@ fn feature_gate_onboarding_ui_actions(cx: &mut App) {
.detach();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OnboardingPage {
Basics,
Editing,
@@ -139,7 +141,7 @@ pub struct OnboardingUI {
current_page: OnboardingPage,
nav_focus: NavigationFocusItem,
page_focus: [PageFocusItem; 4],
completed_pages: [bool; 4],
completed_pages: HashSet<OnboardingPage>,
focus_area: FocusArea,
// Workspace reference for Item trait
@@ -147,6 +149,7 @@ pub struct OnboardingUI {
workspace_id: Option<WorkspaceId>,
client: Arc<Client>,
welcome_page: Option<Entity<WelcomePage>>,
_settings_subscription: Option<Subscription>,
}
impl OnboardingUI {}
@@ -179,6 +182,8 @@ impl Render for OnboardingUI {
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::toggle_focus))
.on_action(cx.listener(Self::handle_enable_ai_assistance))
.on_action(cx.listener(Self::handle_disable_ai_assistance))
.flex()
.items_center()
.justify_center()
@@ -218,31 +223,57 @@ impl Render for OnboardingUI {
impl OnboardingUI {
pub fn new(workspace: &Workspace, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify();
});
Self {
focus_handle: cx.focus_handle(),
current_page: OnboardingPage::Basics,
nav_focus: NavigationFocusItem::Basics,
page_focus: [PageFocusItem(0); 4],
completed_pages: [false; 4],
completed_pages: HashSet::new(),
focus_area: FocusArea::Navigation,
workspace: workspace.weak_handle(),
workspace_id: workspace.database_id(),
client,
welcome_page: None,
_settings_subscription: Some(settings_subscription),
}
}
fn completed_pages_to_string(&self) -> String {
self.completed_pages
.iter()
.map(|&completed| if completed { '1' } else { '0' })
.collect()
let mut result = String::new();
for i in 0..4 {
let page = match i {
0 => OnboardingPage::Basics,
1 => OnboardingPage::Editing,
2 => OnboardingPage::AiSetup,
3 => OnboardingPage::Welcome,
_ => unreachable!(),
};
result.push(if self.completed_pages.contains(&page) {
'1'
} else {
'0'
});
}
result
}
fn completed_pages_from_string(s: &str) -> [bool; 4] {
let mut result = [false; 4];
fn completed_pages_from_string(s: &str) -> HashSet<OnboardingPage> {
let mut result = HashSet::new();
for (i, ch) in s.chars().take(4).enumerate() {
result[i] = ch == '1';
if ch == '1' {
let page = match i {
0 => OnboardingPage::Basics,
1 => OnboardingPage::Editing,
2 => OnboardingPage::AiSetup,
3 => OnboardingPage::Welcome,
_ => continue,
};
result.insert(page);
}
}
result
}
@@ -275,7 +306,7 @@ impl OnboardingUI {
fn reset(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
self.current_page = OnboardingPage::Basics;
self.focus_area = FocusArea::Navigation;
self.completed_pages = [false; 4];
self.completed_pages = HashSet::new();
cx.notify();
}
@@ -460,13 +491,7 @@ impl OnboardingUI {
_window: &mut gpui::Window,
cx: &mut Context<Self>,
) {
let index = match page {
OnboardingPage::Basics => 0,
OnboardingPage::Editing => 1,
OnboardingPage::AiSetup => 2,
OnboardingPage::Welcome => 3,
};
self.completed_pages[index] = true;
self.completed_pages.insert(page);
cx.notify();
}
@@ -510,6 +535,40 @@ impl OnboardingUI {
self.next_page(window, cx);
}
fn handle_enable_ai_assistance(
&mut self,
_: &zed_actions::EnableAiAssistance,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(workspace) = self.workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.features
.get_or_insert(Default::default())
.ai_assistance = Some(true);
});
cx.notify();
}
}
fn handle_disable_ai_assistance(
&mut self,
_: &zed_actions::DisableAiAssistance,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(workspace) = self.workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.features
.get_or_insert(Default::default())
.ai_assistance = Some(false);
});
cx.notify();
}
}
fn handle_previous_page(
&mut self,
_: &PreviousPage,
@@ -626,7 +685,7 @@ impl OnboardingUI {
let area_focused = self.focus_area == FocusArea::Navigation;
Ring::new(corner_radius, item_focused)
FocusOutline::new(corner_radius, item_focused, px(2.))
.active(area_focused && item_focused)
.child(
h_flex()
@@ -1024,6 +1083,10 @@ impl OnboardingUI {
let focused_item = self.page_focus[page_index].0;
let is_page_focused = self.focus_area == FocusArea::PageContent;
let ai_enabled = ai_enabled(cx);
let workspace = self.workspace.clone();
v_flex()
.h_full()
.w_full()
@@ -1035,8 +1098,22 @@ impl OnboardingUI {
CheckboxWithLabel::new(
"disable_ai",
Label::new("Enable AI Features"),
ToggleState::Selected,
|_, _, cx| todo!("implement ai toggle"),
if ai_enabled {
ToggleState::Selected
} else {
ToggleState::Unselected
},
move |state, _, cx| {
let enabled = state == &ToggleState::Selected;
if let Some(workspace) = workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.features
.get_or_insert(Default::default())
.ai_assistance = Some(enabled);
});
}
},
)))
.child(
CalloutRow::new("We don't use your code to train AI models")
@@ -1148,7 +1225,7 @@ impl SerializableItem for OnboardingUI {
let completed = OnboardingUI::completed_pages_from_string(&completed_str);
(page, completed)
} else {
(OnboardingPage::Basics, [false; 4])
(OnboardingPage::Basics, HashSet::new())
};
cx.update(|window, cx| {

View File

@@ -42,6 +42,7 @@ client.workspace = true
collab_ui.workspace = true
collections.workspace = true
command_palette.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
copilot.workspace = true
dap_adapters.workspace = true

View File

@@ -16,6 +16,7 @@ use assets::Assets;
use breadcrumbs::Breadcrumbs;
use client::zed_urls;
use collections::VecDeque;
use command_palette_hooks::CommandPaletteFilter;
use debugger_ui::debugger_panel::DebugPanel;
use editor::ProposedChangesEditorToolbar;
use editor::{Editor, MultiBuffer};
@@ -52,6 +53,7 @@ use settings::{
Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
initial_project_settings_content, initial_tasks_content, update_settings_file,
};
use std::any::TypeId;
use std::path::PathBuf;
use std::sync::atomic::{self, AtomicBool};
use std::{borrow::Cow, path::Path, sync::Arc};
@@ -72,7 +74,8 @@ use workspace::{
use workspace::{CloseIntent, CloseWindow, RestoreBanner, with_active_or_new_workspace};
use workspace::{Pane, notifications::DetachAndPromptErr};
use zed_actions::{
OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
DisableAiAssistance, EnableAiAssistance, OpenAccountSettings, OpenBrowser, OpenDocs,
OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
};
actions!(
@@ -215,6 +218,51 @@ pub fn init(cx: &mut App) {
);
});
});
cx.on_action(|_: &EnableAiAssistance, cx| {
with_active_or_new_workspace(cx, |workspace, _, cx| {
let fs = workspace.app_state().fs.clone();
update_settings_file::<language::language_settings::AllLanguageSettings>(
fs,
cx,
move |file, _| {
file.features
.get_or_insert(Default::default())
.ai_assistance = Some(true);
},
);
});
});
cx.on_action(|_: &DisableAiAssistance, cx| {
with_active_or_new_workspace(cx, |workspace, _, cx| {
let fs = workspace.app_state().fs.clone();
update_settings_file::<language::language_settings::AllLanguageSettings>(
fs,
cx,
move |file, _| {
file.features
.get_or_insert(Default::default())
.ai_assistance = Some(false);
},
);
});
});
// Filter AI assistance actions based on current state
cx.observe_global::<SettingsStore>(move |cx| {
let ai_enabled =
language::language_settings::all_language_settings(None, cx).is_ai_assistance_enabled();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if ai_enabled {
filter.hide_action_types(&[TypeId::of::<EnableAiAssistance>()]);
filter.show_action_types([TypeId::of::<DisableAiAssistance>()].iter());
} else {
filter.show_action_types([TypeId::of::<EnableAiAssistance>()].iter());
filter.hide_action_types(&[TypeId::of::<DisableAiAssistance>()]);
}
});
})
.detach();
}
fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {

View File

@@ -50,6 +50,10 @@ actions!(
OpenLicenses,
/// Opens the telemetry log.
OpenTelemetryLog,
/// Enables AI assistance features.
EnableAiAssistance,
/// Disables AI assistance features.
DisableAiAssistance,
]
);