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