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