diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index c9e73372f60686cf330531926f4129e9c9b25db8..76f0199afb170e9843f32d2c3cd93d0a0c7b1b5e 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -52,7 +52,7 @@ impl AgentProfile { update_settings_file::(fs, cx, { let id = id.clone(); move |settings, _cx| { - settings.create_profile(id, profile_settings).log_err(); + profile_settings.save_to_settings(id, settings).log_err(); } }); diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 42a273e2dcfccfb839e6e7d97efb42dcd7b0bba9..2d39fe85adb92ce67006f4e0c40e8be817222f6a 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -1,15 +1,17 @@ use std::sync::Arc; +use anyhow::{Result, bail}; use collections::IndexMap; use convert_case::{Case, Casing as _}; use fs::Fs; use gpui::{App, SharedString}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings as _, update_settings_file}; +use settings::{ + AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent, + update_settings_file, +}; use util::ResultExt as _; -use crate::AgentSettings; +use crate::{AgentProfileId, AgentSettings}; pub mod builtin_profiles { use super::AgentProfileId; @@ -23,27 +25,6 @@ pub mod builtin_profiles { } } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AgentProfileId(pub Arc); - -impl AgentProfileId { - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Display for AgentProfileId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for AgentProfileId { - fn default() -> Self { - Self("write".into()) - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct AgentProfile { id: AgentProfileId, @@ -87,10 +68,10 @@ impl AgentProfile { .unwrap_or_default(), }; - update_settings_file::(fs, cx, { + update_settings_file(fs, cx, { let id = id.clone(); move |settings, _cx| { - settings.create_profile(id, profile_settings).log_err(); + profile_settings.save_to_settings(id, settings).log_err(); } }); @@ -129,9 +110,71 @@ impl AgentProfileSettings { .get(server_id) .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true)) } + + pub fn save_to_settings( + &self, + profile_id: AgentProfileId, + content: &mut SettingsContent, + ) -> Result<()> { + let profiles = content + .agent + .get_or_insert_default() + .profiles + .get_or_insert_default(); + if profiles.contains_key(&profile_id.0) { + bail!("profile with ID '{profile_id}' already exists"); + } + + profiles.insert( + profile_id.0.clone(), + AgentProfileContent { + name: self.name.clone().into(), + tools: self.tools.clone(), + enable_all_context_servers: Some(self.enable_all_context_servers), + context_servers: self + .context_servers + .clone() + .into_iter() + .map(|(server_id, preset)| { + ( + server_id, + ContextServerPresetContent { + tools: preset.tools, + }, + ) + }) + .collect(), + }, + ); + + Ok(()) + } +} + +impl From for AgentProfileSettings { + fn from(content: AgentProfileContent) -> Self { + Self { + name: content.name.into(), + tools: content.tools, + enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(), + context_servers: content + .context_servers + .into_iter() + .map(|(server_id, preset)| (server_id, preset.into())) + .collect(), + } + } } #[derive(Debug, Clone, Default)] pub struct ContextServerPreset { pub tools: IndexMap, bool>, } + +impl From for ContextServerPreset { + fn from(content: settings::ContextServerPresetContent) -> Self { + Self { + tools: content.tools, + } + } +} diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index e850945a40f46f31543fad2631216139706b405a..546594e663f642559bc9fbdba794ec574bcfd495 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -2,14 +2,16 @@ mod agent_profile; use std::sync::Arc; -use anyhow::{Result, bail}; use collections::IndexMap; -use gpui::{App, Pixels, SharedString}; +use gpui::{App, Pixels, px}; use language_model::LanguageModel; -use schemars::{JsonSchema, json_schema}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; -use std::borrow::Cow; +use settings::{ + AgentDockPosition, DefaultAgentView, LanguageModelParameters, LanguageModelSelection, + NotifyWhenAgentWaiting, Settings, SettingsContent, +}; +use util::MergeFrom; pub use crate::agent_profile::*; @@ -22,32 +24,6 @@ pub fn init(cx: &mut App) { AgentSettings::register(cx); } -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AgentDockPosition { - Left, - #[default] - Right, - Bottom, -} - -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum DefaultView { - #[default] - Thread, - TextThread, -} - -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum NotifyWhenAgentWaiting { - #[default] - PrimaryScreen, - AllScreens, - Never, -} - #[derive(Default, Clone, Debug)] pub struct AgentSettings { pub enabled: bool, @@ -60,9 +36,8 @@ pub struct AgentSettings { pub commit_message_model: Option, pub thread_summary_model: Option, pub inline_alternatives: Vec, - pub using_outdated_settings_version: bool, pub default_profile: AgentProfileId, - pub default_view: DefaultView, + pub default_view: DefaultAgentView, pub profiles: IndexMap, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, @@ -80,71 +55,20 @@ pub struct AgentSettings { impl AgentSettings { pub fn temperature_for_model(model: &Arc, cx: &App) -> Option { let settings = Self::get_global(cx); - settings - .model_parameters - .iter() - .rfind(|setting| setting.matches(model)) - .and_then(|m| m.temperature) - } - - pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - self.inline_assistant_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_commit_message_model(&mut self, provider: String, model: String) { - self.commit_message_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - self.thread_summary_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct LanguageModelParameters { - pub provider: Option, - pub model: Option, - pub temperature: Option, -} - -impl LanguageModelParameters { - pub fn matches(&self, model: &Arc) -> bool { - if let Some(provider) = &self.provider - && provider.0 != model.provider_id().0 - { - return false; - } - if let Some(setting_model) = &self.model - && *setting_model != model.id().0 - { - return false; + for setting in settings.model_parameters.iter().rev() { + if let Some(provider) = &setting.provider + && provider.0 != model.provider_id().0 + { + continue; + } + if let Some(setting_model) = &setting.model + && *setting_model != model.id().0 + { + continue; + } + return setting.temperature; } - true - } -} - -impl AgentSettingsContent { - pub fn set_dock(&mut self, dock: AgentDockPosition) { - self.dock = Some(dock); - } - - pub fn set_model(&mut self, language_model: Arc) { - let model = language_model.id().0.to_string(); - let provider = language_model.provider_id().0.to_string(); - - self.default_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); + return None; } pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { @@ -167,159 +91,6 @@ impl AgentSettingsContent { model, }); } - - pub fn set_always_allow_tool_actions(&mut self, allow: bool) { - self.always_allow_tool_actions = Some(allow); - } - - pub fn set_play_sound_when_agent_done(&mut self, allow: bool) { - self.play_sound_when_agent_done = Some(allow); - } - - pub fn set_single_file_review(&mut self, allow: bool) { - self.single_file_review = Some(allow); - } - - pub fn set_use_modifier_to_send(&mut self, always_use: bool) { - self.use_modifier_to_send = Some(always_use); - } - - pub fn set_profile(&mut self, profile_id: AgentProfileId) { - self.default_profile = Some(profile_id); - } - - pub fn create_profile( - &mut self, - profile_id: AgentProfileId, - profile_settings: AgentProfileSettings, - ) -> Result<()> { - let profiles = self.profiles.get_or_insert_default(); - if profiles.contains_key(&profile_id) { - bail!("profile with ID '{profile_id}' already exists"); - } - - profiles.insert( - profile_id, - AgentProfileContent { - name: profile_settings.name.into(), - tools: profile_settings.tools, - enable_all_context_servers: Some(profile_settings.enable_all_context_servers), - context_servers: profile_settings - .context_servers - .into_iter() - .map(|(server_id, preset)| { - ( - server_id, - ContextServerPresetContent { - tools: preset.tools, - }, - ) - }) - .collect(), - }, - ); - - Ok(()) - } -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)] -#[settings_key(key = "agent", fallback_key = "assistant")] -pub struct AgentSettingsContent { - /// Whether the Agent is enabled. - /// - /// Default: true - enabled: Option, - /// Whether to show the agent panel button in the status bar. - /// - /// Default: true - button: Option, - /// Where to dock the agent panel. - /// - /// Default: right - dock: Option, - /// Default width in pixels when the agent panel is docked to the left or right. - /// - /// Default: 640 - default_width: Option, - /// Default height in pixels when the agent panel is docked to the bottom. - /// - /// Default: 320 - default_height: Option, - /// The default model to use when creating new chats and for other features when a specific model is not specified. - default_model: Option, - /// Model to use for the inline assistant. Defaults to default_model when not specified. - inline_assistant_model: Option, - /// Model to use for generating git commit messages. Defaults to default_model when not specified. - commit_message_model: Option, - /// Model to use for generating thread summaries. Defaults to default_model when not specified. - thread_summary_model: Option, - /// Additional models with which to generate alternatives when performing inline assists. - inline_alternatives: Option>, - /// The default profile to use in the Agent. - /// - /// Default: write - default_profile: Option, - /// Which view type to show by default in the agent panel. - /// - /// Default: "thread" - default_view: Option, - /// The available agent profiles. - pub profiles: Option>, - /// Whenever a tool action would normally wait for your confirmation - /// that you allow it, always choose to allow it. - /// - /// This setting has no effect on external agents that support permission modes, such as Claude Code. - /// - /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code. - /// - /// Default: false - always_allow_tool_actions: Option, - /// Where to show a popup notification when the agent is waiting for user input. - /// - /// Default: "primary_screen" - notify_when_agent_waiting: Option, - /// Whether to play a sound when the agent has either completed its response, or needs user input. - /// - /// Default: false - play_sound_when_agent_done: Option, - /// Whether to stream edits from the agent as they are received. - /// - /// Default: false - stream_edits: Option, - /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. - /// - /// Default: true - single_file_review: Option, - /// Additional parameters for language model requests. When making a request - /// to a model, parameters will be taken from the last entry in this list - /// that matches the model's provider and name. In each entry, both provider - /// and model are optional, so that you can specify parameters for either - /// one. - /// - /// Default: [] - #[serde(default)] - model_parameters: Vec, - /// What completion mode to enable for new threads - /// - /// Default: normal - preferred_completion_mode: Option, - /// Whether to show thumb buttons for feedback in the agent panel. - /// - /// Default: true - enable_feedback: Option, - /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. - /// - /// Default: true - expand_edit_card: Option, - /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. - /// - /// Default: true - expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. - /// - /// Default: false - use_modifier_to_send: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] @@ -340,202 +111,136 @@ impl From for cloud_llm_client::CompletionMode { } } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct LanguageModelSelection { - pub provider: LanguageModelProviderSetting, - pub model: String, +impl From for CompletionMode { + fn from(value: settings::CompletionMode) -> Self { + match value { + settings::CompletionMode::Normal => CompletionMode::Normal, + settings::CompletionMode::Burn => CompletionMode::Burn, + } + } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct LanguageModelProviderSetting(pub String); - -impl JsonSchema for LanguageModelProviderSetting { - fn schema_name() -> Cow<'static, str> { - "LanguageModelProviderSetting".into() - } +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] +pub struct AgentProfileId(pub Arc); - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "enum": [ - "amazon-bedrock", - "anthropic", - "copilot_chat", - "deepseek", - "google", - "lmstudio", - "mistral", - "ollama", - "openai", - "openrouter", - "vercel", - "x_ai", - "zed.dev" - ] - }) +impl AgentProfileId { + pub fn as_str(&self) -> &str { + &self.0 } } -impl From for LanguageModelProviderSetting { - fn from(provider: String) -> Self { - Self(provider) +impl std::fmt::Display for AgentProfileId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } -impl From<&str> for LanguageModelProviderSetting { - fn from(provider: &str) -> Self { - Self(provider.to_string()) +impl Default for AgentProfileId { + fn default() -> Self { + Self("write".into()) } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AgentProfileContent { - pub name: Arc, - #[serde(default)] - pub tools: IndexMap, bool>, - /// Whether all context servers are enabled by default. - pub enable_all_context_servers: Option, - #[serde(default)] - pub context_servers: IndexMap, ContextServerPresetContent>, -} - -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] -pub struct ContextServerPresetContent { - pub tools: IndexMap, bool>, -} - impl Settings for AgentSettings { - const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]); - - type FileContent = AgentSettingsContent; - - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - let mut settings = AgentSettings::default(); + // todo!() test preserved keys logic + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let agent = content.agent.clone().unwrap(); + Self { + enabled: agent.enabled.unwrap(), + button: agent.button.unwrap(), + dock: agent.dock.unwrap(), + default_width: px(agent.default_width.unwrap()), + default_height: px(agent.default_height.unwrap()), + default_model: Some(agent.default_model.unwrap()), + inline_assistant_model: agent.inline_assistant_model, + commit_message_model: agent.commit_message_model, + thread_summary_model: agent.thread_summary_model, + inline_alternatives: agent.inline_alternatives.unwrap_or_default(), + default_profile: AgentProfileId(agent.default_profile.unwrap()), + default_view: agent.default_view.unwrap(), + profiles: agent + .profiles + .unwrap() + .into_iter() + .map(|(key, val)| (AgentProfileId(key), val.into())) + .collect(), + always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), + notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), + play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), + stream_edits: agent.stream_edits.unwrap(), + single_file_review: agent.single_file_review.unwrap(), + model_parameters: agent.model_parameters, + preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(), + enable_feedback: agent.enable_feedback.unwrap(), + expand_edit_card: agent.expand_edit_card.unwrap(), + expand_terminal_card: agent.expand_terminal_card.unwrap(), + use_modifier_to_send: agent.use_modifier_to_send.unwrap(), + } + } - for value in sources.defaults_and_customizations() { - merge(&mut settings.enabled, value.enabled); - merge(&mut settings.button, value.button); - merge(&mut settings.dock, value.dock); - merge( - &mut settings.default_width, - value.default_width.map(Into::into), - ); - merge( - &mut settings.default_height, - value.default_height.map(Into::into), - ); - settings.default_model = value - .default_model - .clone() - .or(settings.default_model.take()); - settings.inline_assistant_model = value - .inline_assistant_model - .clone() - .or(settings.inline_assistant_model.take()); - settings.commit_message_model = value - .clone() - .commit_message_model - .or(settings.commit_message_model.take()); - settings.thread_summary_model = value - .clone() - .thread_summary_model - .or(settings.thread_summary_model.take()); - merge( - &mut settings.inline_alternatives, - value.inline_alternatives.clone(), - ); - merge( - &mut settings.notify_when_agent_waiting, - value.notify_when_agent_waiting, - ); - merge( - &mut settings.play_sound_when_agent_done, - value.play_sound_when_agent_done, - ); - merge(&mut settings.stream_edits, value.stream_edits); - merge(&mut settings.single_file_review, value.single_file_review); - merge(&mut settings.default_profile, value.default_profile.clone()); - merge(&mut settings.default_view, value.default_view); - merge( - &mut settings.preferred_completion_mode, - value.preferred_completion_mode, - ); - merge(&mut settings.enable_feedback, value.enable_feedback); - merge(&mut settings.expand_edit_card, value.expand_edit_card); - merge( - &mut settings.expand_terminal_card, - value.expand_terminal_card, - ); - merge( - &mut settings.use_modifier_to_send, - value.use_modifier_to_send, + fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + let Some(value) = &content.agent else { return }; + self.enabled.merge_from(&value.enabled); + self.button.merge_from(&value.button); + self.dock.merge_from(&value.dock); + self.default_width + .merge_from(&value.default_width.map(Into::into)); + self.default_height + .merge_from(&value.default_height.map(Into::into)); + self.default_model = value.default_model.clone().or(self.default_model.take()); + + self.inline_assistant_model = value + .inline_assistant_model + .clone() + .or(self.inline_assistant_model.take()); + self.commit_message_model = value + .clone() + .commit_message_model + .or(self.commit_message_model.take()); + self.thread_summary_model = value + .clone() + .thread_summary_model + .or(self.thread_summary_model.take()); + self.inline_alternatives + .merge_from(&value.inline_alternatives.clone()); + self.notify_when_agent_waiting + .merge_from(&value.notify_when_agent_waiting); + self.play_sound_when_agent_done + .merge_from(&value.play_sound_when_agent_done); + self.stream_edits.merge_from(&value.stream_edits); + self.single_file_review + .merge_from(&value.single_file_review); + self.default_profile + .merge_from(&value.default_profile.clone().map(AgentProfileId)); + self.default_view.merge_from(&value.default_view); + self.preferred_completion_mode + .merge_from(&value.preferred_completion_mode.map(Into::into)); + self.enable_feedback.merge_from(&value.enable_feedback); + self.expand_edit_card.merge_from(&value.expand_edit_card); + self.expand_terminal_card + .merge_from(&value.expand_terminal_card); + self.use_modifier_to_send + .merge_from(&value.use_modifier_to_send); + + self.model_parameters + .extend_from_slice(&value.model_parameters); + + if let Some(profiles) = value.profiles.as_ref() { + self.profiles.extend( + profiles + .into_iter() + .map(|(id, profile)| (AgentProfileId(id.clone()), profile.clone().into())), ); - - settings - .model_parameters - .extend_from_slice(&value.model_parameters); - - if let Some(profiles) = value.profiles.as_ref() { - settings - .profiles - .extend(profiles.into_iter().map(|(id, profile)| { - ( - id.clone(), - AgentProfileSettings { - name: profile.name.clone().into(), - tools: profile.tools.clone(), - enable_all_context_servers: profile - .enable_all_context_servers - .unwrap_or_default(), - context_servers: profile - .context_servers - .iter() - .map(|(context_server_id, preset)| { - ( - context_server_id.clone(), - ContextServerPreset { - tools: preset.tools.clone(), - }, - ) - }) - .collect(), - }, - ) - })); - } } - - debug_assert!( - !sources.default.always_allow_tool_actions.unwrap_or(false), - "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" - ); - - // For security reasons, only trust the user's global settings for whether to always allow tool actions. - // If this could be overridden locally, an attacker could (e.g. by committing to source control and - // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks. - settings.always_allow_tool_actions = sources - .user - .and_then(|setting| setting.always_allow_tool_actions) - .unwrap_or(false); - - Ok(settings) } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { if let Some(b) = vscode .read_value("chat.agent.enabled") .and_then(|b| b.as_bool()) { - current.enabled = Some(b); - current.button = Some(b); + current.agent.get_or_insert_default().enabled = Some(b); + current.agent.get_or_insert_default().button = Some(b); } } } - -fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } -} diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7334f1cf1283ad45e0e98d296b3641c9b8812ef5..7dd72c53631fa3c818fa1f4573e65c41bb5f0944 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -33,7 +33,7 @@ use agent::{ history_store::{HistoryEntryId, HistoryStore}, thread_store::{TextThreadStore, ThreadStore}, }; -use agent_settings::{AgentDockPosition, AgentSettings, DefaultView}; +use agent_settings::{AgentDockPosition, AgentSettings, DefaultAgentView}; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 6fee5fad886ac47ec97cb4a87a975ff2406e4861..9b4750dc793a2bc072ac49a51d74ac1efb4d95db 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,6 +1,8 @@ +mod agent; mod language; mod terminal; mod theme; +pub use agent::*; pub use language::*; pub use terminal::*; pub use theme::*; @@ -23,6 +25,8 @@ pub struct SettingsContent { #[serde(flatten)] pub theme: ThemeSettingsContent, + pub agent: Option, + /// Configuration of audio in Zed. pub audio: Option, pub auto_update: Option,