agent.rs

  1use collections::{HashMap, IndexMap};
  2use schemars::{JsonSchema, json_schema};
  3use serde::{Deserialize, Serialize};
  4use settings_macros::{MergeFrom, with_fallible_options};
  5use std::sync::Arc;
  6use std::{borrow::Cow, path::PathBuf};
  7
  8use crate::ExtendingVec;
  9
 10use crate::DockPosition;
 11
 12/// Where new threads should start by default.
 13#[derive(
 14    Clone,
 15    Copy,
 16    Debug,
 17    Default,
 18    PartialEq,
 19    Eq,
 20    Serialize,
 21    Deserialize,
 22    JsonSchema,
 23    MergeFrom,
 24    strum::VariantArray,
 25    strum::VariantNames,
 26)]
 27#[serde(rename_all = "snake_case")]
 28pub enum NewThreadLocation {
 29    /// Start threads in the current project.
 30    #[default]
 31    LocalProject,
 32    /// Start threads in a new worktree.
 33    NewWorktree,
 34}
 35
 36/// Where to position the sidebar.
 37#[derive(
 38    Clone,
 39    Copy,
 40    Debug,
 41    Default,
 42    PartialEq,
 43    Eq,
 44    Serialize,
 45    Deserialize,
 46    JsonSchema,
 47    MergeFrom,
 48    strum::VariantArray,
 49    strum::VariantNames,
 50)]
 51#[serde(rename_all = "snake_case")]
 52pub enum SidebarDockPosition {
 53    /// Always show the sidebar on the left side.
 54    #[default]
 55    Left,
 56    /// Always show the sidebar on the right side.
 57    Right,
 58}
 59
 60#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 61pub enum SidebarSide {
 62    #[default]
 63    Left,
 64    Right,
 65}
 66
 67/// How thinking blocks should be displayed by default in the agent panel.
 68#[derive(
 69    Clone,
 70    Copy,
 71    Debug,
 72    Default,
 73    PartialEq,
 74    Eq,
 75    Serialize,
 76    Deserialize,
 77    JsonSchema,
 78    MergeFrom,
 79    strum::VariantArray,
 80    strum::VariantNames,
 81)]
 82#[serde(rename_all = "snake_case")]
 83pub enum ThinkingBlockDisplay {
 84    /// Thinking blocks fully expand during streaming, then auto-collapse
 85    /// when the model finishes thinking. Users can re-expand after collapse.
 86    #[default]
 87    Auto,
 88    /// Thinking blocks auto-expand with a height constraint during streaming,
 89    /// then remain in their constrained state when complete. Users can click
 90    /// to fully expand or collapse.
 91    Preview,
 92    /// Thinking blocks are always fully expanded by default (no height constraint).
 93    AlwaysExpanded,
 94    /// Thinking blocks are always collapsed by default.
 95    AlwaysCollapsed,
 96}
 97
 98#[with_fallible_options]
 99#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
100pub struct AgentSettingsContent {
101    /// Whether the Agent is enabled.
102    ///
103    /// Default: true
104    pub enabled: Option<bool>,
105    /// Whether to show the agent panel button in the status bar.
106    ///
107    /// Default: true
108    pub button: Option<bool>,
109    /// Where to dock the agent panel.
110    ///
111    /// Default: right
112    pub dock: Option<DockPosition>,
113    /// Whether the agent panel should use flexible (proportional) sizing.
114    ///
115    /// Default: true
116    pub flexible: Option<bool>,
117    /// Where to position the sidebar.
118    ///
119    /// Default: left
120    pub sidebar_side: Option<SidebarDockPosition>,
121    /// Default width in pixels when the agent panel is docked to the left or right.
122    ///
123    /// Default: 640
124    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
125    pub default_width: Option<f32>,
126    /// Default height in pixels when the agent panel is docked to the bottom.
127    ///
128    /// Default: 320
129    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
130    pub default_height: Option<f32>,
131    /// Maximum content width in pixels for the agent panel. Content will be
132    /// centered when the panel is wider than this value.
133    ///
134    /// Default: 850
135    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
136    pub max_content_width: Option<f32>,
137    /// The default model to use when creating new chats and for other features when a specific model is not specified.
138    pub default_model: Option<LanguageModelSelection>,
139    /// Favorite models to show at the top of the model selector.
140    #[serde(default)]
141    pub favorite_models: Vec<LanguageModelSelection>,
142    /// Model to use for the inline assistant. Defaults to default_model when not specified.
143    pub inline_assistant_model: Option<LanguageModelSelection>,
144    /// Model to use for the inline assistant when streaming tools are enabled.
145    ///
146    /// Default: true
147    pub inline_assistant_use_streaming_tools: Option<bool>,
148    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
149    pub commit_message_model: Option<LanguageModelSelection>,
150    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
151    pub thread_summary_model: Option<LanguageModelSelection>,
152    /// Additional models with which to generate alternatives when performing inline assists.
153    pub inline_alternatives: Option<Vec<LanguageModelSelection>>,
154    /// The default profile to use in the Agent.
155    ///
156    /// Default: write
157    pub default_profile: Option<Arc<str>>,
158    /// Where new threads should start by default.
159    ///
160    /// Default: "local_project"
161    pub new_thread_location: Option<NewThreadLocation>,
162    /// The available agent profiles.
163    pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
164    /// Where to show a popup notification when the agent is waiting for user input.
165    ///
166    /// Default: "primary_screen"
167    pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
168    /// When to play a sound when the agent has either completed its response, or needs user input.
169    ///
170    /// Default: never
171    pub play_sound_when_agent_done: Option<PlaySoundWhenAgentDone>,
172    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
173    ///
174    /// Default: true
175    pub single_file_review: Option<bool>,
176    /// Additional parameters for language model requests. When making a request
177    /// to a model, parameters will be taken from the last entry in this list
178    /// that matches the model's provider and name. In each entry, both provider
179    /// and model are optional, so that you can specify parameters for either
180    /// one.
181    ///
182    /// Default: []
183    #[serde(default)]
184    pub model_parameters: Vec<LanguageModelParameters>,
185    /// Whether to show thumb buttons for feedback in the agent panel.
186    ///
187    /// Default: true
188    pub enable_feedback: Option<bool>,
189    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
190    ///
191    /// Default: true
192    pub expand_edit_card: Option<bool>,
193    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
194    ///
195    /// Default: true
196    pub expand_terminal_card: Option<bool>,
197    /// How thinking blocks should be displayed by default in the agent panel.
198    ///
199    /// Default: automatic
200    pub thinking_display: Option<ThinkingBlockDisplay>,
201    /// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
202    /// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
203    ///
204    /// Default: true
205    pub cancel_generation_on_terminal_stop: Option<bool>,
206    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
207    ///
208    /// Default: false
209    pub use_modifier_to_send: Option<bool>,
210    /// Minimum number of lines of height the agent message editor should have.
211    ///
212    /// Default: 4
213    pub message_editor_min_lines: Option<usize>,
214    /// Whether to show turn statistics (elapsed time during generation, final turn duration).
215    ///
216    /// Default: false
217    pub show_turn_stats: Option<bool>,
218    /// Whether to show the merge conflict indicator in the status bar
219    /// that offers to resolve conflicts using the agent.
220    ///
221    /// Default: true
222    pub show_merge_conflict_indicator: Option<bool>,
223    /// Per-tool permission rules for granular control over which tool actions
224    /// require confirmation.
225    ///
226    /// The global `default` applies when no tool-specific rules match.
227    /// For external agent servers (e.g. Claude Agent) that define their own
228    /// permission modes, "deny" and "confirm" still take precedence — the
229    /// external agent's permission system is only used when Zed would allow
230    /// the action. Per-tool regex patterns (`always_allow`, `always_deny`,
231    /// `always_confirm`) match against the tool's text input (command, path,
232    /// URL, etc.).
233    pub tool_permissions: Option<ToolPermissionsContent>,
234}
235
236impl AgentSettingsContent {
237    pub fn set_dock(&mut self, dock: DockPosition) {
238        self.dock = Some(dock);
239    }
240
241    pub fn set_sidebar_side(&mut self, position: SidebarDockPosition) {
242        self.sidebar_side = Some(position);
243    }
244
245    pub fn set_flexible_size(&mut self, flexible: bool) {
246        self.flexible = Some(flexible);
247    }
248
249    pub fn set_model(&mut self, language_model: LanguageModelSelection) {
250        self.default_model = Some(language_model)
251    }
252
253    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
254        self.inline_assistant_model = Some(LanguageModelSelection {
255            provider: provider.into(),
256            model,
257            enable_thinking: false,
258            effort: None,
259            speed: None,
260        });
261    }
262
263    pub fn set_profile(&mut self, profile_id: Arc<str>) {
264        self.default_profile = Some(profile_id);
265    }
266
267    pub fn set_new_thread_location(&mut self, value: NewThreadLocation) {
268        self.new_thread_location = Some(value);
269    }
270
271    pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
272        if !self.favorite_models.contains(&model) {
273            self.favorite_models.push(model);
274        }
275    }
276
277    pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
278        self.favorite_models.retain(|m| m != model);
279    }
280
281    pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {
282        let tool_permissions = self.tool_permissions.get_or_insert_default();
283        let tool_rules = tool_permissions
284            .tools
285            .entry(Arc::from(tool_id))
286            .or_default();
287        tool_rules.default = Some(mode);
288    }
289
290    pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
291        let tool_permissions = self.tool_permissions.get_or_insert_default();
292        let tool_rules = tool_permissions
293            .tools
294            .entry(Arc::from(tool_name))
295            .or_default();
296        let always_allow = tool_rules.always_allow.get_or_insert_default();
297        if !always_allow.0.iter().any(|r| r.pattern == pattern) {
298            always_allow.0.push(ToolRegexRule {
299                pattern,
300                case_sensitive: None,
301            });
302        }
303    }
304
305    pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
306        let tool_permissions = self.tool_permissions.get_or_insert_default();
307        let tool_rules = tool_permissions
308            .tools
309            .entry(Arc::from(tool_name))
310            .or_default();
311        let always_deny = tool_rules.always_deny.get_or_insert_default();
312        if !always_deny.0.iter().any(|r| r.pattern == pattern) {
313            always_deny.0.push(ToolRegexRule {
314                pattern,
315                case_sensitive: None,
316            });
317        }
318    }
319}
320
321#[with_fallible_options]
322#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
323pub struct AgentProfileContent {
324    pub name: Arc<str>,
325    #[serde(default)]
326    pub tools: IndexMap<Arc<str>, bool>,
327    /// Whether all context servers are enabled by default.
328    pub enable_all_context_servers: Option<bool>,
329    #[serde(default)]
330    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
331    /// The default language model selected when using this profile.
332    pub default_model: Option<LanguageModelSelection>,
333}
334
335#[with_fallible_options]
336#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
337pub struct ContextServerPresetContent {
338    pub tools: IndexMap<Arc<str>, bool>,
339}
340
341#[derive(
342    Copy,
343    Clone,
344    Default,
345    Debug,
346    Serialize,
347    Deserialize,
348    JsonSchema,
349    MergeFrom,
350    PartialEq,
351    strum::VariantArray,
352    strum::VariantNames,
353)]
354#[serde(rename_all = "snake_case")]
355pub enum NotifyWhenAgentWaiting {
356    #[default]
357    PrimaryScreen,
358    AllScreens,
359    Never,
360}
361
362#[derive(
363    Copy,
364    Clone,
365    Default,
366    Debug,
367    Serialize,
368    Deserialize,
369    JsonSchema,
370    MergeFrom,
371    PartialEq,
372    strum::VariantArray,
373    strum::VariantNames,
374)]
375#[serde(rename_all = "snake_case")]
376pub enum PlaySoundWhenAgentDone {
377    #[default]
378    Never,
379    WhenHidden,
380    Always,
381}
382
383impl PlaySoundWhenAgentDone {
384    pub fn should_play(&self, visible: bool) -> bool {
385        match self {
386            PlaySoundWhenAgentDone::Never => false,
387            PlaySoundWhenAgentDone::WhenHidden => !visible,
388            PlaySoundWhenAgentDone::Always => true,
389        }
390    }
391}
392
393#[with_fallible_options]
394#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
395pub struct LanguageModelSelection {
396    pub provider: LanguageModelProviderSetting,
397    pub model: String,
398    #[serde(default)]
399    pub enable_thinking: bool,
400    pub effort: Option<String>,
401    pub speed: Option<language_model_core::Speed>,
402}
403
404#[with_fallible_options]
405#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
406pub struct LanguageModelParameters {
407    pub provider: Option<LanguageModelProviderSetting>,
408    pub model: Option<String>,
409    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
410    pub temperature: Option<f32>,
411}
412
413#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
414pub struct LanguageModelProviderSetting(pub String);
415
416impl JsonSchema for LanguageModelProviderSetting {
417    fn schema_name() -> Cow<'static, str> {
418        "LanguageModelProviderSetting".into()
419    }
420
421    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
422        // list the builtin providers as a subset so that we still auto complete them in the settings
423        json_schema!({
424            "anyOf": [
425                {
426                    "type": "string",
427                    "enum": [
428                        "amazon-bedrock",
429                        "anthropic",
430                        "copilot_chat",
431                        "deepseek",
432                        "google",
433                        "lmstudio",
434                        "mistral",
435                        "ollama",
436                        "openai",
437                        "openrouter",
438                        "vercel",
439                        "vercel_ai_gateway",
440                        "x_ai",
441                        "zed.dev"
442                    ]
443                },
444                {
445                    "type": "string",
446                }
447            ]
448        })
449    }
450}
451
452impl From<String> for LanguageModelProviderSetting {
453    fn from(provider: String) -> Self {
454        Self(provider)
455    }
456}
457
458impl From<&str> for LanguageModelProviderSetting {
459    fn from(provider: &str) -> Self {
460        Self(provider.to_string())
461    }
462}
463
464#[with_fallible_options]
465#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
466#[serde(transparent)]
467pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
468
469impl std::ops::Deref for AllAgentServersSettings {
470    type Target = HashMap<String, CustomAgentServerSettings>;
471
472    fn deref(&self) -> &Self::Target {
473        &self.0
474    }
475}
476
477impl std::ops::DerefMut for AllAgentServersSettings {
478    fn deref_mut(&mut self) -> &mut Self::Target {
479        &mut self.0
480    }
481}
482
483#[with_fallible_options]
484#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
485#[serde(tag = "type", rename_all = "snake_case")]
486pub enum CustomAgentServerSettings {
487    Custom {
488        #[serde(rename = "command")]
489        path: PathBuf,
490        #[serde(default, skip_serializing_if = "Vec::is_empty")]
491        args: Vec<String>,
492        /// Default: {}
493        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
494        env: HashMap<String, String>,
495        /// The default mode to use for this agent.
496        ///
497        /// Note: Not only all agents support modes.
498        ///
499        /// Default: None
500        default_mode: Option<String>,
501        /// The default model to use for this agent.
502        ///
503        /// This should be the model ID as reported by the agent.
504        ///
505        /// Default: None
506        default_model: Option<String>,
507        /// The favorite models for this agent.
508        ///
509        /// These are the model IDs as reported by the agent.
510        ///
511        /// Default: []
512        #[serde(default, skip_serializing_if = "Vec::is_empty")]
513        favorite_models: Vec<String>,
514        /// Default values for session config options.
515        ///
516        /// This is a map from config option ID to value ID.
517        ///
518        /// Default: {}
519        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
520        default_config_options: HashMap<String, String>,
521        /// Favorited values for session config options.
522        ///
523        /// This is a map from config option ID to a list of favorited value IDs.
524        ///
525        /// Default: {}
526        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
527        favorite_config_option_values: HashMap<String, Vec<String>>,
528    },
529    Extension {
530        /// Additional environment variables to pass to the agent.
531        ///
532        /// Default: {}
533        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
534        env: HashMap<String, String>,
535        /// The default mode to use for this agent.
536        ///
537        /// Note: Not only all agents support modes.
538        ///
539        /// Default: None
540        default_mode: Option<String>,
541        /// The default model to use for this agent.
542        ///
543        /// This should be the model ID as reported by the agent.
544        ///
545        /// Default: None
546        default_model: Option<String>,
547        /// The favorite models for this agent.
548        ///
549        /// These are the model IDs as reported by the agent.
550        ///
551        /// Default: []
552        #[serde(default, skip_serializing_if = "Vec::is_empty")]
553        favorite_models: Vec<String>,
554        /// Default values for session config options.
555        ///
556        /// This is a map from config option ID to value ID.
557        ///
558        /// Default: {}
559        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
560        default_config_options: HashMap<String, String>,
561        /// Favorited values for session config options.
562        ///
563        /// This is a map from config option ID to a list of favorited value IDs.
564        ///
565        /// Default: {}
566        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
567        favorite_config_option_values: HashMap<String, Vec<String>>,
568    },
569    Registry {
570        /// Additional environment variables to pass to the agent.
571        ///
572        /// Default: {}
573        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
574        env: HashMap<String, String>,
575        /// The default mode to use for this agent.
576        ///
577        /// Note: Not only all agents support modes.
578        ///
579        /// Default: None
580        default_mode: Option<String>,
581        /// The default model to use for this agent.
582        ///
583        /// This should be the model ID as reported by the agent.
584        ///
585        /// Default: None
586        default_model: Option<String>,
587        /// The favorite models for this agent.
588        ///
589        /// These are the model IDs as reported by the agent.
590        ///
591        /// Default: []
592        #[serde(default, skip_serializing_if = "Vec::is_empty")]
593        favorite_models: Vec<String>,
594        /// Default values for session config options.
595        ///
596        /// This is a map from config option ID to value ID.
597        ///
598        /// Default: {}
599        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
600        default_config_options: HashMap<String, String>,
601        /// Favorited values for session config options.
602        ///
603        /// This is a map from config option ID to a list of favorited value IDs.
604        ///
605        /// Default: {}
606        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
607        favorite_config_option_values: HashMap<String, Vec<String>>,
608    },
609}
610
611#[with_fallible_options]
612#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
613pub struct ToolPermissionsContent {
614    /// Global default permission when no tool-specific rules match.
615    /// Individual tools can override this with their own default.
616    /// Default: confirm
617    #[serde(alias = "default_mode")]
618    pub default: Option<ToolPermissionMode>,
619
620    /// Per-tool permission rules.
621    /// Keys are tool names (e.g. terminal, edit_file, fetch) including MCP
622    /// tools (e.g. mcp:server_name:tool_name). Any tool name is accepted;
623    /// even tools without meaningful text input can have a `default` set.
624    #[serde(default)]
625    pub tools: HashMap<Arc<str>, ToolRulesContent>,
626}
627
628#[with_fallible_options]
629#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
630pub struct ToolRulesContent {
631    /// Default mode when no regex rules match.
632    /// When unset, inherits from the global `tool_permissions.default`.
633    #[serde(alias = "default_mode")]
634    pub default: Option<ToolPermissionMode>,
635
636    /// Regexes for inputs to auto-approve.
637    /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
638    /// For `copy_path` and `move_path`, patterns are matched independently against each
639    /// path (source and destination).
640    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
641    /// removed by a higher-priority layer—only new patterns can be added.
642    /// Default: []
643    pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
644
645    /// Regexes for inputs to auto-reject.
646    /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
647    /// For `copy_path` and `move_path`, patterns are matched independently against each
648    /// path (source and destination).
649    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
650    /// removed by a higher-priority layer—only new patterns can be added.
651    /// Default: []
652    pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
653
654    /// Regexes for inputs that must always prompt.
655    /// Takes precedence over always_allow but not always_deny.
656    /// For `copy_path` and `move_path`, patterns are matched independently against each
657    /// path (source and destination).
658    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
659    /// removed by a higher-priority layer—only new patterns can be added.
660    /// Default: []
661    pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
662}
663
664#[with_fallible_options]
665#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
666pub struct ToolRegexRule {
667    /// The regex pattern to match.
668    #[serde(default)]
669    pub pattern: String,
670
671    /// Whether the regex is case-sensitive.
672    /// Default: false (case-insensitive)
673    pub case_sensitive: Option<bool>,
674}
675
676#[derive(
677    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
678)]
679#[serde(rename_all = "snake_case")]
680pub enum ToolPermissionMode {
681    /// Auto-approve without prompting.
682    Allow,
683    /// Auto-reject with an error.
684    Deny,
685    /// Always prompt for confirmation (default behavior).
686    #[default]
687    Confirm,
688}
689
690impl std::fmt::Display for ToolPermissionMode {
691    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692        match self {
693            ToolPermissionMode::Allow => write!(f, "Allow"),
694            ToolPermissionMode::Deny => write!(f, "Deny"),
695            ToolPermissionMode::Confirm => write!(f, "Confirm"),
696        }
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn test_set_tool_default_permission_creates_structure() {
706        let mut settings = AgentSettingsContent::default();
707        assert!(settings.tool_permissions.is_none());
708
709        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
710
711        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
712        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
713        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
714    }
715
716    #[test]
717    fn test_set_tool_default_permission_updates_existing() {
718        let mut settings = AgentSettingsContent::default();
719
720        settings.set_tool_default_permission("terminal", ToolPermissionMode::Confirm);
721        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
722
723        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
724        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
725        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
726    }
727
728    #[test]
729    fn test_set_tool_default_permission_for_mcp_tool() {
730        let mut settings = AgentSettingsContent::default();
731
732        settings.set_tool_default_permission("mcp:github:create_issue", ToolPermissionMode::Allow);
733
734        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
735        let mcp_rules = tool_permissions
736            .tools
737            .get("mcp:github:create_issue")
738            .unwrap();
739        assert_eq!(mcp_rules.default, Some(ToolPermissionMode::Allow));
740    }
741
742    #[test]
743    fn test_add_tool_allow_pattern_creates_structure() {
744        let mut settings = AgentSettingsContent::default();
745        assert!(settings.tool_permissions.is_none());
746
747        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
748
749        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
750        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
751        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
752        assert_eq!(always_allow.0.len(), 1);
753        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
754    }
755
756    #[test]
757    fn test_add_tool_allow_pattern_appends_to_existing() {
758        let mut settings = AgentSettingsContent::default();
759
760        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
761        settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
762
763        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
764        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
765        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
766        assert_eq!(always_allow.0.len(), 2);
767        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
768        assert_eq!(always_allow.0[1].pattern, "^npm\\s");
769    }
770
771    #[test]
772    fn test_add_tool_allow_pattern_does_not_duplicate() {
773        let mut settings = AgentSettingsContent::default();
774
775        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
776        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
777        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
778
779        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
780        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
781        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
782        assert_eq!(
783            always_allow.0.len(),
784            1,
785            "Duplicate patterns should not be added"
786        );
787    }
788
789    #[test]
790    fn test_add_tool_allow_pattern_for_different_tools() {
791        let mut settings = AgentSettingsContent::default();
792
793        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
794        settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
795
796        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
797
798        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
799        assert_eq!(
800            terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
801            "^cargo\\s"
802        );
803
804        let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
805        assert_eq!(
806            fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
807            "^https?://github\\.com"
808        );
809    }
810
811    #[test]
812    fn test_add_tool_deny_pattern_creates_structure() {
813        let mut settings = AgentSettingsContent::default();
814        assert!(settings.tool_permissions.is_none());
815
816        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
817
818        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
819        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
820        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
821        assert_eq!(always_deny.0.len(), 1);
822        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
823    }
824
825    #[test]
826    fn test_add_tool_deny_pattern_appends_to_existing() {
827        let mut settings = AgentSettingsContent::default();
828
829        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
830        settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());
831
832        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
833        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
834        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
835        assert_eq!(always_deny.0.len(), 2);
836        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
837        assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
838    }
839
840    #[test]
841    fn test_add_tool_deny_pattern_does_not_duplicate() {
842        let mut settings = AgentSettingsContent::default();
843
844        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
845        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
846        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
847
848        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
849        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
850        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
851        assert_eq!(
852            always_deny.0.len(),
853            1,
854            "Duplicate patterns should not be added"
855        );
856    }
857
858    #[test]
859    fn test_add_tool_deny_and_allow_patterns_separate() {
860        let mut settings = AgentSettingsContent::default();
861
862        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
863        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
864
865        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
866        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
867
868        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
869        assert_eq!(always_allow.0.len(), 1);
870        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
871
872        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
873        assert_eq!(always_deny.0.len(), 1);
874        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
875    }
876}