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 threads 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 threads 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        });
260    }
261
262    pub fn set_profile(&mut self, profile_id: Arc<str>) {
263        self.default_profile = Some(profile_id);
264    }
265
266    pub fn set_new_thread_location(&mut self, value: NewThreadLocation) {
267        self.new_thread_location = Some(value);
268    }
269
270    pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
271        if !self.favorite_models.contains(&model) {
272            self.favorite_models.push(model);
273        }
274    }
275
276    pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
277        self.favorite_models.retain(|m| m != model);
278    }
279
280    pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {
281        let tool_permissions = self.tool_permissions.get_or_insert_default();
282        let tool_rules = tool_permissions
283            .tools
284            .entry(Arc::from(tool_id))
285            .or_default();
286        tool_rules.default = Some(mode);
287    }
288
289    pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
290        let tool_permissions = self.tool_permissions.get_or_insert_default();
291        let tool_rules = tool_permissions
292            .tools
293            .entry(Arc::from(tool_name))
294            .or_default();
295        let always_allow = tool_rules.always_allow.get_or_insert_default();
296        if !always_allow.0.iter().any(|r| r.pattern == pattern) {
297            always_allow.0.push(ToolRegexRule {
298                pattern,
299                case_sensitive: None,
300            });
301        }
302    }
303
304    pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
305        let tool_permissions = self.tool_permissions.get_or_insert_default();
306        let tool_rules = tool_permissions
307            .tools
308            .entry(Arc::from(tool_name))
309            .or_default();
310        let always_deny = tool_rules.always_deny.get_or_insert_default();
311        if !always_deny.0.iter().any(|r| r.pattern == pattern) {
312            always_deny.0.push(ToolRegexRule {
313                pattern,
314                case_sensitive: None,
315            });
316        }
317    }
318}
319
320#[with_fallible_options]
321#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
322pub struct AgentProfileContent {
323    pub name: Arc<str>,
324    #[serde(default)]
325    pub tools: IndexMap<Arc<str>, bool>,
326    /// Whether all context servers are enabled by default.
327    pub enable_all_context_servers: Option<bool>,
328    #[serde(default)]
329    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
330    /// The default language model selected when using this profile.
331    pub default_model: Option<LanguageModelSelection>,
332}
333
334#[with_fallible_options]
335#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
336pub struct ContextServerPresetContent {
337    pub tools: IndexMap<Arc<str>, bool>,
338}
339
340#[derive(
341    Copy,
342    Clone,
343    Default,
344    Debug,
345    Serialize,
346    Deserialize,
347    JsonSchema,
348    MergeFrom,
349    PartialEq,
350    strum::VariantArray,
351    strum::VariantNames,
352)]
353#[serde(rename_all = "snake_case")]
354pub enum NotifyWhenAgentWaiting {
355    #[default]
356    PrimaryScreen,
357    AllScreens,
358    Never,
359}
360
361#[derive(
362    Copy,
363    Clone,
364    Default,
365    Debug,
366    Serialize,
367    Deserialize,
368    JsonSchema,
369    MergeFrom,
370    PartialEq,
371    strum::VariantArray,
372    strum::VariantNames,
373)]
374#[serde(rename_all = "snake_case")]
375pub enum PlaySoundWhenAgentDone {
376    #[default]
377    Never,
378    WhenHidden,
379    Always,
380}
381
382impl PlaySoundWhenAgentDone {
383    pub fn should_play(&self, visible: bool) -> bool {
384        match self {
385            PlaySoundWhenAgentDone::Never => false,
386            PlaySoundWhenAgentDone::WhenHidden => !visible,
387            PlaySoundWhenAgentDone::Always => true,
388        }
389    }
390}
391
392#[with_fallible_options]
393#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
394pub struct LanguageModelSelection {
395    pub provider: LanguageModelProviderSetting,
396    pub model: String,
397    #[serde(default)]
398    pub enable_thinking: bool,
399    pub effort: Option<String>,
400}
401
402#[with_fallible_options]
403#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
404pub struct LanguageModelParameters {
405    pub provider: Option<LanguageModelProviderSetting>,
406    pub model: Option<String>,
407    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
408    pub temperature: Option<f32>,
409}
410
411#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
412pub struct LanguageModelProviderSetting(pub String);
413
414impl JsonSchema for LanguageModelProviderSetting {
415    fn schema_name() -> Cow<'static, str> {
416        "LanguageModelProviderSetting".into()
417    }
418
419    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
420        // list the builtin providers as a subset so that we still auto complete them in the settings
421        json_schema!({
422            "anyOf": [
423                {
424                    "type": "string",
425                    "enum": [
426                        "amazon-bedrock",
427                        "anthropic",
428                        "copilot_chat",
429                        "deepseek",
430                        "google",
431                        "lmstudio",
432                        "mistral",
433                        "ollama",
434                        "openai",
435                        "openrouter",
436                        "vercel",
437                        "vercel_ai_gateway",
438                        "x_ai",
439                        "zed.dev"
440                    ]
441                },
442                {
443                    "type": "string",
444                }
445            ]
446        })
447    }
448}
449
450impl From<String> for LanguageModelProviderSetting {
451    fn from(provider: String) -> Self {
452        Self(provider)
453    }
454}
455
456impl From<&str> for LanguageModelProviderSetting {
457    fn from(provider: &str) -> Self {
458        Self(provider.to_string())
459    }
460}
461
462#[with_fallible_options]
463#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
464#[serde(transparent)]
465pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
466
467impl std::ops::Deref for AllAgentServersSettings {
468    type Target = HashMap<String, CustomAgentServerSettings>;
469
470    fn deref(&self) -> &Self::Target {
471        &self.0
472    }
473}
474
475impl std::ops::DerefMut for AllAgentServersSettings {
476    fn deref_mut(&mut self) -> &mut Self::Target {
477        &mut self.0
478    }
479}
480
481#[with_fallible_options]
482#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
483#[serde(tag = "type", rename_all = "snake_case")]
484pub enum CustomAgentServerSettings {
485    Custom {
486        #[serde(rename = "command")]
487        path: PathBuf,
488        #[serde(default, skip_serializing_if = "Vec::is_empty")]
489        args: Vec<String>,
490        /// Default: {}
491        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
492        env: HashMap<String, String>,
493        /// The default mode to use for this agent.
494        ///
495        /// Note: Not only all agents support modes.
496        ///
497        /// Default: None
498        default_mode: Option<String>,
499        /// The default model to use for this agent.
500        ///
501        /// This should be the model ID as reported by the agent.
502        ///
503        /// Default: None
504        default_model: Option<String>,
505        /// The favorite models for this agent.
506        ///
507        /// These are the model IDs as reported by the agent.
508        ///
509        /// Default: []
510        #[serde(default, skip_serializing_if = "Vec::is_empty")]
511        favorite_models: Vec<String>,
512        /// Default values for session config options.
513        ///
514        /// This is a map from config option ID to value ID.
515        ///
516        /// Default: {}
517        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
518        default_config_options: HashMap<String, String>,
519        /// Favorited values for session config options.
520        ///
521        /// This is a map from config option ID to a list of favorited value IDs.
522        ///
523        /// Default: {}
524        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
525        favorite_config_option_values: HashMap<String, Vec<String>>,
526    },
527    Extension {
528        /// Additional environment variables to pass to the agent.
529        ///
530        /// Default: {}
531        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
532        env: HashMap<String, String>,
533        /// The default mode to use for this agent.
534        ///
535        /// Note: Not only all agents support modes.
536        ///
537        /// Default: None
538        default_mode: Option<String>,
539        /// The default model to use for this agent.
540        ///
541        /// This should be the model ID as reported by the agent.
542        ///
543        /// Default: None
544        default_model: Option<String>,
545        /// The favorite models for this agent.
546        ///
547        /// These are the model IDs as reported by the agent.
548        ///
549        /// Default: []
550        #[serde(default, skip_serializing_if = "Vec::is_empty")]
551        favorite_models: Vec<String>,
552        /// Default values for session config options.
553        ///
554        /// This is a map from config option ID to value ID.
555        ///
556        /// Default: {}
557        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
558        default_config_options: HashMap<String, String>,
559        /// Favorited values for session config options.
560        ///
561        /// This is a map from config option ID to a list of favorited value IDs.
562        ///
563        /// Default: {}
564        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
565        favorite_config_option_values: HashMap<String, Vec<String>>,
566    },
567    Registry {
568        /// Additional environment variables to pass to the agent.
569        ///
570        /// Default: {}
571        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
572        env: HashMap<String, String>,
573        /// The default mode to use for this agent.
574        ///
575        /// Note: Not only all agents support modes.
576        ///
577        /// Default: None
578        default_mode: Option<String>,
579        /// The default model to use for this agent.
580        ///
581        /// This should be the model ID as reported by the agent.
582        ///
583        /// Default: None
584        default_model: Option<String>,
585        /// The favorite models for this agent.
586        ///
587        /// These are the model IDs as reported by the agent.
588        ///
589        /// Default: []
590        #[serde(default, skip_serializing_if = "Vec::is_empty")]
591        favorite_models: Vec<String>,
592        /// Default values for session config options.
593        ///
594        /// This is a map from config option ID to value ID.
595        ///
596        /// Default: {}
597        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
598        default_config_options: HashMap<String, String>,
599        /// Favorited values for session config options.
600        ///
601        /// This is a map from config option ID to a list of favorited value IDs.
602        ///
603        /// Default: {}
604        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
605        favorite_config_option_values: HashMap<String, Vec<String>>,
606    },
607}
608
609#[with_fallible_options]
610#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
611pub struct ToolPermissionsContent {
612    /// Global default permission when no tool-specific rules match.
613    /// Individual tools can override this with their own default.
614    /// Default: confirm
615    #[serde(alias = "default_mode")]
616    pub default: Option<ToolPermissionMode>,
617
618    /// Per-tool permission rules.
619    /// Keys are tool names (e.g. terminal, edit_file, fetch) including MCP
620    /// tools (e.g. mcp:server_name:tool_name). Any tool name is accepted;
621    /// even tools without meaningful text input can have a `default` set.
622    #[serde(default)]
623    pub tools: HashMap<Arc<str>, ToolRulesContent>,
624}
625
626#[with_fallible_options]
627#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
628pub struct ToolRulesContent {
629    /// Default mode when no regex rules match.
630    /// When unset, inherits from the global `tool_permissions.default`.
631    #[serde(alias = "default_mode")]
632    pub default: Option<ToolPermissionMode>,
633
634    /// Regexes for inputs to auto-approve.
635    /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
636    /// For `copy_path` and `move_path`, patterns are matched independently against each
637    /// path (source and destination).
638    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
639    /// removed by a higher-priority layer—only new patterns can be added.
640    /// Default: []
641    pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
642
643    /// Regexes for inputs to auto-reject.
644    /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
645    /// For `copy_path` and `move_path`, patterns are matched independently against each
646    /// path (source and destination).
647    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
648    /// removed by a higher-priority layer—only new patterns can be added.
649    /// Default: []
650    pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
651
652    /// Regexes for inputs that must always prompt.
653    /// Takes precedence over always_allow but not always_deny.
654    /// For `copy_path` and `move_path`, patterns are matched independently against each
655    /// path (source and destination).
656    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
657    /// removed by a higher-priority layer—only new patterns can be added.
658    /// Default: []
659    pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
660}
661
662#[with_fallible_options]
663#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
664pub struct ToolRegexRule {
665    /// The regex pattern to match.
666    #[serde(default)]
667    pub pattern: String,
668
669    /// Whether the regex is case-sensitive.
670    /// Default: false (case-insensitive)
671    pub case_sensitive: Option<bool>,
672}
673
674#[derive(
675    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
676)]
677#[serde(rename_all = "snake_case")]
678pub enum ToolPermissionMode {
679    /// Auto-approve without prompting.
680    Allow,
681    /// Auto-reject with an error.
682    Deny,
683    /// Always prompt for confirmation (default behavior).
684    #[default]
685    Confirm,
686}
687
688impl std::fmt::Display for ToolPermissionMode {
689    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
690        match self {
691            ToolPermissionMode::Allow => write!(f, "Allow"),
692            ToolPermissionMode::Deny => write!(f, "Deny"),
693            ToolPermissionMode::Confirm => write!(f, "Confirm"),
694        }
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    #[test]
703    fn test_set_tool_default_permission_creates_structure() {
704        let mut settings = AgentSettingsContent::default();
705        assert!(settings.tool_permissions.is_none());
706
707        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
708
709        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
710        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
711        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
712    }
713
714    #[test]
715    fn test_set_tool_default_permission_updates_existing() {
716        let mut settings = AgentSettingsContent::default();
717
718        settings.set_tool_default_permission("terminal", ToolPermissionMode::Confirm);
719        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
720
721        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
722        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
723        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
724    }
725
726    #[test]
727    fn test_set_tool_default_permission_for_mcp_tool() {
728        let mut settings = AgentSettingsContent::default();
729
730        settings.set_tool_default_permission("mcp:github:create_issue", ToolPermissionMode::Allow);
731
732        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
733        let mcp_rules = tool_permissions
734            .tools
735            .get("mcp:github:create_issue")
736            .unwrap();
737        assert_eq!(mcp_rules.default, Some(ToolPermissionMode::Allow));
738    }
739
740    #[test]
741    fn test_add_tool_allow_pattern_creates_structure() {
742        let mut settings = AgentSettingsContent::default();
743        assert!(settings.tool_permissions.is_none());
744
745        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
746
747        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
748        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
749        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
750        assert_eq!(always_allow.0.len(), 1);
751        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
752    }
753
754    #[test]
755    fn test_add_tool_allow_pattern_appends_to_existing() {
756        let mut settings = AgentSettingsContent::default();
757
758        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
759        settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
760
761        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
762        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
763        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
764        assert_eq!(always_allow.0.len(), 2);
765        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
766        assert_eq!(always_allow.0[1].pattern, "^npm\\s");
767    }
768
769    #[test]
770    fn test_add_tool_allow_pattern_does_not_duplicate() {
771        let mut settings = AgentSettingsContent::default();
772
773        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
774        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
775        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
776
777        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
778        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
779        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
780        assert_eq!(
781            always_allow.0.len(),
782            1,
783            "Duplicate patterns should not be added"
784        );
785    }
786
787    #[test]
788    fn test_add_tool_allow_pattern_for_different_tools() {
789        let mut settings = AgentSettingsContent::default();
790
791        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
792        settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
793
794        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
795
796        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
797        assert_eq!(
798            terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
799            "^cargo\\s"
800        );
801
802        let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
803        assert_eq!(
804            fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
805            "^https?://github\\.com"
806        );
807    }
808
809    #[test]
810    fn test_add_tool_deny_pattern_creates_structure() {
811        let mut settings = AgentSettingsContent::default();
812        assert!(settings.tool_permissions.is_none());
813
814        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
815
816        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
817        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
818        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
819        assert_eq!(always_deny.0.len(), 1);
820        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
821    }
822
823    #[test]
824    fn test_add_tool_deny_pattern_appends_to_existing() {
825        let mut settings = AgentSettingsContent::default();
826
827        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
828        settings.add_tool_deny_pattern("terminal", "^sudo\\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        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
833        assert_eq!(always_deny.0.len(), 2);
834        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
835        assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
836    }
837
838    #[test]
839    fn test_add_tool_deny_pattern_does_not_duplicate() {
840        let mut settings = AgentSettingsContent::default();
841
842        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
843        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
844        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
845
846        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
847        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
848        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
849        assert_eq!(
850            always_deny.0.len(),
851            1,
852            "Duplicate patterns should not be added"
853        );
854    }
855
856    #[test]
857    fn test_add_tool_deny_and_allow_patterns_separate() {
858        let mut settings = AgentSettingsContent::default();
859
860        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
861        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
862
863        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
864        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
865
866        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
867        assert_eq!(always_allow.0.len(), 1);
868        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
869
870        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
871        assert_eq!(always_deny.0.len(), 1);
872        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
873    }
874}