use collections::{HashMap, IndexMap}; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; use settings_macros::{MergeFrom, with_fallible_options}; use std::sync::Arc; use std::{borrow::Cow, path::PathBuf}; use crate::ExtendingVec; use crate::{DockPosition, DockSide}; #[with_fallible_options] #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] pub struct AgentSettingsContent { /// Whether the Agent is enabled. /// /// Default: true pub enabled: Option, /// Whether to show the agent panel button in the status bar. /// /// Default: true pub button: Option, /// Where to dock the agent panel. /// /// Default: right pub dock: Option, /// Where to dock the utility pane (the thread view pane). /// /// Default: left pub agents_panel_dock: Option, /// Default width in pixels when the agent panel is docked to the left or right. /// /// Default: 640 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, /// Default height in pixels when the agent panel is docked to the bottom. /// /// Default: 320 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_height: Option, /// The default model to use when creating new chats and for other features when a specific model is not specified. pub default_model: Option, /// Favorite models to show at the top of the model selector. #[serde(default)] pub favorite_models: Vec, /// Model to use for the inline assistant. Defaults to default_model when not specified. pub inline_assistant_model: Option, /// Model to use for the inline assistant when streaming tools are enabled. /// /// Default: true pub inline_assistant_use_streaming_tools: Option, /// Model to use for generating git commit messages. /// /// Default: true pub commit_message_model: Option, /// Model to use for generating thread summaries. Defaults to default_model when not specified. pub thread_summary_model: Option, /// Additional models with which to generate alternatives when performing inline assists. pub inline_alternatives: Option>, /// The default profile to use in the Agent. /// /// Default: write pub default_profile: Option>, /// Which view type to show by default in the agent panel. /// /// Default: "thread" pub default_view: Option, /// The available agent profiles. pub profiles: Option, AgentProfileContent>>, /// Whenever a tool action would normally wait for your confirmation /// that you allow it, always choose to allow it. /// /// **Security note**: Even with this enabled, Zed's built-in security rules /// still block some tool actions, such as the terminal tool running `rm -rf /` or `rm -rf ~`, /// to prevent certain classes of failures from happening. /// /// 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 pub always_allow_tool_actions: Option, /// Where to show a popup notification when the agent is waiting for user input. /// /// Default: "primary_screen" pub notify_when_agent_waiting: Option, /// Whether to play a sound when the agent has either completed its response, or needs user input. /// /// Default: false pub play_sound_when_agent_done: Option, /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. /// /// Default: true pub 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)] pub model_parameters: Vec, /// Whether to show thumb buttons for feedback in the agent panel. /// /// Default: true pub enable_feedback: Option, /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. /// /// Default: true pub expand_edit_card: Option, /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. /// /// Default: true pub expand_terminal_card: Option, /// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation. /// Note that this only applies to the stop button, not to ctrl+c inside the terminal. /// /// Default: true pub cancel_generation_on_terminal_stop: Option, /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. /// /// Default: false pub use_modifier_to_send: Option, /// Minimum number of lines of height the agent message editor should have. /// /// Default: 4 pub message_editor_min_lines: Option, /// Whether to show turn statistics (elapsed time during generation, final turn duration). /// /// Default: false pub show_turn_stats: Option, /// Per-tool permission rules for granular control over which tool actions require confirmation. /// /// This setting only applies to the native Zed agent. External agent servers (Claude Code, Gemini CLI, etc.) /// have their own permission systems and are not affected by these settings. pub tool_permissions: Option, } impl AgentSettingsContent { pub fn set_dock(&mut self, dock: DockPosition) { self.dock = Some(dock); } pub fn set_model(&mut self, language_model: LanguageModelSelection) { // 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, // }); self.default_model = Some(language_model) } 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_inline_assistant_use_streaming_tools(&mut self, use_tools: bool) { self.inline_assistant_use_streaming_tools = Some(use_tools); } 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, }); } 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: Arc) { self.default_profile = Some(profile_id); } pub fn add_favorite_model(&mut self, model: LanguageModelSelection) { if !self.favorite_models.contains(&model) { self.favorite_models.push(model); } } pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) { self.favorite_models.retain(|m| m != model); } pub fn set_tool_default_mode(&mut self, tool_id: &str, mode: ToolPermissionMode) { let tool_permissions = self.tool_permissions.get_or_insert_default(); let tool_rules = tool_permissions .tools .entry(Arc::from(tool_id)) .or_default(); tool_rules.default_mode = Some(mode); } pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) { let tool_permissions = self.tool_permissions.get_or_insert_default(); let tool_rules = tool_permissions .tools .entry(Arc::from(tool_name)) .or_default(); let always_allow = tool_rules.always_allow.get_or_insert_default(); if !always_allow.0.iter().any(|r| r.pattern == pattern) { always_allow.0.push(ToolRegexRule { pattern, case_sensitive: None, }); } } pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) { let tool_permissions = self.tool_permissions.get_or_insert_default(); let tool_rules = tool_permissions .tools .entry(Arc::from(tool_name)) .or_default(); let always_deny = tool_rules.always_deny.get_or_insert_default(); if !always_deny.0.iter().any(|r| r.pattern == pattern) { always_deny.0.push(ToolRegexRule { pattern, case_sensitive: None, }); } } } #[with_fallible_options] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] 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>, /// The default language model selected when using this profile. pub default_model: Option, } #[with_fallible_options] #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct ContextServerPresetContent { pub tools: IndexMap, bool>, } #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] #[serde(rename_all = "snake_case")] pub enum DefaultAgentView { #[default] Thread, TextThread, } #[derive( Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum NotifyWhenAgentWaiting { #[default] PrimaryScreen, AllScreens, Never, } #[with_fallible_options] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct LanguageModelSelection { pub provider: LanguageModelProviderSetting, pub model: String, } #[with_fallible_options] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct LanguageModelParameters { pub provider: Option, pub model: Option, #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub temperature: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)] pub struct LanguageModelProviderSetting(pub String); impl JsonSchema for LanguageModelProviderSetting { fn schema_name() -> Cow<'static, str> { "LanguageModelProviderSetting".into() } fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { // list the builtin providers as a subset so that we still auto complete them in the settings json_schema!({ "anyOf": [ { "type": "string", "enum": [ "amazon-bedrock", "anthropic", "copilot_chat", "deepseek", "google", "lmstudio", "mistral", "ollama", "openai", "openrouter", "vercel", "x_ai", "zed.dev" ] }, { "type": "string", } ] }) } } impl From for LanguageModelProviderSetting { fn from(provider: String) -> Self { Self(provider) } } impl From<&str> for LanguageModelProviderSetting { fn from(provider: &str) -> Self { Self(provider.to_string()) } } #[with_fallible_options] #[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, pub codex: Option, /// Custom agent servers configured by the user #[serde(flatten)] pub custom: HashMap, } #[with_fallible_options] #[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct BuiltinAgentServerSettings { /// Absolute path to a binary to be used when launching this agent. /// /// This can be used to run a specific binary without automatic downloads or searching `$PATH`. #[serde(rename = "command")] pub path: Option, /// If a binary is specified in `command`, it will be passed these arguments. pub args: Option>, /// If a binary is specified in `command`, it will be passed these environment variables. pub env: Option>, /// Whether to skip searching `$PATH` for an agent server binary when /// launching this agent. /// /// This has no effect if a `command` is specified. Otherwise, when this is /// `false`, Zed will search `$PATH` for an agent server binary and, if one /// is found, use it for threads with this agent. If no agent binary is /// found on `$PATH`, Zed will automatically install and use its own binary. /// When this is `true`, Zed will not search `$PATH`, and will always use /// its own binary. /// /// Default: true pub ignore_system_version: Option, /// The default mode to use for this agent. /// /// Note: Not only all agents support modes. /// /// Default: None pub default_mode: Option, /// The default model to use for this agent. /// /// This should be the model ID as reported by the agent. /// /// Default: None pub default_model: Option, /// The favorite models for this agent. /// /// These are the model IDs as reported by the agent. /// /// Default: [] #[serde(default, skip_serializing_if = "Vec::is_empty")] pub favorite_models: Vec, /// Default values for session config options. /// /// This is a map from config option ID to value ID. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub default_config_options: HashMap, /// Favorited values for session config options. /// /// This is a map from config option ID to a list of favorited value IDs. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub favorite_config_option_values: HashMap>, } #[with_fallible_options] #[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum CustomAgentServerSettings { Custom { #[serde(rename = "command")] path: PathBuf, #[serde(default, skip_serializing_if = "Vec::is_empty")] args: Vec, /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] env: HashMap, /// The default mode to use for this agent. /// /// Note: Not only all agents support modes. /// /// Default: None default_mode: Option, /// The default model to use for this agent. /// /// This should be the model ID as reported by the agent. /// /// Default: None default_model: Option, /// The favorite models for this agent. /// /// These are the model IDs as reported by the agent. /// /// Default: [] #[serde(default, skip_serializing_if = "Vec::is_empty")] favorite_models: Vec, /// Default values for session config options. /// /// This is a map from config option ID to value ID. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] default_config_options: HashMap, /// Favorited values for session config options. /// /// This is a map from config option ID to a list of favorited value IDs. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] favorite_config_option_values: HashMap>, }, Extension { /// Additional environment variables to pass to the agent. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] env: HashMap, /// The default mode to use for this agent. /// /// Note: Not only all agents support modes. /// /// Default: None default_mode: Option, /// The default model to use for this agent. /// /// This should be the model ID as reported by the agent. /// /// Default: None default_model: Option, /// The favorite models for this agent. /// /// These are the model IDs as reported by the agent. /// /// Default: [] #[serde(default, skip_serializing_if = "Vec::is_empty")] favorite_models: Vec, /// Default values for session config options. /// /// This is a map from config option ID to value ID. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] default_config_options: HashMap, /// Favorited values for session config options. /// /// This is a map from config option ID to a list of favorited value IDs. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] favorite_config_option_values: HashMap>, }, Registry { /// Additional environment variables to pass to the agent. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] env: HashMap, /// The default mode to use for this agent. /// /// Note: Not only all agents support modes. /// /// Default: None default_mode: Option, /// The default model to use for this agent. /// /// This should be the model ID as reported by the agent. /// /// Default: None default_model: Option, /// The favorite models for this agent. /// /// These are the model IDs as reported by the agent. /// /// Default: [] #[serde(default, skip_serializing_if = "Vec::is_empty")] favorite_models: Vec, /// Default values for session config options. /// /// This is a map from config option ID to value ID. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] default_config_options: HashMap, /// Favorited values for session config options. /// /// This is a map from config option ID to a list of favorited value IDs. /// /// Default: {} #[serde(default, skip_serializing_if = "HashMap::is_empty")] favorite_config_option_values: HashMap>, }, } #[with_fallible_options] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct ToolPermissionsContent { /// Per-tool permission rules. /// Keys: terminal, edit_file, delete_path, move_path, create_directory, /// save_file, fetch, web_search #[serde(default)] pub tools: HashMap, ToolRulesContent>, } #[with_fallible_options] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct ToolRulesContent { /// Default mode when no regex rules match. /// Default: confirm pub default_mode: Option, /// Regexes for inputs to auto-approve. /// For terminal: matches command. For file tools: matches path. For fetch: matches URL. /// Default: [] pub always_allow: Option>, /// Regexes for inputs to auto-reject. /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers. /// Default: [] pub always_deny: Option>, /// Regexes for inputs that must always prompt. /// Takes precedence over always_allow but not always_deny. /// Default: [] pub always_confirm: Option>, } #[with_fallible_options] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct ToolRegexRule { /// The regex pattern to match. #[serde(default)] pub pattern: String, /// Whether the regex is case-sensitive. /// Default: false (case-insensitive) pub case_sensitive: Option, } #[derive( Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, )] #[serde(rename_all = "snake_case")] pub enum ToolPermissionMode { /// Auto-approve without prompting. Allow, /// Auto-reject with an error. Deny, /// Always prompt for confirmation (default behavior). #[default] Confirm, } #[cfg(test)] mod tests { use super::*; #[test] fn test_set_tool_default_mode_creates_structure() { let mut settings = AgentSettingsContent::default(); assert!(settings.tool_permissions.is_none()); settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow)); } #[test] fn test_set_tool_default_mode_updates_existing() { let mut settings = AgentSettingsContent::default(); settings.set_tool_default_mode("terminal", ToolPermissionMode::Confirm); settings.set_tool_default_mode("terminal", ToolPermissionMode::Allow); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); assert_eq!(terminal_rules.default_mode, Some(ToolPermissionMode::Allow)); } #[test] fn test_set_tool_default_mode_for_mcp_tool() { let mut settings = AgentSettingsContent::default(); settings.set_tool_default_mode("mcp:github:create_issue", ToolPermissionMode::Allow); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let mcp_rules = tool_permissions .tools .get("mcp:github:create_issue") .unwrap(); assert_eq!(mcp_rules.default_mode, Some(ToolPermissionMode::Allow)); } #[test] fn test_add_tool_allow_pattern_creates_structure() { let mut settings = AgentSettingsContent::default(); assert!(settings.tool_permissions.is_none()); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_allow = terminal_rules.always_allow.as_ref().unwrap(); assert_eq!(always_allow.0.len(), 1); assert_eq!(always_allow.0[0].pattern, "^cargo\\s"); } #[test] fn test_add_tool_allow_pattern_appends_to_existing() { let mut settings = AgentSettingsContent::default(); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_allow = terminal_rules.always_allow.as_ref().unwrap(); assert_eq!(always_allow.0.len(), 2); assert_eq!(always_allow.0[0].pattern, "^cargo\\s"); assert_eq!(always_allow.0[1].pattern, "^npm\\s"); } #[test] fn test_add_tool_allow_pattern_does_not_duplicate() { let mut settings = AgentSettingsContent::default(); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_allow = terminal_rules.always_allow.as_ref().unwrap(); assert_eq!( always_allow.0.len(), 1, "Duplicate patterns should not be added" ); } #[test] fn test_add_tool_allow_pattern_for_different_tools() { let mut settings = AgentSettingsContent::default(); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); assert_eq!( terminal_rules.always_allow.as_ref().unwrap().0[0].pattern, "^cargo\\s" ); let fetch_rules = tool_permissions.tools.get("fetch").unwrap(); assert_eq!( fetch_rules.always_allow.as_ref().unwrap().0[0].pattern, "^https?://github\\.com" ); } #[test] fn test_add_tool_deny_pattern_creates_structure() { let mut settings = AgentSettingsContent::default(); assert!(settings.tool_permissions.is_none()); settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_deny = terminal_rules.always_deny.as_ref().unwrap(); assert_eq!(always_deny.0.len(), 1); assert_eq!(always_deny.0[0].pattern, "^rm\\s"); } #[test] fn test_add_tool_deny_pattern_appends_to_existing() { let mut settings = AgentSettingsContent::default(); settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string()); settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_deny = terminal_rules.always_deny.as_ref().unwrap(); assert_eq!(always_deny.0.len(), 2); assert_eq!(always_deny.0[0].pattern, "^rm\\s"); assert_eq!(always_deny.0[1].pattern, "^sudo\\s"); } #[test] fn test_add_tool_deny_pattern_does_not_duplicate() { let mut settings = AgentSettingsContent::default(); settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string()); settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string()); settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_deny = terminal_rules.always_deny.as_ref().unwrap(); assert_eq!( always_deny.0.len(), 1, "Duplicate patterns should not be added" ); } #[test] fn test_add_tool_deny_and_allow_patterns_separate() { let mut settings = AgentSettingsContent::default(); settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string()); settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string()); let tool_permissions = settings.tool_permissions.as_ref().unwrap(); let terminal_rules = tool_permissions.tools.get("terminal").unwrap(); let always_allow = terminal_rules.always_allow.as_ref().unwrap(); assert_eq!(always_allow.0.len(), 1); assert_eq!(always_allow.0[0].pattern, "^cargo\\s"); let always_deny = terminal_rules.always_deny.as_ref().unwrap(); assert_eq!(always_deny.0.len(), 1); assert_eq!(always_deny.0[0].pattern, "^rm\\s"); } }