mod agent_profile;mod user_agents_md; use std::cmp::Ordering::{Equal, Greater, Less};use std::fmt;use std::path::{Component, Path};use std::sync::{Arc, LazyLock}; use anyhow::Context as _;use collections::{HashSet, IndexMap};use fs::Fs;use futures::channel::oneshot;use gpui::{App, Pixels, SharedString, px};use language_model::LanguageModel;use project::DisableAiSettings;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{ DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, RegisterSetting, Settings, SettingsContent, SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode, update_settings_file, update_settings_file_with_completion,};use util::ResultExt as _; pub use crate::agent_profile::*;pub use crate::user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md}; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = include_str!("prompts/summarize_thread_detailed_prompt.txt");pub const COMPACTION_PROMPT: &str = include_str!("prompts/compaction_prompt.txt"); #[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct PanelLayout { pub(crate) agent_dock: Option<DockPosition>, pub(crate) project_panel_dock: Option<DockSide>, pub(crate) outline_panel_dock: Option<DockSide>, pub(crate) collaboration_panel_dock: Option<DockPosition>, pub(crate) git_panel_dock: Option<DockPosition>,} impl PanelLayout { const AGENT: Self = Self { agent_dock: Some(DockPosition::Left), project_panel_dock: Some(DockSide::Right), outline_panel_dock: Some(DockSide::Right), collaboration_panel_dock: Some(DockPosition::Right), git_panel_dock: Some(DockPosition::Right), }; const EDITOR: Self = Self { agent_dock: Some(DockPosition::Right), project_panel_dock: Some(DockSide::Left), outline_panel_dock: Some(DockSide::Left), collaboration_panel_dock: Some(DockPosition::Left), git_panel_dock: Some(DockPosition::Left), }; pub fn is_agent_layout(&self) -> bool { *self == Self::AGENT } pub fn is_editor_layout(&self) -> bool { *self == Self::EDITOR } fn read_from(content: &SettingsContent) -> Self { Self { agent_dock: content.agent.as_ref().and_then(|a| a.dock), project_panel_dock: content.project_panel.as_ref().and_then(|p| p.dock), outline_panel_dock: content.outline_panel.as_ref().and_then(|p| p.dock), collaboration_panel_dock: content.collaboration_panel.as_ref().and_then(|p| p.dock), git_panel_dock: content.git_panel.as_ref().and_then(|p| p.dock), } } fn write_to(&self, settings: &mut SettingsContent) { settings.agent.get_or_insert_default().dock = self.agent_dock; settings.project_panel.get_or_insert_default().dock = self.project_panel_dock; settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock; settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock; settings.git_panel.get_or_insert_default().dock = self.git_panel_dock; } fn write_diff_to(&self, current_merged: &PanelLayout, settings: &mut SettingsContent) { if self.agent_dock != current_merged.agent_dock { settings.agent.get_or_insert_default().dock = self.agent_dock; } if self.project_panel_dock != current_merged.project_panel_dock { settings.project_panel.get_or_insert_default().dock = self.project_panel_dock; } if self.outline_panel_dock != current_merged.outline_panel_dock { settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock; } if self.collaboration_panel_dock != current_merged.collaboration_panel_dock { settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock; } if self.git_panel_dock != current_merged.git_panel_dock { settings.git_panel.get_or_insert_default().dock = self.git_panel_dock; } } fn backfill_to(&self, user_layout: &PanelLayout, settings: &mut SettingsContent) { if user_layout.agent_dock.is_none() { settings.agent.get_or_insert_default().dock = self.agent_dock; } if user_layout.project_panel_dock.is_none() { settings.project_panel.get_or_insert_default().dock = self.project_panel_dock; } if user_layout.outline_panel_dock.is_none() { settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock; } if user_layout.collaboration_panel_dock.is_none() { settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock; } if user_layout.git_panel_dock.is_none() { settings.git_panel.get_or_insert_default().dock = self.git_panel_dock; } }} #[derive(Debug, Clone, PartialEq, Eq)]pub enum WindowLayout { Editor(Option<PanelLayout>), Agent(Option<PanelLayout>), Custom(PanelLayout),} impl WindowLayout { pub fn agent() -> Self { Self::Agent(None) } pub fn editor() -> Self { Self::Editor(None) }} #[derive(Clone, Copy, Debug, PartialEq)]pub enum AutoCompactThreshold { /// Compact once the context window is at least this full, as a fraction in /// the range `(0.0, 1.0]`. Percentage(f64), /// Compact once at least this many tokens have been used. TokensUsed(u64), /// Compact once fewer than this many tokens remain in the context window. TokensRemaining(u64),} impl AutoCompactThreshold { /// The threshold used when none is configured, or when the configured value /// is invalid (90% of the context window). pub const DEFAULT: Self = Self::Percentage(0.9);} impl fmt::Display for AutoCompactThreshold { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Percentage(percent) => write!(formatter, "{}%", percent * 100.0), Self::TokensUsed(tokens) => write!(formatter, "{tokens}"), Self::TokensRemaining(tokens) => write!(formatter, "-{tokens}"), } }} #[derive(Clone, Copy, Debug, PartialEq)]pub struct AutoCompactSettings { pub enabled: bool, pub threshold: AutoCompactThreshold,} fn parse_auto_compact_threshold(raw: &str) -> anyhow::Result<AutoCompactThreshold> { let trimmed = raw.trim(); if let Some(percent) = trimmed.strip_suffix('%') { let value: f64 = percent .trim_end() .parse() .with_context(|| format!("invalid auto_compact threshold percentage {raw:?}"))?; anyhow::ensure!( value > 0.0 && value <= 100.0, "auto_compact threshold percentage must be between 0% and 100%, got {raw:?}" ); Ok(AutoCompactThreshold::Percentage(value / 100.0)) } else { let tokens: i64 = trimmed.parse().with_context(|| { format!( "invalid auto_compact threshold {raw:?}; \ expected a percentage like \"90%\" or an integer number of tokens" ) })?; match tokens.cmp(&0) { Greater => Ok(AutoCompactThreshold::TokensUsed(tokens as u64)), Less => Ok(AutoCompactThreshold::TokensRemaining(tokens.unsigned_abs())), Equal => { anyhow::bail!("auto_compact threshold of 0 is not valid") } } }} #[derive(Clone, Debug, RegisterSetting)]pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, pub flexible: bool, pub sidebar_side: SidebarDockPosition, pub default_width: Pixels, pub default_height: Pixels, pub max_content_width: Option<Pixels>, pub default_model: Option<LanguageModelSelection>, pub subagent_model: Option<LanguageModelSelection>, pub inline_assistant_model: Option<LanguageModelSelection>, pub inline_assistant_use_streaming_tools: bool, pub commit_message_model: Option<LanguageModelSelection>, pub commit_message_include_project_rules: bool, pub commit_message_instructions: Option<String>, pub thread_summary_model: Option<LanguageModelSelection>, pub compaction_model: Option<LanguageModelSelection>, pub inline_alternatives: Vec<LanguageModelSelection>, pub favorite_models: Vec<LanguageModelSelection>, pub default_profile: AgentProfileId, pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, pub play_sound_when_agent_done: PlaySoundWhenAgentDone, pub single_file_review: bool, pub model_parameters: Vec<LanguageModelParameters>, pub auto_compact: AutoCompactSettings, pub enable_feedback: bool, pub expand_edit_card: bool, pub expand_terminal_card: bool, pub terminal_init_command: Option<String>, pub thinking_display: ThinkingBlockDisplay, pub cancel_generation_on_terminal_stop: bool, pub use_modifier_to_send: bool, pub message_editor_min_lines: usize, pub show_turn_stats: bool, pub show_merge_conflict_indicator: bool, pub tool_permissions: ToolPermissions, pub sandbox_permissions: SandboxPermissions,} impl AgentSettings { pub fn enabled(&self, cx: &App) -> bool { self.enabled && !DisableAiSettings::get_global(cx).disable_ai } pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> { let settings = Self::get_global(cx); for setting in settings.model_parameters.iter().rev() { if let Some(provider) = &setting.provider && provider.0 != model.provider_id().0 { continue; } if let Some(setting_model) = &setting.model && *setting_model != model.id().0 { continue; } return setting.temperature; } return None; } pub fn sidebar_side(&self) -> SidebarSide { match self.sidebar_side { SidebarDockPosition::Left => SidebarSide::Left, SidebarDockPosition::Right => SidebarSide::Right, } } pub fn set_message_editor_max_lines(&self) -> usize { self.message_editor_min_lines * 2 } pub fn favorite_model_ids(&self) -> HashSet<SharedString> { self.favorite_models .iter() .map(|sel| SharedString::from(format!("{}/{}", sel.provider.0, sel.model))) .collect() }} pub fn language_model_to_selection( model: &Arc<dyn LanguageModel>, override_selection: Option<&LanguageModelSelection>,) -> LanguageModelSelection { let provider = model.provider_id().0.to_string().into(); let model_name = model.id().0.to_string(); match override_selection { Some(current) => LanguageModelSelection { provider, model: model_name, enable_thinking: current.enable_thinking && model.supports_thinking(), effort: current .effort .clone() .filter(|value| { model .supported_effort_levels() .iter() .any(|level| level.value.as_ref() == value.as_str()) }) .or_else(|| { model .default_effort_level() .map(|effort| effort.value.to_string()) }), speed: current.speed.filter(|_| model.supports_fast_mode()), }, None => LanguageModelSelection { provider, model: model_name, enable_thinking: model.supports_thinking(), effort: model .default_effort_level() .map(|effort| effort.value.to_string()), speed: None, }, }} impl AgentSettings { pub fn get_layout(cx: &App) -> WindowLayout { let store = cx.global::<SettingsStore>(); let merged = store.merged_settings(); let user_layout = store .raw_user_settings() .map(|u| PanelLayout::read_from(u.content.as_ref())) .unwrap_or_default(); let merged_layout = PanelLayout::read_from(merged); if merged_layout.is_agent_layout() { return WindowLayout::Agent(Some(user_layout)); } if merged_layout.is_editor_layout() { return WindowLayout::Editor(Some(user_layout)); } WindowLayout::Custom(user_layout) } pub fn backfill_editor_layout(fs: Arc<dyn Fs>, cx: &App) { let user_layout = cx .global::<SettingsStore>() .raw_user_settings() .map(|u| PanelLayout::read_from(u.content.as_ref())) .unwrap_or_default(); update_settings_file(fs, cx, move |settings, _cx| { PanelLayout::EDITOR.backfill_to(&user_layout, settings); }); } pub fn set_layout( layout: WindowLayout, fs: Arc<dyn Fs>, cx: &App, ) -> oneshot::Receiver<anyhow::Result<()>> { let merged = PanelLayout::read_from(cx.global::<SettingsStore>().merged_settings()); match layout { WindowLayout::Agent(None) => { update_settings_file_with_completion(fs, cx, move |settings, _cx| { PanelLayout::AGENT.write_diff_to(&merged, settings); }) } WindowLayout::Editor(None) => { update_settings_file_with_completion(fs, cx, move |settings, _cx| { PanelLayout::EDITOR.write_diff_to(&merged, settings); }) } WindowLayout::Agent(Some(saved)) | WindowLayout::Editor(Some(saved)) | WindowLayout::Custom(saved) => { update_settings_file_with_completion(fs, cx, move |settings, _cx| { saved.write_to(settings); }) } } }} #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]pub struct AgentProfileId(pub Arc<str>); impl AgentProfileId { pub fn as_str(&self) -> &str { &self.0 }} impl std::fmt::Display for AgentProfileId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }} impl Default for AgentProfileId { fn default() -> Self { Self("write".into()) }} /// Persistent "allow always" sandbox grants for agent-run terminal commands.////// Coverage decisions for these grants are made in/// `agent::sandboxing::ThreadSandboxGrants::covers_with_persistent`, which/// combines them with the in-memory per-thread grants. `write_paths` are/// stored as minimal, lexically-normalized subtrees (see/// [`compile_sandbox_permissions`]).#[derive(Clone, Debug, PartialEq, Eq)]pub struct SandboxPermissions { /// Allow sandboxed commands to reach any host over the network. pub allow_all_hosts: bool, /// Hosts sandboxed commands may always reach, in canonical form (exact /// hostnames or leading-`*.` subdomain wildcards). Parsed/validated where /// consumed (`agent::sandboxing`). pub network_hosts: Vec<String>, pub allow_fs_write_all: bool, /// Persistently run agent terminal commands outside the OS sandbox. This is /// the model-facing "off switch": when set, the sandboxed terminal tool is /// not exposed and the system prompt omits the sandbox section, so the /// model uses the plain `terminal` tool (on Windows, WSL sandbox setup is /// skipped). Distinct from the model-requested `unsandboxed: true` escape /// approved "once" or "for this thread", which keeps the sandboxed /// tool/prompt in place — see `agent::sandboxing`. pub allow_unsandboxed: bool, /// Directory subtree grants, each paired with the canonical /// (symlink-resolved) target established when the grant was approved. pub write_paths: Vec<settings::GrantedWritePath>, /// Whether sandbox escalation prompts warn about domains or write paths /// that contain potentially confusable Unicode characters (homoglyphs, /// invisible characters, or bidirectional overrides). Enabled by default. pub warn_confusable_unicode: bool, /// Whether to warn (Windows/WSL only) when a sandbox grant targets a file on /// a Windows-hosted (DrvFs) filesystem, whose sandbox-integrity guarantees /// are weaker than a distro-native filesystem. Enabled by default. pub warn_ntfs_grants: bool,} impl Default for SandboxPermissions { fn default() -> Self { Self { allow_all_hosts: false, network_hosts: Vec::new(), allow_fs_write_all: false, allow_unsandboxed: false, write_paths: Vec::new(), // The confusable-Unicode warning is a safety net, so it defaults on. warn_confusable_unicode: true, // The weaker-guarantee warning for Windows-hosted grants defaults on. warn_ntfs_grants: true, } }} #[derive(Clone, Debug, Default)]pub struct ToolPermissions { /// Global default permission when no tool-specific rules or patterns match. pub default: ToolPermissionMode, pub tools: collections::HashMap<Arc<str>, ToolRules>,} impl ToolPermissions { /// Returns all invalid regex patterns across all tools. pub fn invalid_patterns(&self) -> Vec<&InvalidRegexPattern> { self.tools .values() .flat_map(|rules| rules.invalid_patterns.iter()) .collect() } /// Returns true if any tool has invalid regex patterns. pub fn has_invalid_patterns(&self) -> bool { self.tools .values() .any(|rules| !rules.invalid_patterns.is_empty()) }} /// Represents a regex pattern that failed to compile.#[derive(Clone, Debug)]pub struct InvalidRegexPattern { /// The pattern string that failed to compile. pub pattern: String, /// Which rule list this pattern was in (e.g., "always_deny", "always_allow", "always_confirm"). pub rule_type: String, /// The error message from the regex compiler. pub error: String,} #[derive(Clone, Debug, Default)]pub struct ToolRules {Lines 1–500 of 1976. Load more
mod agent_profile; mod user_agents_md; use std::cmp::Ordering::{Equal, Greater, Less}; use std::fmt; use std::path::{Component, Path}; use std::sync::{Arc, LazyLock}; use anyhow::Context as _; use collections::{HashSet, IndexMap}; use fs::Fs; use futures::channel::oneshot; use gpui::{App, Pixels, SharedString, px}; use language_model::LanguageModel; use project::DisableAiSettings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, RegisterSetting, Settings, SettingsContent, SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode, update_settings_file, update_settings_file_with_completion, }; use util::ResultExt as _; pub use crate::agent_profile::*; pub use crate::user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md}; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt"); pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = include_str!("prompts/summarize_thread_detailed_prompt.txt"); pub const COMPACTION_PROMPT: &str = include_str!("prompts/compaction_prompt.txt"); #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PanelLayout { pub(crate) agent_dock: Option<DockPosition>, pub(crate) project_panel_dock: Option<DockSide>, pub(crate) outline_panel_dock: Option<DockSide>, pub(crate) collaboration_panel_dock: Option<DockPosition>, pub(crate) git_panel_dock: Option<DockPosition>, } impl PanelLayout { const AGENT: Self = Self { agent_dock: Some(DockPosition::Left), project_panel_dock: Some(DockSide::Right), outline_panel_dock: Some(DockSide::Right), collaboration_panel_dock: Some(DockPosition::Right), git_panel_dock: Some(DockPosition::Right), }; const EDITOR: Self = Self { agent_dock: Some(DockPosition::Right), project_panel_dock: Some(DockSide::Left), outline_panel_dock: Some(DockSide::Left), collaboration_panel_dock: Some(DockPosition::Left), git_panel_dock: Some(DockPosition::Left), }; pub fn is_agent_layout(&self) -> bool { *self == Self::AGENT } pub fn is_editor_layout(&self) -> bool { *self == Self::EDITOR } fn read_from(content: &SettingsContent) -> Self { Self { agent_dock: content.agent.as_ref().and_then(|a| a.dock), project_panel_dock: content.project_panel.as_ref().and_then(|p| p.dock), outline_panel_dock: content.outline_panel.as_ref().and_then(|p| p.dock), collaboration_panel_dock: content.collaboration_panel.as_ref().and_then(|p| p.dock), git_panel_dock: content.git_panel.as_ref().and_then(|p| p.dock), } } fn write_to(&self, settings: &mut SettingsContent) { settings.agent.get_or_insert_default().dock = self.agent_dock; settings.project_panel.get_or_insert_default().dock = self.project_panel_dock; settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock; settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock; settings.git_panel.get_or_insert_default().dock = self.git_panel_dock; } fn write_diff_to(&self, current_merged: &PanelLayout, settings: &mut SettingsContent) { if self.agent_dock != current_merged.agent_dock { settings.agent.get_or_insert_default().dock = self.agent_dock; } if self.project_panel_dock != current_merged.project_panel_dock { settings.project_panel.get_or_insert_default().dock = self.project_panel_dock; } if self.outline_panel_dock != current_merged.outline_panel_dock { settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock; } if self.collaboration_panel_dock != current_merged.collaboration_panel_dock { settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock; } if self.git_panel_dock != current_merged.git_panel_dock { settings.git_panel.get_or_insert_default().dock = self.git_panel_dock; } } fn backfill_to(&self, user_layout: &PanelLayout, settings: &mut SettingsContent) { if user_layout.agent_dock.is_none() { settings.agent.get_or_insert_default().dock = self.agent_dock; } if user_layout.project_panel_dock.is_none() { settings.project_panel.get_or_insert_default().dock = self.project_panel_dock; } if user_layout.outline_panel_dock.is_none() { settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock; } if user_layout.collaboration_panel_dock.is_none() { settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock; } if user_layout.git_panel_dock.is_none() { settings.git_panel.get_or_insert_default().dock = self.git_panel_dock; } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum WindowLayout { Editor(Option<PanelLayout>), Agent(Option<PanelLayout>), Custom(PanelLayout), } impl WindowLayout { pub fn agent() -> Self { Self::Agent(None) } pub fn editor() -> Self { Self::Editor(None) } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum AutoCompactThreshold { /// Compact once the context window is at least this full, as a fraction in /// the range `(0.0, 1.0]`. Percentage(f64), /// Compact once at least this many tokens have been used. TokensUsed(u64), /// Compact once fewer than this many tokens remain in the context window. TokensRemaining(u64), } impl AutoCompactThreshold { /// The threshold used when none is configured, or when the configured value /// is invalid (90% of the context window). pub const DEFAULT: Self = Self::Percentage(0.9); } impl fmt::Display for AutoCompactThreshold { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Percentage(percent) => write!(formatter, "{}%", percent * 100.0), Self::TokensUsed(tokens) => write!(formatter, "{tokens}"), Self::TokensRemaining(tokens) => write!(formatter, "-{tokens}"), } } } #[derive(Clone, Copy, Debug, PartialEq)] pub struct AutoCompactSettings { pub enabled: bool, pub threshold: AutoCompactThreshold, } fn parse_auto_compact_threshold(raw: &str) -> anyhow::Result<AutoCompactThreshold> { let trimmed = raw.trim(); if let Some(percent) = trimmed.strip_suffix('%') { let value: f64 = percent .trim_end() .parse() .with_context(|| format!("invalid auto_compact threshold percentage {raw:?}"))?; anyhow::ensure!( value > 0.0 && value <= 100.0, "auto_compact threshold percentage must be between 0% and 100%, got {raw:?}" ); Ok(AutoCompactThreshold::Percentage(value / 100.0)) } else { let tokens: i64 = trimmed.parse().with_context(|| { format!( "invalid auto_compact threshold {raw:?}; \ expected a percentage like \"90%\" or an integer number of tokens" ) })?; match tokens.cmp(&0) { Greater => Ok(AutoCompactThreshold::TokensUsed(tokens as u64)), Less => Ok(AutoCompactThreshold::TokensRemaining(tokens.unsigned_abs())), Equal => { anyhow::bail!("auto_compact threshold of 0 is not valid") } } } } #[derive(Clone, Debug, RegisterSetting)] pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, pub flexible: bool, pub sidebar_side: SidebarDockPosition, pub default_width: Pixels, pub default_height: Pixels, pub max_content_width: Option<Pixels>, pub default_model: Option<LanguageModelSelection>, pub subagent_model: Option<LanguageModelSelection>, pub inline_assistant_model: Option<LanguageModelSelection>, pub inline_assistant_use_streaming_tools: bool, pub commit_message_model: Option<LanguageModelSelection>, pub commit_message_include_project_rules: bool, pub commit_message_instructions: Option<String>, pub thread_summary_model: Option<LanguageModelSelection>, pub compaction_model: Option<LanguageModelSelection>, pub inline_alternatives: Vec<LanguageModelSelection>, pub favorite_models: Vec<LanguageModelSelection>, pub default_profile: AgentProfileId, pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, pub play_sound_when_agent_done: PlaySoundWhenAgentDone, pub single_file_review: bool, pub model_parameters: Vec<LanguageModelParameters>, pub auto_compact: AutoCompactSettings, pub enable_feedback: bool, pub expand_edit_card: bool, pub expand_terminal_card: bool, pub terminal_init_command: Option<String>, pub thinking_display: ThinkingBlockDisplay, pub cancel_generation_on_terminal_stop: bool, pub use_modifier_to_send: bool, pub message_editor_min_lines: usize, pub show_turn_stats: bool, pub show_merge_conflict_indicator: bool, pub tool_permissions: ToolPermissions, pub sandbox_permissions: SandboxPermissions, } impl AgentSettings { pub fn enabled(&self, cx: &App) -> bool { self.enabled && !DisableAiSettings::get_global(cx).disable_ai } pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> { let settings = Self::get_global(cx); for setting in settings.model_parameters.iter().rev() { if let Some(provider) = &setting.provider && provider.0 != model.provider_id().0 { continue; } if let Some(setting_model) = &setting.model && *setting_model != model.id().0 { continue; } return setting.temperature; } return None; } pub fn sidebar_side(&self) -> SidebarSide { match self.sidebar_side { SidebarDockPosition::Left => SidebarSide::Left, SidebarDockPosition::Right => SidebarSide::Right, } } pub fn set_message_editor_max_lines(&self) -> usize { self.message_editor_min_lines * 2 } pub fn favorite_model_ids(&self) -> HashSet<SharedString> { self.favorite_models .iter() .map(|sel| SharedString::from(format!("{}/{}", sel.provider.0, sel.model))) .collect() } } pub fn language_model_to_selection( model: &Arc<dyn LanguageModel>, override_selection: Option<&LanguageModelSelection>, ) -> LanguageModelSelection { let provider = model.provider_id().0.to_string().into(); let model_name = model.id().0.to_string(); match override_selection { Some(current) => LanguageModelSelection { provider, model: model_name, enable_thinking: current.enable_thinking && model.supports_thinking(), effort: current .effort .clone() .filter(|value| { model .supported_effort_levels() .iter() .any(|level| level.value.as_ref() == value.as_str()) }) .or_else(|| { model .default_effort_level() .map(|effort| effort.value.to_string()) }), speed: current.speed.filter(|_| model.supports_fast_mode()), }, None => LanguageModelSelection { provider, model: model_name, enable_thinking: model.supports_thinking(), effort: model .default_effort_level() .map(|effort| effort.value.to_string()), speed: None, }, } } impl AgentSettings { pub fn get_layout(cx: &App) -> WindowLayout { let store = cx.global::<SettingsStore>(); let merged = store.merged_settings(); let user_layout = store .raw_user_settings() .map(|u| PanelLayout::read_from(u.content.as_ref())) .unwrap_or_default(); let merged_layout = PanelLayout::read_from(merged); if merged_layout.is_agent_layout() { return WindowLayout::Agent(Some(user_layout)); } if merged_layout.is_editor_layout() { return WindowLayout::Editor(Some(user_layout)); } WindowLayout::Custom(user_layout) } pub fn backfill_editor_layout(fs: Arc<dyn Fs>, cx: &App) { let user_layout = cx .global::<SettingsStore>() .raw_user_settings() .map(|u| PanelLayout::read_from(u.content.as_ref())) .unwrap_or_default(); update_settings_file(fs, cx, move |settings, _cx| { PanelLayout::EDITOR.backfill_to(&user_layout, settings); }); } pub fn set_layout( layout: WindowLayout, fs: Arc<dyn Fs>, cx: &App, ) -> oneshot::Receiver<anyhow::Result<()>> { let merged = PanelLayout::read_from(cx.global::<SettingsStore>().merged_settings()); match layout { WindowLayout::Agent(None) => { update_settings_file_with_completion(fs, cx, move |settings, _cx| { PanelLayout::AGENT.write_diff_to(&merged, settings); }) } WindowLayout::Editor(None) => { update_settings_file_with_completion(fs, cx, move |settings, _cx| { PanelLayout::EDITOR.write_diff_to(&merged, settings); }) } WindowLayout::Agent(Some(saved)) | WindowLayout::Editor(Some(saved)) | WindowLayout::Custom(saved) => { update_settings_file_with_completion(fs, cx, move |settings, _cx| { saved.write_to(settings); }) } } } } #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] pub struct AgentProfileId(pub Arc<str>); impl AgentProfileId { pub fn as_str(&self) -> &str { &self.0 } } impl std::fmt::Display for AgentProfileId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Default for AgentProfileId { fn default() -> Self { Self("write".into()) } } /// Persistent "allow always" sandbox grants for agent-run terminal commands. /// /// Coverage decisions for these grants are made in /// `agent::sandboxing::ThreadSandboxGrants::covers_with_persistent`, which /// combines them with the in-memory per-thread grants. `write_paths` are /// stored as minimal, lexically-normalized subtrees (see /// [`compile_sandbox_permissions`]). #[derive(Clone, Debug, PartialEq, Eq)] pub struct SandboxPermissions { /// Allow sandboxed commands to reach any host over the network. pub allow_all_hosts: bool, /// Hosts sandboxed commands may always reach, in canonical form (exact /// hostnames or leading-`*.` subdomain wildcards). Parsed/validated where /// consumed (`agent::sandboxing`). pub network_hosts: Vec<String>, pub allow_fs_write_all: bool, /// Persistently run agent terminal commands outside the OS sandbox. This is /// the model-facing "off switch": when set, the sandboxed terminal tool is /// not exposed and the system prompt omits the sandbox section, so the /// model uses the plain `terminal` tool (on Windows, WSL sandbox setup is /// skipped). Distinct from the model-requested `unsandboxed: true` escape /// approved "once" or "for this thread", which keeps the sandboxed /// tool/prompt in place — see `agent::sandboxing`. pub allow_unsandboxed: bool, /// Directory subtree grants, each paired with the canonical /// (symlink-resolved) target established when the grant was approved. pub write_paths: Vec<settings::GrantedWritePath>, /// Whether sandbox escalation prompts warn about domains or write paths /// that contain potentially confusable Unicode characters (homoglyphs, /// invisible characters, or bidirectional overrides). Enabled by default. pub warn_confusable_unicode: bool, /// Whether to warn (Windows/WSL only) when a sandbox grant targets a file on /// a Windows-hosted (DrvFs) filesystem, whose sandbox-integrity guarantees /// are weaker than a distro-native filesystem. Enabled by default. pub warn_ntfs_grants: bool, } impl Default for SandboxPermissions { fn default() -> Self { Self { allow_all_hosts: false, network_hosts: Vec::new(), allow_fs_write_all: false, allow_unsandboxed: false, write_paths: Vec::new(), // The confusable-Unicode warning is a safety net, so it defaults on. warn_confusable_unicode: true, // The weaker-guarantee warning for Windows-hosted grants defaults on. warn_ntfs_grants: true, } } } #[derive(Clone, Debug, Default)] pub struct ToolPermissions { /// Global default permission when no tool-specific rules or patterns match. pub default: ToolPermissionMode, pub tools: collections::HashMap<Arc<str>, ToolRules>, } impl ToolPermissions { /// Returns all invalid regex patterns across all tools. pub fn invalid_patterns(&self) -> Vec<&InvalidRegexPattern> { self.tools .values() .flat_map(|rules| rules.invalid_patterns.iter()) .collect() } /// Returns true if any tool has invalid regex patterns. pub fn has_invalid_patterns(&self) -> bool { self.tools .values() .any(|rules| !rules.invalid_patterns.is_empty()) } } /// Represents a regex pattern that failed to compile. #[derive(Clone, Debug)] pub struct InvalidRegexPattern { /// The pattern string that failed to compile. pub pattern: String, /// Which rule list this pattern was in (e.g., "always_deny", "always_allow", "always_confirm"). pub rule_type: String, /// The error message from the regex compiler. pub error: String, } #[derive(Clone, Debug, Default)] pub struct ToolRules {