Add setting to disable all AI features (#34896)
https://github.com/user-attachments/assets/674bba41-40ac-4a98-99e4-0b47f9097b6a Release Notes: - Added setting to disable all AI features
This commit is contained in:
parent
939f9fffa3
commit
96f9942791
19 changed files with 308 additions and 68 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -210,6 +210,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
|
"command_palette_hooks",
|
||||||
"component",
|
"component",
|
||||||
"context_server",
|
"context_server",
|
||||||
"db",
|
"db",
|
||||||
|
@ -6360,6 +6361,7 @@ dependencies = [
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"call",
|
"call",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
"component",
|
"component",
|
||||||
|
|
|
@ -1076,6 +1076,10 @@
|
||||||
// Send anonymized usage data like what languages you're using Zed with.
|
// Send anonymized usage data like what languages you're using Zed with.
|
||||||
"metrics": true
|
"metrics": true
|
||||||
},
|
},
|
||||||
|
// Whether to disable all AI features in Zed.
|
||||||
|
//
|
||||||
|
// Default: false
|
||||||
|
"disable_ai": false,
|
||||||
// Automatically update Zed. This setting may be ignored on Linux if
|
// Automatically update Zed. This setting may be ignored on Linux if
|
||||||
// installed through a package manager.
|
// installed through a package manager.
|
||||||
"auto_update": true,
|
"auto_update": true,
|
||||||
|
|
|
@ -32,6 +32,7 @@ buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
|
command_palette_hooks.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
context_server.workspace = true
|
context_server.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
|
|
|
@ -43,7 +43,7 @@ use anyhow::{Result, anyhow};
|
||||||
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||||
use assistant_slash_command::SlashCommandWorkingSet;
|
use assistant_slash_command::SlashCommandWorkingSet;
|
||||||
use assistant_tool::ToolWorkingSet;
|
use assistant_tool::ToolWorkingSet;
|
||||||
use client::{UserStore, zed_urls};
|
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||||
use feature_flags::{self, FeatureFlagAppExt};
|
use feature_flags::{self, FeatureFlagAppExt};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
@ -744,6 +744,7 @@ impl AgentPanel {
|
||||||
if workspace
|
if workspace
|
||||||
.panel::<Self>(cx)
|
.panel::<Self>(cx)
|
||||||
.is_some_and(|panel| panel.read(cx).enabled(cx))
|
.is_some_and(|panel| panel.read(cx).enabled(cx))
|
||||||
|
&& !DisableAiSettings::get_global(cx).disable_ai
|
||||||
{
|
{
|
||||||
workspace.toggle_panel_focus::<Self>(window, cx);
|
workspace.toggle_panel_focus::<Self>(window, cx);
|
||||||
}
|
}
|
||||||
|
@ -1665,7 +1666,10 @@ impl Panel for AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||||
(self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
|
(self.enabled(cx)
|
||||||
|
&& AgentSettings::get_global(cx).button
|
||||||
|
&& !DisableAiSettings::get_global(cx).disable_ai)
|
||||||
|
.then_some(IconName::ZedAssistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
||||||
|
|
|
@ -31,7 +31,8 @@ use std::sync::Arc;
|
||||||
use agent::{Thread, ThreadId};
|
use agent::{Thread, ThreadId};
|
||||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||||
use assistant_slash_command::SlashCommandRegistry;
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
use client::Client;
|
use client::{Client, DisableAiSettings};
|
||||||
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{Action, App, Entity, actions};
|
use gpui::{Action, App, Entity, actions};
|
||||||
|
@ -43,6 +44,7 @@ use prompt_store::PromptBuilder;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
pub use crate::active_thread::ActiveThread;
|
pub use crate::active_thread::ActiveThread;
|
||||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||||
|
@ -52,6 +54,7 @@ use crate::slash_command_settings::SlashCommandSettings;
|
||||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||||
|
use zed_actions;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
agent,
|
agent,
|
||||||
|
@ -241,6 +244,66 @@ pub fn init(
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe_new(ManageProfilesModal::register).detach();
|
cx.observe_new(ManageProfilesModal::register).detach();
|
||||||
|
|
||||||
|
// Update command palette filter based on AI settings
|
||||||
|
update_command_palette_filter(cx);
|
||||||
|
|
||||||
|
// Watch for settings changes
|
||||||
|
cx.observe_global::<SettingsStore>(|app_cx| {
|
||||||
|
// When settings change, update the command palette filter
|
||||||
|
update_command_palette_filter(app_cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_command_palette_filter(cx: &mut App) {
|
||||||
|
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||||
|
if disable_ai {
|
||||||
|
filter.hide_namespace("agent");
|
||||||
|
filter.hide_namespace("assistant");
|
||||||
|
filter.hide_namespace("zed_predict_onboarding");
|
||||||
|
filter.hide_namespace("edit_prediction");
|
||||||
|
|
||||||
|
use editor::actions::{
|
||||||
|
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||||
|
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||||
|
};
|
||||||
|
let edit_prediction_actions = [
|
||||||
|
TypeId::of::<AcceptEditPrediction>(),
|
||||||
|
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||||
|
TypeId::of::<ShowEditPrediction>(),
|
||||||
|
TypeId::of::<NextEditPrediction>(),
|
||||||
|
TypeId::of::<PreviousEditPrediction>(),
|
||||||
|
TypeId::of::<ToggleEditPrediction>(),
|
||||||
|
];
|
||||||
|
filter.hide_action_types(&edit_prediction_actions);
|
||||||
|
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||||
|
} else {
|
||||||
|
filter.show_namespace("agent");
|
||||||
|
filter.show_namespace("assistant");
|
||||||
|
filter.show_namespace("zed_predict_onboarding");
|
||||||
|
|
||||||
|
filter.show_namespace("edit_prediction");
|
||||||
|
|
||||||
|
use editor::actions::{
|
||||||
|
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||||
|
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||||
|
};
|
||||||
|
let edit_prediction_actions = [
|
||||||
|
TypeId::of::<AcceptEditPrediction>(),
|
||||||
|
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||||
|
TypeId::of::<ShowEditPrediction>(),
|
||||||
|
TypeId::of::<NextEditPrediction>(),
|
||||||
|
TypeId::of::<PreviousEditPrediction>(),
|
||||||
|
TypeId::of::<ToggleEditPrediction>(),
|
||||||
|
];
|
||||||
|
filter.show_action_types(edit_prediction_actions.iter());
|
||||||
|
|
||||||
|
filter
|
||||||
|
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_language_model_settings(cx: &mut App) {
|
fn init_language_model_settings(cx: &mut App) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use agent::{
|
||||||
};
|
};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use client::{DisableAiSettings, telemetry::Telemetry};
|
||||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||||
use editor::SelectionEffects;
|
use editor::SelectionEffects;
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -57,6 +57,17 @@ pub fn init(
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
||||||
|
|
||||||
|
cx.observe_global::<SettingsStore>(|cx| {
|
||||||
|
if DisableAiSettings::get_global(cx).disable_ai {
|
||||||
|
// Hide any active inline assist UI when AI is disabled
|
||||||
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.cancel_all_active_completions(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
|
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
|
||||||
let Some(window) = window else {
|
let Some(window) = window else {
|
||||||
return;
|
return;
|
||||||
|
@ -141,6 +152,26 @@ impl InlineAssistant {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hides all active inline assists when AI is disabled
|
||||||
|
pub fn cancel_all_active_completions(&mut self, cx: &mut App) {
|
||||||
|
// Cancel all active completions in editors
|
||||||
|
for (editor_handle, _) in self.assists_by_editor.iter() {
|
||||||
|
if let Some(editor) = editor_handle.upgrade() {
|
||||||
|
let windows = cx.windows();
|
||||||
|
if !windows.is_empty() {
|
||||||
|
let window = windows[0];
|
||||||
|
let _ = window.update(cx, |_, window, cx| {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
if editor.has_active_inline_completion() {
|
||||||
|
editor.cancel(&Default::default(), window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_workspace_event(
|
fn handle_workspace_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: Entity<Workspace>,
|
workspace: Entity<Workspace>,
|
||||||
|
@ -176,7 +207,7 @@ impl InlineAssistant {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let is_assistant2_enabled = true;
|
let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
|
||||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
|
@ -199,6 +230,13 @@ impl InlineAssistant {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if DisableAiSettings::get_global(cx).disable_ai {
|
||||||
|
// Cancel any active completions
|
||||||
|
if editor.has_active_inline_completion() {
|
||||||
|
editor.cancel(&Default::default(), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the Assistant1 code action provider, as it still might be registered.
|
// Remove the Assistant1 code action provider, as it still might be registered.
|
||||||
editor.remove_code_action_provider("assistant".into(), window, cx);
|
editor.remove_code_action_provider("assistant".into(), window, cx);
|
||||||
} else {
|
} else {
|
||||||
|
@ -219,7 +257,7 @@ impl InlineAssistant {
|
||||||
cx: &mut Context<Workspace>,
|
cx: &mut Context<Workspace>,
|
||||||
) {
|
) {
|
||||||
let settings = AgentSettings::get_global(cx);
|
let settings = AgentSettings::get_global(cx);
|
||||||
if !settings.enabled {
|
if !settings.enabled || DisableAiSettings::get_global(cx).disable_ai {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ anyhow.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
|
|
@ -20,14 +20,13 @@ mod thinking_tool;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod web_search_tool;
|
mod web_search_tool;
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use assistant_tool::ToolRegistry;
|
use assistant_tool::ToolRegistry;
|
||||||
use copy_path_tool::CopyPathTool;
|
use copy_path_tool::CopyPathTool;
|
||||||
use gpui::{App, Entity};
|
use gpui::{App, Entity};
|
||||||
use http_client::HttpClientWithUrl;
|
use http_client::HttpClientWithUrl;
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use move_path_tool::MovePathTool;
|
use move_path_tool::MovePathTool;
|
||||||
|
use std::sync::Arc;
|
||||||
use web_search_tool::WebSearchTool;
|
use web_search_tool::WebSearchTool;
|
||||||
|
|
||||||
pub(crate) use templates::*;
|
pub(crate) use templates::*;
|
||||||
|
|
|
@ -151,6 +151,7 @@ impl Settings for ProxySettings {
|
||||||
|
|
||||||
pub fn init_settings(cx: &mut App) {
|
pub fn init_settings(cx: &mut App) {
|
||||||
TelemetrySettings::register(cx);
|
TelemetrySettings::register(cx);
|
||||||
|
DisableAiSettings::register(cx);
|
||||||
ClientSettings::register(cx);
|
ClientSettings::register(cx);
|
||||||
ProxySettings::register(cx);
|
ProxySettings::register(cx);
|
||||||
}
|
}
|
||||||
|
@ -548,6 +549,33 @@ impl settings::Settings for TelemetrySettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to disable all AI features in Zed.
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct DisableAiSettings {
|
||||||
|
pub disable_ai: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl settings::Settings for DisableAiSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("disable_ai");
|
||||||
|
|
||||||
|
type FileContent = Option<bool>;
|
||||||
|
|
||||||
|
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
disable_ai: sources
|
||||||
|
.user
|
||||||
|
.or(sources.server)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||||
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
clock: Arc<dyn SystemClock>,
|
clock: Arc<dyn SystemClock>,
|
||||||
|
|
|
@ -6,6 +6,7 @@ mod sign_in;
|
||||||
use crate::sign_in::initiate_sign_in_within_workspace;
|
use crate::sign_in::initiate_sign_in_within_workspace;
|
||||||
use ::fs::Fs;
|
use ::fs::Fs;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
|
use client::DisableAiSettings;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
|
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
|
||||||
|
@ -25,6 +26,7 @@ use node_runtime::NodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use request::StatusNotification;
|
use request::StatusNotification;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use settings::Settings;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
@ -93,26 +95,34 @@ pub fn init(
|
||||||
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
||||||
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
||||||
let status = handle.read(cx).status();
|
let status = handle.read(cx).status();
|
||||||
|
|
||||||
|
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
let filter = CommandPaletteFilter::global_mut(cx);
|
let filter = CommandPaletteFilter::global_mut(cx);
|
||||||
|
|
||||||
match status {
|
if is_ai_disabled {
|
||||||
Status::Disabled => {
|
filter.hide_action_types(&copilot_action_types);
|
||||||
filter.hide_action_types(&copilot_action_types);
|
filter.hide_action_types(&copilot_auth_action_types);
|
||||||
filter.hide_action_types(&copilot_auth_action_types);
|
filter.hide_action_types(&copilot_no_auth_action_types);
|
||||||
filter.hide_action_types(&copilot_no_auth_action_types);
|
} else {
|
||||||
}
|
match status {
|
||||||
Status::Authorized => {
|
Status::Disabled => {
|
||||||
filter.hide_action_types(&copilot_no_auth_action_types);
|
filter.hide_action_types(&copilot_action_types);
|
||||||
filter.show_action_types(
|
filter.hide_action_types(&copilot_auth_action_types);
|
||||||
copilot_action_types
|
filter.hide_action_types(&copilot_no_auth_action_types);
|
||||||
.iter()
|
}
|
||||||
.chain(&copilot_auth_action_types),
|
Status::Authorized => {
|
||||||
);
|
filter.hide_action_types(&copilot_no_auth_action_types);
|
||||||
}
|
filter.show_action_types(
|
||||||
_ => {
|
copilot_action_types
|
||||||
filter.hide_action_types(&copilot_action_types);
|
.iter()
|
||||||
filter.hide_action_types(&copilot_auth_action_types);
|
.chain(&copilot_auth_action_types),
|
||||||
filter.show_action_types(copilot_no_auth_action_types.iter());
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
filter.hide_action_types(&copilot_action_types);
|
||||||
|
filter.hide_action_types(&copilot_auth_action_types);
|
||||||
|
filter.show_action_types(copilot_no_auth_action_types.iter());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,6 +23,7 @@ askpass.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
call.workspace = true
|
call.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::branch_picker::{self, BranchList};
|
use crate::branch_picker::{self, BranchList};
|
||||||
use crate::git_panel::{GitPanel, commit_message_editor};
|
use crate::git_panel::{GitPanel, commit_message_editor};
|
||||||
|
use client::DisableAiSettings;
|
||||||
use git::repository::CommitOptions;
|
use git::repository::CommitOptions;
|
||||||
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
||||||
use panel::{panel_button, panel_editor_style};
|
use panel::{panel_button, panel_editor_style};
|
||||||
|
use settings::Settings;
|
||||||
use ui::{
|
use ui::{
|
||||||
ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
|
ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
|
@ -569,11 +571,13 @@ impl Render for CommitModal {
|
||||||
.on_action(cx.listener(Self::dismiss))
|
.on_action(cx.listener(Self::dismiss))
|
||||||
.on_action(cx.listener(Self::commit))
|
.on_action(cx.listener(Self::commit))
|
||||||
.on_action(cx.listener(Self::amend))
|
.on_action(cx.listener(Self::amend))
|
||||||
.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
|
.when(!DisableAiSettings::get_global(cx).disable_ai, |this| {
|
||||||
this.git_panel.update(cx, |panel, cx| {
|
this.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
|
||||||
panel.generate_commit_message(cx);
|
this.git_panel.update(cx, |panel, cx| {
|
||||||
})
|
panel.generate_commit_message(cx);
|
||||||
}))
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
||||||
this.toggle_branch_selector(window, cx);
|
this.toggle_branch_selector(window, cx);
|
||||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use askpass::AskPassDelegate;
|
use askpass::AskPassDelegate;
|
||||||
|
use client::DisableAiSettings;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{
|
use editor::{
|
||||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||||
|
@ -53,7 +54,7 @@ use project::{
|
||||||
git_store::{GitStoreEvent, Repository},
|
git_store::{GitStoreEvent, Repository},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -464,9 +465,14 @@ impl GitPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
|
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
|
||||||
|
let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
|
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
|
||||||
if assistant_enabled != AgentSettings::get_global(cx).enabled {
|
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
if assistant_enabled != AgentSettings::get_global(cx).enabled
|
||||||
|
|| was_ai_disabled != is_ai_disabled
|
||||||
|
{
|
||||||
assistant_enabled = AgentSettings::get_global(cx).enabled;
|
assistant_enabled = AgentSettings::get_global(cx).enabled;
|
||||||
|
was_ai_disabled = is_ai_disabled;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1806,7 +1812,7 @@ impl GitPanel {
|
||||||
|
|
||||||
/// Generates a commit message using an LLM.
|
/// Generates a commit message using an LLM.
|
||||||
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
|
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
|
||||||
if !self.can_commit() {
|
if !self.can_commit() || DisableAiSettings::get_global(cx).disable_ai {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4305,8 +4311,10 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
||||||
agent_settings::AgentSettings::get_global(cx)
|
let is_enabled = agent_settings::AgentSettings::get_global(cx).enabled
|
||||||
.enabled
|
&& !DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
|
||||||
|
is_enabled
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let ConfiguredModel { provider, model } =
|
let ConfiguredModel { provider, model } =
|
||||||
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
||||||
|
@ -5037,6 +5045,7 @@ mod tests {
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
client::DisableAiSettings::register(cx);
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{UserStore, zed_urls};
|
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||||
use copilot::{Copilot, Status};
|
use copilot::{Copilot, Status};
|
||||||
use editor::{
|
use editor::{
|
||||||
Editor, SelectionEffects,
|
Editor, SelectionEffects,
|
||||||
|
@ -72,6 +72,11 @@ enum SupermavenButtonStatus {
|
||||||
|
|
||||||
impl Render for InlineCompletionButton {
|
impl Render for InlineCompletionButton {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
// Return empty div if AI is disabled
|
||||||
|
if DisableAiSettings::get_global(cx).disable_ai {
|
||||||
|
return div();
|
||||||
|
}
|
||||||
|
|
||||||
let all_language_settings = all_language_settings(None, cx);
|
let all_language_settings = all_language_settings(None, cx);
|
||||||
|
|
||||||
match all_language_settings.edit_predictions.provider {
|
match all_language_settings.edit_predictions.provider {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use client::{TelemetrySettings, telemetry::Telemetry};
|
use client::{DisableAiSettings, TelemetrySettings, telemetry::Telemetry};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||||
|
@ -174,23 +174,25 @@ impl Render for WelcomePage {
|
||||||
.ok();
|
.ok();
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.when(!DisableAiSettings::get_global(cx).disable_ai, |parent| {
|
||||||
Button::new(
|
parent.child(
|
||||||
"try-zed-edit-prediction",
|
Button::new(
|
||||||
edit_prediction_label,
|
"edit_prediction_onboarding",
|
||||||
|
edit_prediction_label,
|
||||||
|
)
|
||||||
|
.disabled(edit_prediction_provider_is_zed)
|
||||||
|
.icon(IconName::ZedPredict)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|_, _, window, cx| {
|
||||||
|
telemetry::event!("Welcome Screen Try Edit Prediction clicked");
|
||||||
|
window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.disabled(edit_prediction_provider_is_zed)
|
})
|
||||||
.icon(IconName::ZedPredict)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.icon_position(IconPosition::Start)
|
|
||||||
.on_click(
|
|
||||||
cx.listener(|_, _, window, cx| {
|
|
||||||
telemetry::event!("Welcome Screen Try Edit Prediction clicked");
|
|
||||||
window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("edit settings", "Edit Settings")
|
Button::new("edit settings", "Edit Settings")
|
||||||
.icon(IconName::Settings)
|
.icon(IconName::Settings)
|
||||||
|
|
|
@ -242,6 +242,7 @@ struct PanelEntry {
|
||||||
|
|
||||||
pub struct PanelButtons {
|
pub struct PanelButtons {
|
||||||
dock: Entity<Dock>,
|
dock: Entity<Dock>,
|
||||||
|
_settings_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dock {
|
impl Dock {
|
||||||
|
@ -373,6 +374,12 @@ impl Dock {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn first_enabled_panel_idx_excluding(&self, exclude_name: &str, cx: &App) -> Option<usize> {
|
||||||
|
self.panel_entries.iter().position(|entry| {
|
||||||
|
entry.panel.persistent_name() != exclude_name && entry.panel.enabled(cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn active_panel_entry(&self) -> Option<&PanelEntry> {
|
fn active_panel_entry(&self) -> Option<&PanelEntry> {
|
||||||
self.active_panel_index
|
self.active_panel_index
|
||||||
.and_then(|index| self.panel_entries.get(index))
|
.and_then(|index| self.panel_entries.get(index))
|
||||||
|
@ -833,7 +840,11 @@ impl Render for Dock {
|
||||||
impl PanelButtons {
|
impl PanelButtons {
|
||||||
pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
|
pub fn new(dock: Entity<Dock>, cx: &mut Context<Self>) -> Self {
|
||||||
cx.observe(&dock, |_, _, cx| cx.notify()).detach();
|
cx.observe(&dock, |_, _, cx| cx.notify()).detach();
|
||||||
Self { dock }
|
let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
|
||||||
|
Self {
|
||||||
|
dock,
|
||||||
|
_settings_subscription: settings_subscription,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -554,6 +554,7 @@ pub fn main() {
|
||||||
supermaven::init(app_state.client.clone(), cx);
|
supermaven::init(app_state.client.clone(), cx);
|
||||||
language_model::init(app_state.client.clone(), cx);
|
language_model::init(app_state.client.clone(), cx);
|
||||||
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
||||||
|
agent_settings::init(cx);
|
||||||
agent_servers::init(cx);
|
agent_servers::init(cx);
|
||||||
web_search::init(cx);
|
web_search::init(cx);
|
||||||
web_search_providers::init(app_state.client.clone(), cx);
|
web_search_providers::init(app_state.client.clone(), cx);
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod preview;
|
||||||
mod repl_menu;
|
mod repl_menu;
|
||||||
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
|
use client::DisableAiSettings;
|
||||||
use editor::actions::{
|
use editor::actions::{
|
||||||
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
||||||
GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
|
GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
|
||||||
|
@ -32,6 +33,7 @@ const MAX_CODE_ACTION_MENU_LINES: u32 = 16;
|
||||||
|
|
||||||
pub struct QuickActionBar {
|
pub struct QuickActionBar {
|
||||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||||
|
_ai_settings_subscription: Subscription,
|
||||||
active_item: Option<Box<dyn ItemHandle>>,
|
active_item: Option<Box<dyn ItemHandle>>,
|
||||||
buffer_search_bar: Entity<BufferSearchBar>,
|
buffer_search_bar: Entity<BufferSearchBar>,
|
||||||
show: bool,
|
show: bool,
|
||||||
|
@ -46,8 +48,28 @@ impl QuickActionBar {
|
||||||
workspace: &Workspace,
|
workspace: &Workspace,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
let mut was_agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||||
|
let mut was_agent_button = AgentSettings::get_global(cx).button;
|
||||||
|
|
||||||
|
let ai_settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
|
||||||
|
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
let agent_settings = AgentSettings::get_global(cx);
|
||||||
|
|
||||||
|
if was_ai_disabled != is_ai_disabled
|
||||||
|
|| was_agent_enabled != agent_settings.enabled
|
||||||
|
|| was_agent_button != agent_settings.button
|
||||||
|
{
|
||||||
|
was_ai_disabled = is_ai_disabled;
|
||||||
|
was_agent_enabled = agent_settings.enabled;
|
||||||
|
was_agent_button = agent_settings.button;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
_inlay_hints_enabled_subscription: None,
|
_inlay_hints_enabled_subscription: None,
|
||||||
|
_ai_settings_subscription: ai_settings_subscription,
|
||||||
active_item: None,
|
active_item: None,
|
||||||
buffer_search_bar,
|
buffer_search_bar,
|
||||||
show: true,
|
show: true,
|
||||||
|
@ -575,7 +597,9 @@ impl Render for QuickActionBar {
|
||||||
.children(self.render_preview_button(self.workspace.clone(), cx))
|
.children(self.render_preview_button(self.workspace.clone(), cx))
|
||||||
.children(search_button)
|
.children(search_button)
|
||||||
.when(
|
.when(
|
||||||
AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
|
AgentSettings::get_global(cx).enabled
|
||||||
|
&& AgentSettings::get_global(cx).button
|
||||||
|
&& !DisableAiSettings::get_global(cx).disable_ai,
|
||||||
|bar| bar.child(assistant_button),
|
|bar| bar.child(assistant_button),
|
||||||
)
|
)
|
||||||
.children(code_actions_dropdown)
|
.children(code_actions_dropdown)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
|
use client::DisableAiSettings;
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
|
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
|
||||||
use gpui::actions;
|
use gpui::actions;
|
||||||
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
|
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
|
||||||
use settings::update_settings_file;
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use ui::App;
|
use ui::App;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ actions!(
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
|
feature_gate_predict_edits_actions(cx);
|
||||||
|
|
||||||
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
||||||
workspace.register_action(|workspace, _: &RateCompletions, window, cx| {
|
workspace.register_action(|workspace, _: &RateCompletions, window, cx| {
|
||||||
if cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>() {
|
if cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>() {
|
||||||
|
@ -53,27 +56,57 @@ pub fn init(cx: &mut App) {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
feature_gate_predict_edits_rating_actions(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn feature_gate_predict_edits_rating_actions(cx: &mut App) {
|
fn feature_gate_predict_edits_actions(cx: &mut App) {
|
||||||
let rate_completion_action_types = [TypeId::of::<RateCompletions>()];
|
let rate_completion_action_types = [TypeId::of::<RateCompletions>()];
|
||||||
|
let reset_onboarding_action_types = [TypeId::of::<ResetOnboarding>()];
|
||||||
|
let zeta_all_action_types = [
|
||||||
|
TypeId::of::<RateCompletions>(),
|
||||||
|
TypeId::of::<ResetOnboarding>(),
|
||||||
|
zed_actions::OpenZedPredictOnboarding.type_id(),
|
||||||
|
TypeId::of::<crate::ClearHistory>(),
|
||||||
|
TypeId::of::<crate::ThumbsUpActiveCompletion>(),
|
||||||
|
TypeId::of::<crate::ThumbsDownActiveCompletion>(),
|
||||||
|
TypeId::of::<crate::NextEdit>(),
|
||||||
|
TypeId::of::<crate::PreviousEdit>(),
|
||||||
|
];
|
||||||
|
|
||||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||||
filter.hide_action_types(&rate_completion_action_types);
|
filter.hide_action_types(&rate_completion_action_types);
|
||||||
|
filter.hide_action_types(&reset_onboarding_action_types);
|
||||||
filter.hide_action_types(&[zed_actions::OpenZedPredictOnboarding.type_id()]);
|
filter.hide_action_types(&[zed_actions::OpenZedPredictOnboarding.type_id()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.observe_global::<SettingsStore>(move |cx| {
|
||||||
|
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
let has_feature_flag = cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>();
|
||||||
|
|
||||||
|
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||||
|
if is_ai_disabled {
|
||||||
|
filter.hide_action_types(&zeta_all_action_types);
|
||||||
|
} else {
|
||||||
|
if has_feature_flag {
|
||||||
|
filter.show_action_types(rate_completion_action_types.iter());
|
||||||
|
} else {
|
||||||
|
filter.hide_action_types(&rate_completion_action_types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe_flag::<PredictEditsRateCompletionsFeatureFlag, _>(move |is_enabled, cx| {
|
cx.observe_flag::<PredictEditsRateCompletionsFeatureFlag, _>(move |is_enabled, cx| {
|
||||||
if is_enabled {
|
if !DisableAiSettings::get_global(cx).disable_ai {
|
||||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
if is_enabled {
|
||||||
filter.show_action_types(rate_completion_action_types.iter());
|
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||||
});
|
filter.show_action_types(rate_completion_action_types.iter());
|
||||||
} else {
|
});
|
||||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
} else {
|
||||||
filter.hide_action_types(&rate_completion_action_types);
|
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||||
});
|
filter.hide_action_types(&rate_completion_action_types);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue