//! Settings for Blocklist AI. //! //! These settings are currently used to configure the underlying model/API used to power the AI //! UX, as well as small UX configurations. use std::collections::HashMap; use std::path::PathBuf; use chrono::{DateTime, Utc}; pub use cloud_object_models::{ AgentModeCommandExecutionPredicate, DEFAULT_COMMAND_EXECUTION_ALLOWLIST, DEFAULT_COMMAND_EXECUTION_DENYLIST, }; use galaxy_core::execution_mode::AppExecutionMode; use galaxy_core::features::FeatureFlag; use indexmap::IndexMap; use regex::Regex; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; use settings::{ define_settings_group, ChangeEventReason, RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud, }; use strum::IntoEnumIterator; use strum_macros::EnumIter; use warpui::platform::keyboard::KeyCode; use warpui::platform::OperatingSystem; use warpui::{AppContext, Entity, ModelContext, SingletonEntity, UpdateModel}; use crate::ai::request_usage_model::RequestLimitInfo; use crate::report_if_error; use crate::settings::PrivacySettings; use crate::terminal::CLIAgent; use crate::workspaces::user_workspaces::UserWorkspaces; pub enum FocusedTerminalInfoEvent { TerminalInfoUpdated, } /// Singleton model that is used to track the remote sessions in the terminal. /// Useful for organizations that have restrictions on using AI in sessions in /// remote sessions. #[derive(Default, Clone, Debug)] pub struct FocusedTerminalInfo { contains_any_remote_blocks: bool, contains_any_restored_remote_blocks: bool, } impl FocusedTerminalInfo { pub fn new(_: &mut ModelContext) -> Self { Self { contains_any_remote_blocks: false, contains_any_restored_remote_blocks: false, } } pub fn contains_any_remote_blocks(&self) -> bool { self.contains_any_remote_blocks } pub fn contains_any_restored_remote_blocks(&self) -> bool { self.contains_any_restored_remote_blocks } /// Updates both remote blocks and restored blocks status in a single atomic operation. /// Only emits a TerminalInfoUpdated event if either value changes. /// Returns true if the event was emitted. pub fn update( &mut self, contains_any_remote_blocks: bool, contains_any_restored_remote_blocks: bool, ctx: &mut ModelContext, ) -> bool { let remote_changed = self.contains_any_remote_blocks != contains_any_remote_blocks; let restored_changed = self.contains_any_restored_remote_blocks != contains_any_restored_remote_blocks; if remote_changed || restored_changed { self.contains_any_remote_blocks = contains_any_remote_blocks; self.contains_any_restored_remote_blocks = contains_any_restored_remote_blocks; ctx.emit(FocusedTerminalInfoEvent::TerminalInfoUpdated); return true; } false } } impl Entity for FocusedTerminalInfo { type Event = FocusedTerminalInfoEvent; } impl SingletonEntity for FocusedTerminalInfo {} #[derive( Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "Physical key used to toggle voice input.", rename_all = "snake_case" )] pub enum VoiceInputToggleKey { #[default] #[schemars(description = "No toggle key assigned.")] None, /// Fn key is default toggle key for Mac, when the feature is toggled on. #[schemars(description = "Fn key.")] Fn, /// Alt or Option key (left side). #[schemars(description = "Alt or Option key (left side).")] AltLeft, /// Alt or Option key (right side). Used as default toggle /// key for Windows and Linux, , when the feature is toggled on. #[schemars(description = "Alt or Option key (right side).")] AltRight, #[schemars(description = "Control key (left side).")] ControlLeft, #[schemars(description = "Control key (right side).")] ControlRight, /// The Windows, ⌘, Command, or other OS symbol key. #[schemars(description = "Super, Windows, or Command key (left side).")] SuperLeft, /// The Windows, ⌘, Command, or other OS symbol key. #[schemars(description = "Super, Windows, or Command key (right side).")] SuperRight, #[schemars(description = "Shift key (left side).")] ShiftLeft, #[schemars(description = "Shift key (right side).")] ShiftRight, } settings::macros::implement_setting_for_enum!( VoiceInputToggleKey, AISettings, SupportedPlatforms::DESKTOP, // Never sync to cloud to allow users to use different toggle keys on different devices, // especially given platform differences. SyncToCloud::Never, private: false, toml_path: "agents.voice.voice_input_toggle_key", description: "The key used to toggle voice input.", ); impl VoiceInputToggleKey { pub fn all_possible_values() -> Vec { let all_keys = VoiceInputToggleKey::iter().collect(); match OperatingSystem::get() { OperatingSystem::Mac => all_keys, // For non-Mac platforms, we exclude the `Fn` key since it may not be correctly identified by winit. // In particular, we saw it is unidentified for a ThinkPad with a standard keyboard. OperatingSystem::Windows | OperatingSystem::Linux | OperatingSystem::Other(_) => { all_keys .into_iter() .filter(|key| *key != VoiceInputToggleKey::Fn) .collect() } } } /// Display name for choosing key from the AI settings page. pub fn display_name(&self) -> &'static str { // We use the underlying host OS to determine the correct key name to display. let (super_key_name, alt_key_name): (&'static str, &'static str) = match OperatingSystem::get() { OperatingSystem::Mac => ("Command", "Option"), OperatingSystem::Windows => ("Windows", "Alt"), OperatingSystem::Linux | OperatingSystem::Other(_) => ("Super", "Alt"), }; match self { VoiceInputToggleKey::None => "None", VoiceInputToggleKey::Fn => "Fn", VoiceInputToggleKey::AltLeft => { Box::leak(format!("{alt_key_name} (Left)").into_boxed_str()) } VoiceInputToggleKey::AltRight => { Box::leak(format!("{alt_key_name} (Right)").into_boxed_str()) } VoiceInputToggleKey::ControlLeft => "Control (Left)", VoiceInputToggleKey::ControlRight => "Control (Right)", VoiceInputToggleKey::SuperLeft => { Box::leak(format!("{super_key_name} (Left)").into_boxed_str()) } VoiceInputToggleKey::SuperRight => { Box::leak(format!("{super_key_name} (Right)").into_boxed_str()) } VoiceInputToggleKey::ShiftLeft => "Shift (Left)", VoiceInputToggleKey::ShiftRight => "Shift (Right)", } } pub fn to_key_code(&self) -> Option { match self { VoiceInputToggleKey::None => None, VoiceInputToggleKey::Fn => Some(KeyCode::Fn), VoiceInputToggleKey::AltLeft => Some(KeyCode::AltLeft), VoiceInputToggleKey::AltRight => Some(KeyCode::AltRight), VoiceInputToggleKey::ControlLeft => Some(KeyCode::ControlLeft), VoiceInputToggleKey::ControlRight => Some(KeyCode::ControlRight), VoiceInputToggleKey::SuperLeft => Some(KeyCode::SuperLeft), VoiceInputToggleKey::SuperRight => Some(KeyCode::SuperRight), VoiceInputToggleKey::ShiftLeft => Some(KeyCode::ShiftLeft), VoiceInputToggleKey::ShiftRight => Some(KeyCode::ShiftRight), } } /// Converts the voice input toggle key to a Keystroke representation. /// Since these are standalone modifier keys, we construct the Keystroke directly /// rather than using `parse()` (which always requires a non-modifier key to be included). pub fn keystroke(&self) -> Option { use galaxyui::keymap::Keystroke; let keystroke = match self { VoiceInputToggleKey::None => return None, VoiceInputToggleKey::Fn => Keystroke { key: "fn".to_string(), ..Default::default() }, VoiceInputToggleKey::AltLeft | VoiceInputToggleKey::AltRight => Keystroke { alt: true, ..Default::default() }, VoiceInputToggleKey::ControlLeft | VoiceInputToggleKey::ControlRight => Keystroke { ctrl: true, ..Default::default() }, VoiceInputToggleKey::SuperLeft | VoiceInputToggleKey::SuperRight => Keystroke { cmd: true, ..Default::default() }, VoiceInputToggleKey::ShiftLeft | VoiceInputToggleKey::ShiftRight => Keystroke { shift: true, ..Default::default() }, }; Some(keystroke) } pub fn tooltip_message(&self) -> String { match self.keystroke() { Some(keystroke) => { let symbol = keystroke.displayed(); let side = match self { VoiceInputToggleKey::AltLeft | VoiceInputToggleKey::ControlLeft | VoiceInputToggleKey::SuperLeft | VoiceInputToggleKey::ShiftLeft => Some("Left"), VoiceInputToggleKey::AltRight | VoiceInputToggleKey::ControlRight | VoiceInputToggleKey::SuperRight | VoiceInputToggleKey::ShiftRight => Some("Right"), VoiceInputToggleKey::None | VoiceInputToggleKey::Fn => None, }; let key_name = match side { Some(side) => format!("{side} {symbol}"), None => symbol, }; format!("Voice input (hold {key_name} key)") } None => "Voice input".to_string(), } } pub fn is_none(&self) -> bool { matches!(self, VoiceInputToggleKey::None) } } /// The default mode for new terminal sessions. #[derive( Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "Default mode for new sessions.", rename_all = "snake_case" )] pub enum DefaultSessionMode { /// New sessions start in the terminal mode (default). #[default] Terminal, /// New sessions start in agent view. Agent, /// New sessions start in cloud (ambient) agent mode. CloudAgent, /// New sessions open a user-defined tab config. /// The specific config is identified by the companion `default_tab_config_path` setting. TabConfig, /// New sessions open in a local Docker sandbox. /// Requires the `LocalDockerSandbox` feature flag; falls back to `Terminal` when disabled. DockerSandbox, } settings::macros::implement_setting_for_enum!( DefaultSessionMode, AISettings, SupportedPlatforms::ALL, SyncToCloud::Never, private: false, toml_path: "general.default_session_mode", description: "The default mode for new terminal sessions.", ); impl DefaultSessionMode { /// Display name for the settings dropdown. pub fn display_name(&self) -> &'static str { match self { DefaultSessionMode::Terminal => "Terminal", DefaultSessionMode::Agent => "Agent", DefaultSessionMode::CloudAgent => "Cloud Oz", DefaultSessionMode::TabConfig => "Tab Config", DefaultSessionMode::DockerSandbox => "Local Docker Sandbox", } } } /// Controls how agent thinking/reasoning traces are displayed after streaming. #[derive( Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "Controls how agent thinking is displayed after streaming.", rename_all = "snake_case" )] pub enum ThinkingDisplayMode { /// Show reasoning blocks while streaming, then collapse them when complete (default). #[default] ShowAndCollapse, /// Always keep reasoning blocks expanded, even after streaming finishes. AlwaysShow, /// Never show reasoning blocks. NeverShow, } settings::macros::implement_setting_for_enum!( ThinkingDisplayMode, AISettings, SupportedPlatforms::ALL, SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.other.thinking_display_mode", description: "Controls how agent thinking traces are displayed after streaming.", ); impl ThinkingDisplayMode { /// Display name for the settings dropdown. pub fn display_name(&self) -> &'static str { match self { ThinkingDisplayMode::ShowAndCollapse => "Show & collapse", ThinkingDisplayMode::AlwaysShow => "Always show", ThinkingDisplayMode::NeverShow => "Never show", } } pub fn command_palette_description(&self) -> &'static str { match self { ThinkingDisplayMode::ShowAndCollapse => "Set agent thinking display: show & collapse", ThinkingDisplayMode::AlwaysShow => "Set agent thinking display: always show", ThinkingDisplayMode::NeverShow => "Set agent thinking display: never show", } } pub fn should_render(&self) -> bool { !matches!(self, ThinkingDisplayMode::NeverShow) } pub fn should_keep_expanded(&self) -> bool { matches!(self, ThinkingDisplayMode::AlwaysShow) } } /// Controls how child-agent message bodies are displayed. #[derive( Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "Controls how child-agent messages are displayed.", rename_all = "snake_case" )] pub enum OrchestrationMessageDisplayMode { /// Show child-agent messages while streaming, then collapse them. ShowAndCollapse, /// Keep child-agent message bodies expanded. AlwaysShow, /// Keep child-agent message bodies collapsed. #[default] AlwaysCollapse, } settings::macros::implement_setting_for_enum!( OrchestrationMessageDisplayMode, AISettings, SupportedPlatforms::ALL, SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.orchestration_message_display_mode", description: "Controls how child-agent messages are displayed.", ); impl OrchestrationMessageDisplayMode { /// Display name for the settings dropdown. pub fn display_name(&self) -> &'static str { match self { OrchestrationMessageDisplayMode::ShowAndCollapse => "Show & collapse", OrchestrationMessageDisplayMode::AlwaysShow => "Always show", OrchestrationMessageDisplayMode::AlwaysCollapse => "Always collapse", } } pub fn command_palette_description(&self) -> &'static str { match self { OrchestrationMessageDisplayMode::ShowAndCollapse => { "Set child-agent message display: show & collapse" } OrchestrationMessageDisplayMode::AlwaysShow => { "Set child-agent message display: always show" } OrchestrationMessageDisplayMode::AlwaysCollapse => { "Set child-agent message display: always collapse" } } } /// Whether child-agent message bodies should expand while streaming. pub fn should_expand_agent_message_body(&self) -> bool { matches!( self, OrchestrationMessageDisplayMode::ShowAndCollapse | OrchestrationMessageDisplayMode::AlwaysShow ) } /// Whether child-agent message bodies should collapse after streaming. pub fn should_collapse_agent_message_body_on_finish(&self) -> bool { matches!(self, OrchestrationMessageDisplayMode::ShowAndCollapse) } } /// Controls what happens when a user submits a new prompt while the agent is /// still responding to an earlier prompt. /// /// This is the *default* used when a conversation has no explicit auto-queue /// override. Per-conversation overrides live on `QueuedQueryModel` and take /// precedence over this setting. #[derive( Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "Default behavior when submitting a new prompt while the agent is still responding.", rename_all = "snake_case" )] pub enum PromptSubmissionMode { /// Cancel the in-flight response and submit the new prompt immediately /// (default). #[default] Interrupt, /// Hold the new prompt until the in-flight response finishes, then submit. Queue, } settings::macros::implement_setting_for_enum!( PromptSubmissionMode, AISettings, SupportedPlatforms::ALL, SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.default_prompt_submission_mode", description: "Default behavior when submitting a new prompt while the agent is still responding.", feature_flag: FeatureFlag::QueueSlashCommand, ); impl PromptSubmissionMode { /// Display name for the settings dropdown. pub fn display_name(&self) -> &'static str { match self { PromptSubmissionMode::Interrupt => "Interrupt response", PromptSubmissionMode::Queue => "Queue until response finishes", } } pub fn command_palette_description(&self) -> &'static str { match self { PromptSubmissionMode::Interrupt => "Set default prompt submission: interrupt response", PromptSubmissionMode::Queue => { "Set default prompt submission: queue until response finishes" } } } } /// What happens when a prompt is submitted while an agent controls an agent-requested /// long-running command (LRC). /// /// Only consulted when [`PromptSubmissionMode`] is `Interrupt`: in `Queue` mode /// prompts always queue until the full response finishes, so this setting is /// hidden and ignored. #[derive( Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "What happens when a prompt is submitted while an agent controls an agent-requested long-running command.", rename_all = "snake_case" )] pub enum LongRunningCommandSubmissionMode { /// Send the prompt to the agent immediately, steering it mid-command. SendImmediately, /// Queue the prompt and send it to the agent when the command finishes /// (default). #[default] QueueUntilCommandCompletes, } settings::macros::implement_setting_for_enum!( LongRunningCommandSubmissionMode, AISettings, SupportedPlatforms::ALL, SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.long_running_command_submission_mode", description: "What happens when a prompt is submitted while an agent controls an agent-requested long-running command.", feature_flag: FeatureFlag::QueueSlashCommand, ); impl LongRunningCommandSubmissionMode { /// Display name for the settings dropdown. pub fn display_name(&self) -> &'static str { match self { LongRunningCommandSubmissionMode::SendImmediately => "Send immediately", LongRunningCommandSubmissionMode::QueueUntilCommandCompletes => { "Queue until command finishes" } } } pub fn command_palette_description(&self) -> &'static str { match self { LongRunningCommandSubmissionMode::SendImmediately => { "Set long-running command submission: send immediately" } LongRunningCommandSubmissionMode::QueueUntilCommandCompletes => { "Set long-running command submission: queue until command finishes" } } } } /// Tracks the state of the quota reset banner #[derive( Debug, Serialize, Deserialize, Clone, PartialEq, Default, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "State of the quota reset banner.")] pub struct BannerState { #[serde(default)] #[schemars(description = "Whether the banner has been dismissed.")] pub dismissed: bool, } /// Tracks information about a single billing cycle for AI request usage #[derive( Debug, Serialize, Deserialize, Clone, PartialEq, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "Information about a single billing cycle.")] pub struct CycleInfo { /// End date of the billing cycle #[schemars(description = "End date of the billing cycle.")] pub end_date: DateTime, /// Whether the quota was exceeded in this cycle #[schemars(description = "Whether the usage quota was exceeded in this cycle.")] pub was_quota_exceeded: bool, /// State of the quota reset banner #[schemars(description = "State of the quota reset banner for this cycle.")] pub banner_state: BannerState, } #[derive( Debug, Serialize, Deserialize, Clone, Default, PartialEq, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "AI usage quota information across billing cycles.")] pub struct AIRequestQuotaInfo { /// History of billing cycles and their usage. /// /// Note that these are only populated going forward from when this setting /// was introduced. #[schemars(description = "History of billing cycles and their quota usage.")] pub cycle_history: Vec, } #[derive( Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "File read permission level for the agent.", rename_all = "snake_case" )] pub enum AgentModeCodingPermissionsType { /// Agent Mode must ask for explicit permission for any type of file read. #[default] AlwaysAskBeforeReading, /// Agent Mode can always read files without explicit consent. AlwaysAllowReading, /// Agent Mode can only read certain files without explicit consent. /// /// The specific filepaths are backed by the /// [`AISettings::agent_mode_coding_file_read_allowlist`] setting. AllowReadingSpecificFiles, } /// Maps custom toolbar command regex patterns to CLI agent names. /// Keys are regex patterns (insertion-ordered), values are serialized CLIAgent names (e.g. "Claude"). /// An empty string value means "Any CLI Agent" (CLIAgent::Unknown). /// /// Uses `IndexMap` to preserve insertion order so the settings UI list is deterministic. /// Supports backward-compatible deserialization from the legacy `Vec` format, /// where each string is converted to a key with an empty agent value. #[derive(Debug, Clone, Default, PartialEq, Serialize)] #[serde(transparent)] pub struct ToolbarCommandMap(IndexMap); impl ToolbarCommandMap { pub(crate) fn new(map: IndexMap) -> Self { Self(map) } } impl<'de> Deserialize<'de> for ToolbarCommandMap { fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(untagged)] enum MapOrVec { Map(IndexMap), Vec(Vec), } match MapOrVec::deserialize(deserializer) { Ok(MapOrVec::Map(map)) => Ok(ToolbarCommandMap::new(map)), Ok(MapOrVec::Vec(vec)) => { let map = vec .into_iter() .map(|pattern| (pattern, String::new())) .collect(); Ok(ToolbarCommandMap::new(map)) } Err(e) => Err(e), } } } impl schemars::JsonSchema for ToolbarCommandMap { fn schema_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed("ToolbarCommandMap") } fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { generator.subschema_for::>() } } impl std::ops::Deref for ToolbarCommandMap { type Target = IndexMap; fn deref(&self) -> &Self::Target { &self.0 } } impl settings_value::SettingsValue for ToolbarCommandMap { fn to_file_value(&self) -> serde_json::Value { serde_json::to_value(&self.0).unwrap_or_default() } fn from_file_value(value: &serde_json::Value) -> Option { // Try map format first (using from_value to preserve insertion order), then legacy array format. if value.is_object() { if let Ok(map) = serde_json::from_value::>(value.clone()) { return Some(ToolbarCommandMap::new(map)); } } if let Some(arr) = value.as_array() { let result: IndexMap = arr .iter() .filter_map(|v| v.as_str().map(|s| (s.to_string(), String::new()))) .collect(); return Some(ToolbarCommandMap::new(result)); } None } } #[derive( Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "Authentication method for AWS Bedrock.", rename_all = "snake_case" )] pub enum BedrockAuthMethod { #[default] #[serde(alias = "profile")] Profile, #[serde(alias = "static_keys")] StaticKeys, #[serde(alias = "sso")] Sso, } settings::macros::implement_setting_for_enum!( BedrockAuthMethod, AISettings, SupportedPlatforms::DESKTOP, SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "ai.bedrock.auth_method", description: "Authentication method for AWS Bedrock.", ); impl BedrockAuthMethod { pub fn display_name(&self) -> &'static str { match self { BedrockAuthMethod::Profile => "AWS Profile", BedrockAuthMethod::StaticKeys => "Static Keys", BedrockAuthMethod::Sso => "SSO", } } } /// Configuration for a single Bedrock model. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] #[schemars(description = "Configuration for a single AWS Bedrock model.")] pub struct BedrockModelConfig { #[schemars( description = "The Bedrock model ID (e.g. anthropic.claude-sonnet-4-20250514-v1:0)." )] pub model_id: String, #[schemars(description = "Display name shown in the model picker.")] pub display_name: String, #[serde(default)] #[schemars(description = "Whether the model supports image/vision input.")] pub vision_supported: bool, #[serde(default)] #[schemars( description = "Route this model through Galaxy's Rig Bedrock runtime. Disabled by default while compatibility validation is in progress." )] pub use_rig: bool, } impl settings_value::SettingsValue for BedrockModelConfig {} fn default_context_size() -> u32 { 200_000 } fn default_enabled() -> bool { true } /// Configuration for a single OpenAI-compatible (LiteLLM) model. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] #[schemars(description = "Configuration for a single OpenAI-compatible model (e.g. via LiteLLM).")] pub struct OpenAIModelConfig { #[schemars( description = "The model ID to send in the API request (e.g. claude-sonnet-4-20250514)." )] pub model_id: String, #[schemars(description = "Display name shown in the model picker.")] pub display_name: String, #[serde(default, alias = "vision_support", alias = "supports_vision")] #[schemars(description = "Whether the model supports image/vision input.")] pub vision_supported: bool, #[serde( default = "default_context_size", alias = "token_size", alias = "max_model_len", alias = "context_window" )] #[schemars(description = "Maximum context window size in tokens.")] pub context_size: u32, #[serde(default, skip_serializing_if = "Option::is_none")] #[schemars(description = "Optional maximum input tokens supported by this model.")] pub max_input_tokens: Option, #[serde( default, alias = "output_token_limit", alias = "max_completion_tokens", alias = "max_tokens", skip_serializing_if = "Option::is_none" )] #[schemars(description = "Optional maximum output tokens to request from this model.")] pub max_output_tokens: Option, #[serde(default)] #[schemars( description = "Optional provider hint (e.g. anthropic, openai, google) for icon display." )] pub provider: Option, #[serde(default)] #[schemars( description = "Route this model through Galaxy's Rig runtime. This is an opt-in migration path." )] pub use_rig: bool, #[serde(default, skip_serializing_if = "Option::is_none")] #[schemars( description = "Whether this endpoint accepts system-role messages. Set false for ChatGPT-backed LiteLLM models that reject them." )] pub supports_system_messages: Option, /// Per-model capability overrides. Missing entries mean Auto: use provider /// metadata when available and allow the request path to determine support. #[serde(default, skip_serializing_if = "HashMap::is_empty")] #[schemars( description = "Optional per-capability overrides: auto, supported, or unsupported." )] pub capability_overrides: HashMap, #[serde(default)] #[schemars( description = "Reasoning effort modes supported by this model when using the ChatGPT subscription provider." )] pub reasoning_efforts: Vec, #[serde(default = "default_enabled")] #[schemars(description = "Whether this model is enabled for the model picker.")] pub enabled: bool, } impl settings_value::SettingsValue for OpenAIModelConfig {} impl OpenAIModelConfig { pub fn capability_override(&self, capability: &str) -> ModelCapabilityOverride { self.capability_overrides .get(capability) .copied() .unwrap_or_default() } pub fn effective_vision_supported(&self) -> bool { match self.capability_override("vision") { ModelCapabilityOverride::Auto => self.vision_supported, ModelCapabilityOverride::Supported => true, ModelCapabilityOverride::Unsupported => false, } } pub fn supports_system_messages(&self) -> bool { if self.capability_override("system_messages") == ModelCapabilityOverride::Unsupported { return false; } if self.model_id.starts_with("codex-gpt-") { return false; } match self.capability_override("system_messages") { ModelCapabilityOverride::Supported => true, ModelCapabilityOverride::Auto => self.supports_system_messages.unwrap_or(true), ModelCapabilityOverride::Unsupported => false, } } } #[derive( Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema, )] #[serde(rename_all = "snake_case")] pub enum ModelCapabilityOverride { #[default] Auto, Supported, Unsupported, } impl ModelCapabilityOverride { pub fn next(self) -> Self { match self { Self::Auto => Self::Supported, Self::Supported => Self::Unsupported, Self::Unsupported => Self::Auto, } } pub fn label(self) -> &'static str { match self { Self::Auto => "Auto", Self::Supported => "On", Self::Unsupported => "Off", } } } /// The protocol and authentication used by an OpenAI model provider. #[derive( Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema, )] #[serde(rename_all = "snake_case")] pub enum OpenAIProviderKind { /// OpenAI's native Chat Completions API. #[serde(rename = "openai", alias = "open_ai")] OpenAI, /// A LiteLLM endpoint using the OpenAI-compatible API plus LiteLLM metadata APIs. #[serde(rename = "litellm", alias = "openai_compatible")] #[default] LiteLLM, /// The ChatGPT subscription backend, authenticated with ChatGPT OAuth. ChatGPTSubscription, /// Anthropic's native Messages API. Anthropic, /// Google's Gemini API. Gemini, /// Google's Gemini models hosted through Vertex AI. #[serde(rename = "vertex_ai", alias = "vertex_a_i")] VertexAI, } /// Configuration for a single direct model provider endpoint. /// /// Multiple providers can be configured simultaneously. Each provider has its own endpoint, /// credentials, and model list. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] #[schemars(description = "Configuration for a direct model provider endpoint.")] pub struct OpenAIProviderConfig { #[serde(default)] #[schemars(description = "Provider protocol and authentication kind.")] pub kind: OpenAIProviderKind, #[serde(default = "default_enabled")] #[schemars(description = "Whether this provider is enabled for AI requests.")] pub enabled: bool, #[schemars(description = "Display name for this provider (shown in model picker).")] pub name: String, #[schemars(description = "Base URL for the OpenAI-compatible API endpoint.")] pub base_url: String, #[serde(default)] #[schemars(description = "API key for this endpoint (optional if the proxy handles auth).")] pub api_key: Option, #[serde(default)] #[schemars(description = "Google Cloud project ID for Vertex AI providers.")] pub project_id: Option, #[serde(default)] #[schemars(description = "Google Cloud location for Vertex AI providers.")] pub location: Option, #[serde(default)] #[schemars(description = "Models available from this provider.")] pub models: Vec, } impl settings_value::SettingsValue for OpenAIProviderConfig {} fn default_acp_agent_id() -> String { "codex".to_string() } fn default_remote_logging_endpoint() -> String { "https://logging.ryserve.net/api/logs".to_string() } fn default_remote_logging_model_payload_max_chars() -> usize { 100_000 } fn acp_agent_display_name(agent_id: &str) -> String { galaxy_acp::known_acp_agents() .iter() .find(|agent| agent.id.eq_ignore_ascii_case(agent_id.trim())) .map(|agent| agent.name.to_string()) .unwrap_or_else(|| agent_id.trim().to_string()) } /// Configuration for a single Agent Client Protocol provider connection. /// /// ACP agents own their own model, login, session, and tool loop. Galaxy stores /// enough connection metadata to launch the configured local agent and route /// discovered model/mode entries back to the right connection. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] #[schemars(description = "Configuration for an Agent Client Protocol provider connection.")] pub struct AcpProviderConfig { #[serde(default)] #[schemars(description = "Stable local identifier for this ACP connection.")] pub id: String, #[serde(default = "default_enabled")] #[schemars(description = "Whether this ACP connection is enabled for agent requests.")] pub enabled: bool, #[serde(default)] #[schemars(description = "Display name for this ACP connection.")] pub name: String, #[serde(default = "default_acp_agent_id")] #[schemars(description = "Identifier for the local Agent Client Protocol agent preset.")] pub agent_id: String, #[serde(default)] #[schemars(description = "Executable used to launch this ACP agent.")] pub command: String, #[serde(default)] #[schemars(description = "Arguments passed to this ACP agent executable.")] pub args: Vec, #[serde(default)] #[schemars( description = "Model, mode, and thought-level options discovered from this ACP agent." )] pub config_options: Vec, } impl AcpProviderConfig { pub(crate) fn new( name: String, agent_id: String, command: String, args: Vec, config_options: Vec, ) -> Self { Self { id: uuid::Uuid::new_v4().to_string(), enabled: true, name, agent_id, command, args, config_options, } } pub(crate) fn display_name(&self) -> String { let name = self.name.trim(); if name.is_empty() || name == "ACP agent runtime" { acp_agent_display_name(self.agent_id.trim()) } else { name.to_string() } } } impl settings_value::SettingsValue for AcpProviderConfig {} /// Cached metadata and runtime session options for an ACP agent. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] pub struct AcpAgentSettings { pub id: String, pub name: String, #[serde(default)] pub version: Option, #[serde(default)] pub description: Option, #[serde(default)] pub icon_url: Option, #[serde(default)] pub capabilities: Vec, #[serde(default)] pub config_options: Vec, /// RFC 3339 timestamp for the last discovery attempt. #[serde(default)] pub discovery_timestamp: Option, /// Identifier for the source of the cached discovery data. #[serde(default)] pub discovery_source: Option, /// Actionable error from the last failed discovery attempt. #[serde(default)] pub discovery_error: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] pub struct AcpConfigOptionSettings { pub id: String, pub name: String, #[serde(default)] pub description: Option, #[serde(default)] pub category: Option, pub kind: String, #[serde(default)] pub current_value: serde_json::Value, #[serde(default)] pub options: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] pub struct AcpConfigValueSettings { pub value: serde_json::Value, pub name: String, #[serde(default)] pub description: Option, } impl settings_value::SettingsValue for AcpAgentSettings {} // Nested ACP discovery data is intentionally persisted as one setting so refreshes are atomic. define_settings_group!(AISettings, settings: [ // If `false`, all AI features are disabled. is_any_ai_enabled: IsAnyAIEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.is_any_ai_enabled", description: "Controls whether all AI features are enabled.", }, // This field should not be referenced directly to lookup active AI enablement -- use the // `is_active_ai_enabled()` getter. is_active_ai_enabled_internal: IsActiveAIEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.enabled", description: "Controls whether proactive AI features like suggestions are enabled.", }, // This field should not be referenced directly to lookup autodetection enablement -- use the // `is_ai_autodetection_enabled()` getter. ai_autodetection_enabled_internal: AIAutoDetectionEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.input.ai_auto_detection_enabled", description: "Controls whether AI automatically detects natural language input.", }, // This field should not be referenced directly -- use the // `is_nld_in_terminal_enabled()` getter. // Controls whether natural language detection is enabled in the terminal input. // // This is only used when `FeatureFlag::AgentView` is enabled. nld_in_terminal_enabled_internal: NLDInTerminalEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.input.nld_in_terminal_enabled", description: "Controls whether natural language detection is enabled in the terminal input.", }, autodetection_command_denylist: AICommandDenylist { type: String, default: String::new(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.input.ai_command_denylist", description: "Commands to exclude from AI natural language autodetection.", } // Whether to use the local AI model for input classification and suggestions // instead of making Bedrock API calls. When enabled, SmolLM2-135M handles // autodetection, prompt suggestions, and code banners locally. use_local_model: UseLocalModel { type: bool, default: true, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.input.use_local_model", description: "Use a locally downloaded AI model for command recognition and suggestions instead of Bedrock.", }, // This field should not be referenced directly to lookup intelligent autosuggestion enablement // -- use the `is_intelligent_autosuggestions_enabled()` getter. intelligent_autosuggestions_enabled_internal: IntelligentAutosuggestionsEnabled { type: bool, default: true, // TODO(roland): revisit this when launched to stable supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.intelligent_autosuggestions_enabled", description: "Controls whether AI-powered intelligent autosuggestions are enabled.", } // This field should not be referenced directly to lookup Prompt Suggestions // enablement -- use the `is_prompt_suggestions_enabled()` getter. // Note that AgentModeQuerySuggestionsEnabled is a legacy name (the feature was initially named Agent // Mode Query Suggestions), however, we do not want to change the name of the setting key to avoid // breaking existing user settings. prompt_suggestions_enabled_internal: AgentModeQuerySuggestionsEnabled { type: bool, default: true, // TODO(advait): revisit this when launched to stable supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.agent_mode_query_suggestions_enabled", description: "Controls whether prompt suggestions are shown in agent mode.", } // This field should not be referenced directly to lookup Code Suggestions // enablement -- use the `is_code_suggestions_enabled()` getter. code_suggestions_enabled_internal: CodeSuggestionsEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.code_suggestions_enabled", description: "Controls whether AI code suggestions are enabled.", } // This field should not be referenced directly to lookup natural language autosuggestions // enablement -- use the `is_natural_language_autosuggestions_enabled()` getter. // This feature refers to ghosted text for AI input queries. natural_language_autosuggestions_enabled_internal: NaturalLanguageAutosuggestionsEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.natural_language_autosuggestions_enabled", description: "Controls whether ghosted text autosuggestions are shown for AI input queries.", feature_flag: FeatureFlag::PredictAMQueries, } // This field should not be referenced directly to lookup shared block title generations // enablement -- use the `is_shared_block_title_generation_enabled()` getter. // This feature refers to the auto title generation when the user opens the shared block dialog. shared_block_title_generation_enabled_internal: SharedBlockTitleGenerationEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.shared_block_title_generation_enabled", description: "Controls whether titles are auto-generated when sharing blocks.", } // This field should not be referenced directly to lookup git operations AI autogen // enablement -- use the `is_git_operations_autogen_enabled()` getter. git_operations_autogen_enabled_internal: GitOperationsAutogenEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.git_operations_autogen_enabled", description: "Controls whether AI auto-generates commit messages and PR title/body in the code review dialogs.", } // This field should not be referenced directly to lookup Rule Suggestions // enablement -- use the `is_rule_suggestions_enabled()` getter. rule_suggestions_enabled_internal: RuleSuggestionsEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.active_ai.rule_suggestions_enabled", description: "Controls whether the agent suggests rules to save after responses.", feature_flag: FeatureFlag::SuggestedRules, } // This field should not be referenced directly to lookup Voice AI enablement -- use the // `is_voice_input_enabled()` getter. voice_input_enabled_internal: VoiceInputEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.voice.voice_input_enabled", description: "Controls whether voice input is enabled for AI interactions.", }, // The number of times the user has entered Agent Mode. // Not a user-visible setting. We model it so we can show the voice input new feature popup // the correct number of times. entered_agent_mode_num_times: EnteredAgentModeNumTimes { type: usize, default: 0, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, }, // Whether or not the user has manually dismissed the voice input new feature popup. dismissed_voice_input_new_feature_popup: DismissedVoiceInputNewFeaturePopup { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: true, }, // This field is used to store the key used for voice input toggling. // Note this is not the named key, but rather corresponds to the physical key. voice_input_toggle_key: VoiceInputToggleKey, // This is not a user-visible setting - it's merely a one-time flag to track if the user has // explicitly interacted with voice input. We use this to determine whether we should show a toast // to inform the user about voice input and auto-set the keybinding. explicitly_interacted_with_voice: ExplicitlyInteractedWithVoice { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, // Never sync to cloud to keep state separate across devices, since microphone access is per-device. private: true, }, // Predicates that Agent Mode can use to decide if it can execute // a command without explicit user consent. // // Prefer [`BlocklistAIPermissions::can_autoexecute_command`] to // interpret this allowlist. agent_mode_command_execution_allowlist: AgentModeCommandExecutionAllowlist { type: Vec, default: DEFAULT_COMMAND_EXECUTION_ALLOWLIST.clone(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.profiles.agent_mode_command_execution_allowlist", description: "Commands that the agent can execute without explicit permission.", }, // Predicates that Agent Mode can use to decide if a command must // be executed by the user. // // Prefer [`BlocklistAIPermissions::can_autoexecute_command`] to // interpret this denylist. agent_mode_command_execution_denylist: AgentModeCommandExecutionDenylist { type: Vec, default: DEFAULT_COMMAND_EXECUTION_DENYLIST.clone(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.profiles.agent_mode_command_execution_denylist", description: "Commands that the agent must always ask before executing.", }, // Enabled iff Agent Mode can execute readonly commands without explicit user consent. // // Prefer [`BlocklistAIPermissions::can_autoexecute_command`] to // interpret this setting. agent_mode_execute_read_only_commands: AgentModeExecuteReadonlyCommands { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.profiles.agent_mode_execute_readonly_commands", description: "Whether the agent can auto-execute read-only commands without asking.", }, // Determines coding permissions that Agent Mode has. // Note that if Agent Mode has permissions to execute readonly commands, // that automatically gives Agent Mode the ability to also _read_ files for coding // tasks, including codebase search. // // Prefer [`BlocklistAIPermissions::can_read_file`] to interpret this setting. agent_mode_coding_permissions: AgentModeCodingPermissions { type: AgentModeCodingPermissionsType, default: AgentModeCodingPermissionsType::default(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.profiles.agent_mode_coding_permissions", description: "The file read permission level for the agent.", } // Specific filepaths that Agent Mode can read without asking for additional permissions. // These should be persisted as absolute filepaths to avoid ambiguity. // // This is used in conjunction with [`AgentModeCodingPermissionsType::AllowReadingSpecificFiles`] // but modelled as a separate setting because it is not cloud-synced. // // Prefer [`BlocklistAIPermissions::can_read_file`] to interpret this setting. agent_mode_coding_file_read_allowlist: AgentModeCodingFileReadAllowlist { type: Vec, default: vec![], supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.profiles.agent_mode_coding_file_read_allowlist", description: "File paths the agent can read without asking for permission.", } // Whether or not the profile-level command autoexecution speedbump has been shown. // // Not a user-visible setting - we model it as a setting so we can track how often // it's shown across devices. has_shown_agent_mode_profile_command_autoexecution_speedbump: HasShownAgentModeProfileCommandAutoexecutionSpeedbump { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether or not we should show the speedbump for auto-executing readonly cmds. // // Not a user-visible settings - we model it as a setting so we can track how often // it's shown across devices. should_show_agent_mode_autoexecute_readonly_commands_speedbump: ShouldShowAgentModeModelExecuteReadonlyCommandsSpeedbump { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether or not we should show the speedbump for auto-writing to the PTY. // // Not a user-visible settings - we model it as a setting so we can track how often // it's shown across devices. should_show_agent_mode_write_to_pty_speedbump: ShouldShowAgentModeWriteToPtySpeedbump { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether or not we should show the speedbump for auto-reading files. // // Not a user-visible settings - we model it as a setting so we can track how often // it's shown across devices. should_show_agent_mode_autoread_files_speedbump: ShouldShowAgentModeCodingReadPermissionsNudge { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether or not we should show the one-shot speedbump on Ask-User-Question cards. // // Not a user-visible setting - we model it as a setting so we can track state. // Intentionally NOT cloud-synced: we want users to see the first-time nudge on // each fresh device, and we avoid a cloud-sync race that would make the flag // silently stay `false` on new devices after being consumed once elsewhere. should_show_agent_mode_ask_user_question_speedbump: ShouldShowAgentModeAskUserQuestionSpeedbump { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether new local agent conversations should use an Agent Client Protocol backend. acp_enabled: AcpEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.enabled", description: "Whether new local agent conversations use an Agent Client Protocol backend.", feature_flag: FeatureFlag::AgentClientProtocol, } // Stable identifier for the selected ACP agent preset. acp_agent_id: AcpAgentId { type: String, default: "codex".to_string(), supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.agent_id", description: "Identifier for the local Agent Client Protocol agent preset.", feature_flag: FeatureFlag::AgentClientProtocol, } // Friendly name shown for the configured ACP provider card. acp_connection_name: AcpConnectionName { type: String, default: "ACP agent runtime".to_string(), supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.connection_name", description: "Friendly name for the configured ACP agent runtime.", feature_flag: FeatureFlag::AgentClientProtocol, } // Executable used to launch the configured local ACP agent. acp_agent_command: AcpAgentCommand { type: String, default: String::new(), supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.agent_command", description: "Executable used to launch the local Agent Client Protocol agent.", feature_flag: FeatureFlag::AgentClientProtocol, } // Arguments passed directly to the configured ACP agent executable. acp_agent_args: AcpAgentArgs { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.agent_args", description: "Arguments passed to the local Agent Client Protocol agent executable.", feature_flag: FeatureFlag::AgentClientProtocol, } // Configured local ACP provider connections. acp_providers: AcpProviders { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.providers", description: "Configured Agent Client Protocol provider connections.", feature_flag: FeatureFlag::AgentClientProtocol, } // Cached ACP registry and runtime discovery data. Values are refreshed when the agent is queried. acp_agents: AcpAgents { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()), sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.acp.agents", description: "Cached ACP agent metadata, capabilities, and session configuration options.", feature_flag: FeatureFlag::AgentClientProtocol, } // Whether to use locally loaded AWS credentials for Bedrock-enabled requests. bedrock_enabled: BedrockEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.enabled", description: "Whether to use AWS Bedrock directly for AI requests.", } // Authentication method for Bedrock: "profile", "static_keys", or "sso". bedrock_auth_method: BedrockAuthMethod, // Friendly name shown for the configured Bedrock provider card. bedrock_connection_name: BedrockConnectionName { type: String, default: "AWS Bedrock".to_string(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.connection_name", description: "Friendly name for the configured AWS Bedrock connection.", } // AWS profile name to use when auth_method is Profile or SSO. bedrock_profile: BedrockProfile { type: String, default: "default".to_string(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.profile", description: "The AWS profile name to use for Bedrock credentials.", } // AWS region for Bedrock API calls. Empty string means auto-detect from profile/config. bedrock_region: BedrockRegion { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.region", description: "AWS region for Bedrock API calls. Leave empty to auto-detect from profile.", } // Whether to automatically add cross-region inference prefixes to model IDs. bedrock_cross_region_inference: BedrockCrossRegionInference { type: bool, default: true, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.cross_region_inference", description: "Whether to automatically add cross-region inference prefixes to model IDs.", } // Cached Bedrock models that passed foundation-model availability checks. bedrock_models: BedrockModels { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.models", description: "AWS Bedrock models discovered as authorized and available in the configured region.", } // Whether to automatically run the login command when Bedrock credentials expire. bedrock_auto_login: BedrockAutoLogin { type: bool, default: true, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.auto_login", description: "Whether to automatically run the login command when Bedrock credentials expire.", } // Command to run to refresh AWS credentials (e.g. "aws sso login"). bedrock_auth_refresh_command: BedrockAuthRefreshCommand { type: String, default: "aws sso login".to_string(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.bedrock.auth_refresh_command", description: "The command to run to refresh AWS credentials for Bedrock.", } // AWS access key ID for static key authentication (stored in OS keychain). bedrock_access_key_id: BedrockAccessKeyId { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: true, } // AWS secret access key for static key authentication (stored in OS keychain). bedrock_secret_access_key: BedrockSecretAccessKey { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether the Bedrock login banner has been permanently dismissed. bedrock_login_banner_dismissed: BedrockLoginBannerDismissed { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether to mint and attach Gemini Enterprise (GEAP) credentials to eligible agent // requests, routing them through the workspace's Google Cloud project. Only consulted // when the admin sets the GEAP host to RESPECT_USER_SETTING; ENFORCE bypasses it. // Prefer [`UserWorkspaces::is_gemini_enterprise_credentials_enabled`] to interpret // this setting. gemini_enterprise_credentials_enabled: GeminiEnterpriseCredentialsEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "cloud_platform.third_party_api_keys.gemini_enterprise_credentials_enabled", description: "Whether Warp should route eligible requests through your workspace's Gemini Enterprise Google Cloud project.", } // Whether a configured OpenAI-compatible provider is enabled. openai_enabled: OpenAIEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "ai.openai.enabled", description: "Whether to use a configured OpenAI-compatible endpoint for AI requests.", } // Base URL for the OpenAI-compatible API endpoint. openai_base_url: OpenAIBaseUrl { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "ai.openai.base_url", description: "Base URL for the OpenAI-compatible API endpoint (e.g. LiteLLM proxy).", } // API key for the OpenAI-compatible endpoint (optional if proxy handles auth). openai_api_key: OpenAIApiKey { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.openai.api_key", description: "API key for the OpenAI-compatible endpoint (optional if proxy handles auth).", } // Model name to send to the OpenAI-compatible endpoint. Empty = use selected model ID. openai_model: OpenAIModel { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "ai.openai.model", description: "Model name to send to the OpenAI-compatible endpoint. Leave empty to use the selected model ID.", } // Custom OpenAI-compatible model configurations (fetched or manually configured). openai_models: OpenAIModels { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "ai.openai.models", description: "Custom OpenAI-compatible model configurations.", } // Multiple OpenAI-compatible provider endpoints. // Each provider has its own name, base_url, api_key, and model list. openai_providers: OpenAIProviders { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::DESKTOP, // Provider entries may contain API keys, so the complete setting must // remain local even when preference sync is enabled. sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.providers", description: "Multiple OpenAI-compatible provider endpoints.", } // Whether to send opt-in AI diagnostics to a remote logging endpoint. remote_logging_enabled: RemoteLoggingEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.remote_logging.enabled", description: "Whether to send opt-in AI diagnostics to the configured remote logger.", } // Endpoint for opt-in AI diagnostics. May be either the logger base URL or the full /api/logs URL. remote_logging_endpoint: RemoteLoggingEndpoint { type: String, default: default_remote_logging_endpoint(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.remote_logging.endpoint", description: "Remote logging endpoint for opt-in AI diagnostics.", } // API key used to write opt-in AI diagnostics to the remote logger. Kept local only. remote_logging_api_key: RemoteLoggingApiKey { type: String, default: String::new(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.remote_logging.api_key", description: "API key used to write opt-in AI diagnostics to the remote logger.", } // Whether to include raw model request and response payloads in opt-in AI diagnostics. // This can include prompts, model output, tool arguments, and file contents. remote_logging_log_model_payloads: RemoteLoggingLogModelPayloads { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.remote_logging.log_model_payloads", description: "Whether opt-in AI diagnostics include raw model request and response payloads.", } // Maximum number of trailing characters kept for each raw model payload log. remote_logging_model_payload_max_chars: RemoteLoggingModelPayloadMaxChars { type: usize, default: default_remote_logging_model_payload_max_chars(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "ai.remote_logging.model_payload_max_chars", description: "Maximum number of trailing characters to send for each raw model payload diagnostic event.", } // Whether or not the user wants agent mode requests to use their saved rules. memory_enabled: MemoryEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.knowledge.rules_enabled", description: "Whether the agent uses your saved rules during requests.", } // Whether warp drive context should be included in AI requests warp_drive_context_enabled: WarpDriveContextEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.knowledge.warp_drive_context_enabled", description: "Whether Galaxy Drive context is included in AI requests.", } // Whether the codebase speedbump banner has been permanently dismissed for a given repo path. // // Not a user-visible settings - we model it as a setting so we can track state. codebase_index_speedbump_banner_dismissed_for_repo_paths: CodebaseIndexSpeedbumpBannerDismissedForRepoPaths { type: Vec, default: vec![], supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether the agent mode setup banner has been shown for a given repo path. // Once shown, it will not be shown again for that repo. // // Not a user-visible settings - we model it as a setting so we can track state. agent_mode_setup_banner_shown_for_repo_paths: AgentModeSetupBannerShownForRepoPaths { type: Vec, default: vec![], supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether the codebase speedbump banner has been globally dismissed ("Don't show again"). // // Not a user-visible settings - we model it as a setting so we can track state. codebase_index_speedbump_banner_globally_dismissed: CodebaseIndexSpeedbumpBannerGloballyDismissed { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Information about AI request quotas and usage across billing cycles ai_request_quota_info: AIRequestQuotaInfoSetting { type: AIRequestQuotaInfo, default: AIRequestQuotaInfo::default(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, }, // Whether or not we should show the speedbump for showing code suggestion banners. // This includes both passive code diffs and suggested prompts (passive unit tests). // // Not a user-visible settings - we model it as a setting so we can track if the speedbump has already been shown or not. show_code_suggestion_speedbump: ShouldShowCodeSuggestionSpeedbump { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } mcp_execution_path: MCPExecutionPath { type: Option, default: None, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, }, // This is not a user-visible setting - its merely a one-time flag to track if the agents 3 launch modal // has been shown to the user. // // We model it as a setting so it's only shown once to a given user regardless of the number of // devices they use. did_check_to_trigger_agents_3_launch_modal: DidShowAgents3LaunchModal { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // This is not a user-visible setting - it's merely a one-time flag to track if the Oz launch modal // has been shown to the user. // // We model it as a setting so it's only shown once to a given user regardless of the number of // devices they use. did_check_to_trigger_oz_launch_modal: DidShowOzLaunchModal { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: true, } // This is not a user-visible setting - it's merely a one-time flag to track if the // orchestration launch modal has been shown to the user. // // We model it as a setting so it's only shown once to a given user regardless of the number of // devices they use. did_check_to_trigger_orchestration_launch_modal: DidShowOrchestrationLaunchModal { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: true, } // This is not a user-visible setting - it's merely a one-time flag to track if the // free-AI-removal notice modal has been shown to (or silently marked as seen for) the user. // // We model it as a setting so it's only shown once to a given user regardless of the number of // devices they use. did_check_to_trigger_free_ai_removal_modal: DidShowFreeAiRemovalModal { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: true, } // Used to determine whether the "What's new in Oz" section of the agent view // zero state is expanded or collapsed by default. should_expand_oz_updates: ShouldExpandOzUpdates { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Used to determine whether the "What's new in Oz" section of the agent view // zero state is shown or hidden. should_show_oz_updates_in_zero_state: ShouldShowOzUpdatesInZeroState { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_show_oz_updates_in_zero_state", description: "Whether the \"What's new\" section is shown in the agent view.", } should_render_use_agent_footer_for_user_commands: ShouldRenderUseAgentToolbarForUserCommands { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.other.should_render_use_agent_toolbar_for_user_commands", description: "Whether to show the \"Use Agent\" footer for terminal commands.", } // Whether to render the CLI agent footer for commands like Claude, Codex, Gemini, etc. // This is independent of the "Use Agent" footer setting. should_render_cli_agent_footer: ShouldRenderCLIAgentToolbar { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.third_party.should_render_cli_agent_toolbar", description: "Whether to show the CLI agent footer for coding agent commands.", } // When enabled and a CLI agent session has a plugin listener, rich input // auto-closes when the session enters a Blocked state (the agent requires // direct keyboard interaction) and auto-opens when it leaves Blocked. auto_toggle_rich_input: AutoToggleRichInput { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.third_party.auto_toggle_composer", description: "Whether CLI agent Rich Input automatically closes and reopens based on the agent's blocked state.", } // When enabled and a CLI agent session has a plugin listener, rich input // auto-opens once when the session starts or when the listener is registered. auto_open_rich_input_on_cli_agent_start: AutoOpenRichInputOnCLIAgentStart { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.third_party.auto_open_composer_on_cli_agent_start", description: "Whether CLI agent Rich Input automatically opens when a CLI agent session starts.", } // When enabled and a CLI agent session does NOT have a plugin listener, // rich input auto-closes after the user submits a prompt. // When the plugin IS present, this setting has no effect (auto-show/hide // from auto_toggle_rich_input handles rich input lifecycle). auto_dismiss_rich_input_after_submit: AutoDismissRichInputAfterSubmit { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.third_party.auto_dismiss_composer_after_submit", description: "Whether CLI agent Rich Input automatically closes after the user submits a prompt.", } // When enabled, the Rich Input editor submits on Ctrl+Enter instead of Enter. // Enter inserts a newline; Ctrl+Enter submits. submit_on_ctrl_enter: SubmitRichInputOnCtrlEnter { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.third_party.submit_on_ctrl_enter", description: "When enabled, the Rich Input editor submits on Ctrl+Enter instead of Enter. Enter inserts a newline.", } // Maps custom toolbar command regex patterns to specific CLI agents. // Keys are regex patterns matched against the full command string. // Values are serialized CLIAgent names (empty string = any agent). // Supports migration from the legacy Vec format. cli_agent_footer_enabled_commands: CLIAgentToolbarEnabledCommands { type: ToolbarCommandMap, default: ToolbarCommandMap::default(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.third_party.cli_agent_toolbar_enabled_commands", max_table_depth: 1, description: "Maps custom toolbar command patterns to specific CLI agents.", } // This is not a user-visible setting - it tracks whether a paid user has dismissed the // agent management help page by clicking "View Agents". // // When false and user is on a paid plan, the help page is shown. // When true, the help page is hidden (user dismissed it). // Free users never see the help page by default regardless of this setting. did_dismiss_cloud_setup_guide: DidDismissAgentManagementHelpPage { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether the ambient agent trial widget has been dismissed by the user. // // Not a user-visible setting - we model it as a setting so we can track state. ambient_agent_trial_widget_dismissed: AmbientAgentTrialWidgetDismissed { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } // The raw stored default mode for new sessions. Use `default_session_mode()` to retrieve the // effective value, which is gated on AI availability. default_session_mode_internal: DefaultSessionMode, // The file path of the tab config used when default_session_mode_internal is TabConfig. // Only read when mode is TabConfig; ignored for all other modes. // Machine-local (tab config paths vary per machine), so never synced to cloud. default_tab_config_path: DefaultTabConfigPath { type: String, default: String::new(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "general.default_tab_config_path", } // Whether computer use is enabled for cloud agent conversations started from the Warp app. // This setting is only used when the AI autonomy setting is AlwaysAsk or not set. cloud_agent_computer_use_enabled: CloudAgentComputerUseEnabled { type: bool, default: galaxy_core::channel::ChannelState::channel().is_dogfood(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.cloud_agent_computer_use_enabled", description: "Whether computer use is enabled for cloud agent conversations.", } // Whether file-based MCP servers from third-party AI tools (e.g. Claude, Codex) should // be automatically detected and spawned. Warp-native config files (.warp-core/.mcp.json) are // always detected and spawned, regardless of this setting. file_based_mcp_enabled: FileBasedMcpEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.mcp_servers.file_based_mcp_enabled", description: "Whether third-party file-based MCP servers are automatically detected.", } // Controls how agent thinking/reasoning traces are displayed. thinking_display_mode: ThinkingDisplayMode, // Controls how orchestration message bodies are expanded by default. orchestration_message_display_mode: OrchestrationMessageDisplayMode, // Default behavior when the user submits a new prompt while the agent is still // responding. Per-conversation overrides live on `QueuedQueryModel`; this // setting is the fallback used when a conversation has no explicit override. default_prompt_submission_mode: PromptSubmissionMode, // What happens when a prompt is submitted while an agent controls an agent-requested // long-running command. Only consulted when `default_prompt_submission_mode` is `Interrupt`; // per-LRC manual overrides live on `QueuedQueryModel`. long_running_command_submission_mode: LongRunningCommandSubmissionMode, // Whether agent-executed shell commands should be included in command history // (up-arrow, Ctrl-R search, inline history menu). // When false, commands run by the AI agent are excluded from history. include_agent_commands_in_history: IncludeAgentCommandsInHistory { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.input.include_agent_commands_in_history", description: "Whether agent-executed commands are included in command history.", } // Controls whether the conversation history view appears in the tools panel. show_conversation_history: ShowConversationHistory { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.other.show_conversation_history", description: "Whether conversation history appears in the tools panel.", } // Controls whether agent notifications (mailbox button, toasts, notification items) are shown. show_agent_notifications: ShowAgentNotifications { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.other.show_agent_notifications", description: "Whether agent notifications are shown.", } // Per-agent, per-host tracking of whether the user dismissed the plugin install chip. // Keys are "" for local sessions or "@" for remote. // Local-only so dismissal doesn't sync across devices. plugin_install_chip_dismissed_map: PluginInstallChipDismissedMap { type: HashMap, default: HashMap::default(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: true, } // Per-agent, per-host tracking of the MINIMUM_PLUGIN_VERSION for which the user // dismissed the plugin update chip. Empty/absent means not dismissed. // Keys are "" for local sessions or "@" for remote. // Local-only so dismissal doesn't sync across devices. plugin_update_chip_dismissed_for_version_map: PluginUpdateChipDismissedForVersionMap { type: HashMap, default: HashMap::default(), supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Never, private: true, } // Whether Oz should add attribution (co-author line) to commit messages and PRs. // This is the user-level preference; it may be overridden by the team-level // `enable_warp_attribution` AdminEnablementSetting (see // `UserWorkspaces::get_agent_attribution_setting`). agent_attribution_enabled: AgentAttributionEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: false, toml_path: "agents.warp_agent.other.agent_attribution_enabled", description: "Whether the Galaxy Agent adds an attribution co-author line to commit messages and pull requests it creates.", } // Tracks whether predefined system rules have been seeded on first launch. // Once set to true, predefined rules will not be auto-created again even if the user // deletes all their rules. has_seeded_predefined_rules: HasSeededPredefinedRules { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, private: true, } should_force_disable_cloud_handoff: ShouldForceDisableCloudHandoff { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_force_disable_cloud_handoff", description: "Whether to force-disable local-to-cloud handoff.", } should_force_disable_ampersand_handoff: ShouldForceDisableAmpersandHandoff { type: bool, default: false, supported_platforms: SupportedPlatforms::DESKTOP, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.should_force_disable_ampersand_handoff", description: "Whether to force-disable the & prefix for cloud handoff compose mode.", } auto_handoff_on_sleep_enabled: AutoHandoffOnSleepEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::MAC, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.warp_agent.other.auto_handoff_on_sleep_enabled", description: "Whether Warp automatically hands off local agent conversations to cloud when the computer is about to sleep.", } // This is not a user-visible setting - it's merely a one-time flag to track if the // auto-handoff sleep modal has been shown to the user. // // We model it as a setting so it's only shown once to a given user regardless of the number of // devices they use. did_show_auto_handoff_sleep_modal: DidShowAutoHandoffSleepModal { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: true, } // Whether the "Crosscheck Work" experiment is enabled. // When enabled, a reviewer sub-agent is spawned after the main agent finishes // a turn (with no pending tool calls) to critique the output. The feedback // loop continues until the reviewer responds with "LGTM!". crosscheck_enabled: CrosscheckEnabled { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.experiments.crosscheck_enabled", description: "Enables the Crosscheck Work experiment: a reviewer agent critiques agent output until satisfied.", feature_flag: FeatureFlag::CrosscheckWork, } // The model ID to use for the crosscheck reviewer agent. // Must be a valid model ID from the configured LLM providers. // If empty, the currently active model for the conversation is used. crosscheck_model_id: CrosscheckModelId { type: String, default: String::new(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.experiments.crosscheck_model_id", description: "The LLM model ID used by the crosscheck reviewer agent.", feature_flag: FeatureFlag::CrosscheckWork, } // Maximum number of crosscheck review cycles before stopping. // Prevents infinite feedback loops between the main agent and reviewer. crosscheck_max_iterations: CrosscheckMaxIterations { type: u32, default: 3, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), private: false, toml_path: "agents.experiments.crosscheck_max_iterations", description: "Maximum number of crosscheck review cycles before auto-accepting.", feature_flag: FeatureFlag::CrosscheckWork, } ]); impl AISettings { pub fn register_and_subscribe_to_events(app: &mut AppContext) { Self::register(app); app.add_singleton_model(FocusedTerminalInfo::new); CompiledCommandsForCodingAgentToolbar::register(app); app.update_model(&Self::handle(app), |_me, ctx| { ctx.subscribe_to_model(&FocusedTerminalInfo::handle(ctx), |_me, _, event, ctx| { if matches!(event, FocusedTerminalInfoEvent::TerminalInfoUpdated) { // Pipe the event so that any view that listens for settings changes will be notified. ctx.emit(AISettingsChangedEvent::IsAnyAIEnabled { change_event_reason: ChangeEventReason::LocalChange, }); } }); }); } pub fn is_ai_disabled_due_to_remote_session_org_policy(&self, app: &AppContext) -> bool { let contains_remote_blocks = FocusedTerminalInfo::as_ref(app).contains_any_remote_blocks(); let contains_restored_remote_blocks = FocusedTerminalInfo::as_ref(app).contains_any_restored_remote_blocks(); let is_ai_allowed_in_remote_sessions = UserWorkspaces::as_ref(app).is_ai_allowed_in_remote_sessions(); if is_ai_allowed_in_remote_sessions { return false; } contains_remote_blocks || contains_restored_remote_blocks } pub fn is_any_ai_enabled(&self, app: &AppContext) -> bool { // Galaxy does not require Warp authentication for AI. // AI is enabled only when the user hasn't explicitly disabled it, at least one local // runtime is enabled, and there's no org policy blocking it. *self.is_any_ai_enabled && self.has_enabled_ai_runtime() && !self.is_ai_disabled_due_to_remote_session_org_policy(app) } pub(crate) fn configured_acp_providers(&self) -> Vec { let providers = self .acp_providers .value() .iter() .filter(|provider| !provider.agent_id.trim().is_empty()) .cloned() .collect::>(); if !providers.is_empty() { return providers; } self.legacy_acp_provider().into_iter().collect() } pub(crate) fn enabled_acp_providers(&self) -> Vec { if !*self.acp_enabled.value() { return Vec::new(); } self.configured_acp_providers() .into_iter() .filter(|provider| provider.enabled) .collect() } pub(crate) fn enabled_acp_provider_by_id( &self, provider_id: &str, ) -> Option { self.enabled_acp_providers() .into_iter() .find(|provider| provider.id == provider_id) } pub(crate) fn legacy_acp_provider(&self) -> Option { if !*self.acp_enabled.value() { return None; } let agent_id = self.acp_agent_id.value().trim(); let agent_id = if agent_id.is_empty() { "codex" } else { agent_id }; let config_options = self .acp_agents .value() .iter() .find(|agent| agent.id.eq_ignore_ascii_case(agent_id)) .map(|agent| agent.config_options.clone()) .unwrap_or_default(); Some(AcpProviderConfig { id: "legacy".to_string(), enabled: true, name: self.acp_connection_name.value().clone(), agent_id: agent_id.to_string(), command: self.acp_agent_command.value().clone(), args: self.acp_agent_args.value().clone(), config_options, }) } /// Returns whether Galaxy has a local model provider or agent runtime enabled. pub fn has_enabled_ai_runtime(&self) -> bool { *self.bedrock_enabled.value() || *self.openai_enabled.value() || (cfg!(unix) && FeatureFlag::AgentClientProtocol.is_enabled() && *self.acp_enabled.value()) } pub fn default_session_mode(&self, app: &AppContext) -> DefaultSessionMode { let mode = *self.default_session_mode_internal.value(); match mode { // Terminal and TabConfig don't require AI. DefaultSessionMode::Terminal | DefaultSessionMode::TabConfig => mode, // Agent and CloudAgent require AI to be enabled. DefaultSessionMode::Agent | DefaultSessionMode::CloudAgent => { if self.is_any_ai_enabled(app) { mode } else { DefaultSessionMode::Terminal } } // DockerSandbox is gated on its feature flag; fall back to Terminal // when disabled so a stale stored value doesn't wedge the user. DefaultSessionMode::DockerSandbox => { if FeatureFlag::LocalDockerSandbox.is_enabled() { mode } else { DefaultSessionMode::Terminal } } } } /// Returns the stored default tab config path (only meaningful when mode is `TabConfig`). pub fn default_tab_config_path(&self) -> &str { &self.default_tab_config_path } /// Looks up the `TabConfig` matching the stored `default_tab_config_path`. /// Returns `None` if the path is empty or no loaded config matches. pub fn resolved_default_tab_config( &self, app: &AppContext, ) -> Option { let path_str = self.default_tab_config_path.as_str(); if path_str.is_empty() { return None; } let path = std::path::Path::new(path_str); crate::user_config::GalaxyConfig::as_ref(app) .tab_configs() .iter() .find(|config| config.source_path.as_deref().is_some_and(|p| p == path)) .cloned() } pub fn is_active_ai_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) && *self.is_active_ai_enabled_internal && AppExecutionMode::as_ref(app).allows_active_ai() } pub fn is_prompt_suggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.prompt_suggestions_enabled_internal } pub fn is_rule_suggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.rule_suggestions_enabled_internal } pub fn is_code_suggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.code_suggestions_enabled_internal } pub fn should_use_local_model(&self) -> bool { *self.use_local_model } pub fn is_natural_language_autosuggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.natural_language_autosuggestions_enabled_internal } pub fn is_shared_block_title_generation_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.shared_block_title_generation_enabled_internal } pub fn is_git_operations_autogen_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.git_operations_autogen_enabled_internal } pub fn is_intelligent_autosuggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_active_ai_enabled(app) && *self.intelligent_autosuggestions_enabled_internal } pub fn is_voice_input_enabled(&self, app: &galaxyui::AppContext) -> bool { // Voice input is conditionally-compiled because it requires additional dependencies on some platforms. cfg!(feature = "voice_input") && self.is_any_ai_enabled(app) && *self.voice_input_enabled_internal } /// Returns `true` if input autodetection is enabled. /// /// If `FeatureFlag::AgentView` is enabled, this specifically gates NLD enablement in the agent /// view only. pub fn is_ai_autodetection_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) && *self.ai_autodetection_enabled_internal } /// Returns `true` if NLD is enabled in the terminal. /// /// This is only used when `FeatureFlag::AgentView` is enabled. /// If the user has not explicitly set this setting, it defaults to the value of /// `ai_autodetection_enabled_internal`. pub fn is_nld_in_terminal_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) && *self.nld_in_terminal_enabled_internal } pub fn is_memory_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) && *self.memory_enabled } pub fn is_warp_drive_context_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) && *self.warp_drive_context_enabled } pub fn is_file_based_mcp_enabled(&self, app: &galaxyui::AppContext) -> bool { if !FeatureFlag::FileBasedMcp.is_enabled() || !self.is_any_ai_enabled(app) { return false; } // NOTE: we intentionally do not force-enable this in Cloud Mode. Previously // we auto-spawned file-based MCPs in autonomous execution, but that bypassed // the user's explicit opt-in and let any MCP config checked into a repo run // arbitrary commands as part of a cloud agent run. Respecting the toggle // closes that attack surface; cloud agents that need project-scoped MCP // servers should surface an explicit, auditable opt-in. A more robust // solution (e.g. per-environment allowlisting, signed configs) should be // explored in the future. *self.file_based_mcp_enabled } pub fn is_orchestration_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) } /// Returns true when local-to-cloud handoff is effectively enabled. /// False when the user/org has disabled it, cloud conversations are off, /// or AI is globally off. /// Returns `true` when the Crosscheck Work experiment is active. pub fn is_crosscheck_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_any_ai_enabled(app) && FeatureFlag::CrosscheckWork.is_enabled() && *self.crosscheck_enabled } /// Returns the model ID configured for the crosscheck reviewer, or empty /// string if the active model should be used. pub fn crosscheck_model_id(&self) -> &str { &self.crosscheck_model_id } /// Maximum number of crosscheck review iterations. pub fn crosscheck_max_iterations(&self) -> u32 { (*self.crosscheck_max_iterations).max(1) } pub fn is_cloud_handoff_enabled(&self, app: &galaxyui::AppContext) -> bool { if !self.is_any_ai_enabled(app) || *self.should_force_disable_cloud_handoff { return false; } if !FeatureFlag::OzHandoff.is_enabled() || !FeatureFlag::HandoffLocalCloud.is_enabled() || !cfg!(all(feature = "local_fs", not(target_family = "wasm"))) { return false; } let privacy = PrivacySettings::as_ref(app); if !privacy.is_cloud_conversation_storage_enabled { return false; } !matches!( UserWorkspaces::as_ref(app).get_cloud_conversation_storage_enablement_setting(), crate::workspaces::workspace::AdminEnablementSetting::Disable ) } pub fn is_ampersand_handoff_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_cloud_handoff_enabled(app) && !*self.should_force_disable_ampersand_handoff } pub fn is_auto_handoff_on_sleep_enabled(&self, app: &galaxyui::AppContext) -> bool { self.is_cloud_handoff_enabled(app) && self .auto_handoff_on_sleep_enabled .is_supported_on_current_platform() && *self.auto_handoff_on_sleep_enabled } /// Determines whether a quota reset banner should be displayed to the user. /// /// The banner should be shown if the most recent completed billing cycle had /// quota exceeded and the banner was not manually dismissed. pub fn should_display_quota_reset_banner(&self) -> bool { let quota_info = &self.ai_request_quota_info; let most_recent_completed_cycle = quota_info .cycle_history .iter() .rev() .find(|cycle| cycle.end_date < Utc::now()); if let Some(cycle) = most_recent_completed_cycle { if cycle.was_quota_exceeded && !cycle.banner_state.dismissed { return true; } } false } /// Marks the banner as dismissed for all completed cycles. pub fn mark_quota_banner_as_dismissed(&mut self, ctx: &mut ModelContext) { let mut cycle_history = self.ai_request_quota_info.cycle_history.clone(); for cycle in cycle_history.iter_mut() { if cycle.end_date < Utc::now() { cycle.banner_state.dismissed = true; } } report_if_error!(self .ai_request_quota_info .set_value(AIRequestQuotaInfo { cycle_history }, ctx)); } /// Updates the quota info based on the latest RequestLimitInfo. /// /// This method finds or creates the appropriate CycleInfo based on the /// request_limit_info's next refresh time and updates its fields accordingly. pub fn update_quota_info( &mut self, request_limit_info: &RequestLimitInfo, ctx: &mut ModelContext, ) { // Convert ServerTimestamp to DateTime let next_refresh_time = request_limit_info.next_refresh_time.utc(); let now = Utc::now(); // Check if request_limit_info has unlimited requests let is_quota_exceeded = !request_limit_info.is_unlimited && request_limit_info.num_requests_used_since_refresh >= request_limit_info.limit; let mut cycle_history = self.ai_request_quota_info.cycle_history.clone(); // Track if we updated an existing cycle let mut updated_existing_cycle = false; // Find or create a cycle that matches the current period if let Some(current_cycle) = cycle_history.last_mut() { if now <= current_cycle.end_date { // Update existing cycle current_cycle.was_quota_exceeded = is_quota_exceeded; updated_existing_cycle = true; } } // Only create a new cycle if we didn't update an existing one if !updated_existing_cycle { // Create a new cycle let new_cycle = CycleInfo { end_date: next_refresh_time, was_quota_exceeded: is_quota_exceeded, banner_state: BannerState::default(), }; cycle_history.push(new_cycle); } report_if_error!(self .ai_request_quota_info .set_value(AIRequestQuotaInfo { cycle_history }, ctx)); } pub fn is_command_denylist_editable(&self, app: &AppContext) -> bool { self.is_any_ai_enabled(app) } pub fn is_command_allowlist_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_execute_commands_allowlist(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_directory_allowlist_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_read_files_allowlist(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_execute_commands_permissions_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_execute_commands(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_write_to_pty_permissions_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_write_to_pty(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_computer_use_permissions_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_computer_use(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_read_files_permissions_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_read_files(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_code_diffs_permissions_editable(&self, app: &AppContext) -> bool { let set_by_workspace = UserWorkspaces::as_ref(app) .ai_autonomy_settings() .has_override_for_code_diffs(); self.is_any_ai_enabled(app) && !set_by_workspace } pub fn is_ask_user_question_permissions_editable(&self, app: &AppContext) -> bool { self.is_any_ai_enabled(app) } pub fn is_mcp_permission_editable(&self, app: &AppContext) -> bool { // TODO: Allow workspace overrides on MCP permissions. self.is_any_ai_enabled(app) } pub fn is_run_agents_permissions_editable(&self, app: &AppContext) -> bool { self.is_orchestration_enabled(app) } pub fn show_code_suggestion_speedbump(&self, app: &AppContext) -> bool { self.is_any_ai_enabled(app) && *self.show_code_suggestion_speedbump } /// Handles first-time voice input setup when user clicks the voice button. /// /// If the user hasn't explicitly interacted with voice yet: /// - Sets the default voice input toggle key based on the OS /// - Marks `explicitly_interacted_with_voice` as true /// - Returns `Some(toggle_key)` so the caller can show a toast /// /// If the user has already interacted with voice, returns `None`. pub fn maybe_setup_first_time_voice( &mut self, ctx: &mut ModelContext, ) -> Option { if *self.explicitly_interacted_with_voice.value() { return None; } let voice_input_toggle_key = match OperatingSystem::get() { OperatingSystem::Mac => VoiceInputToggleKey::Fn, OperatingSystem::Windows | OperatingSystem::Linux | OperatingSystem::Other(_) => { VoiceInputToggleKey::AltRight } }; report_if_error!(self .voice_input_toggle_key .set_value(voice_input_toggle_key, ctx)); report_if_error!(self.explicitly_interacted_with_voice.set_value(true, ctx)); Some(voice_input_toggle_key) } pub fn add_cli_agent_footer_enabled_command( &mut self, command: &str, ctx: &mut ModelContext, ) { let command = command.trim(); if command.is_empty() { return; } if self .cli_agent_footer_enabled_commands .value() .contains_key(command) { return; } let mut map = self.cli_agent_footer_enabled_commands.value().0.clone(); map.insert(command.to_string(), String::new()); report_if_error!(self .cli_agent_footer_enabled_commands .set_value(ToolbarCommandMap::new(map), ctx)); } pub fn remove_cli_agent_footer_enabled_command( &mut self, command: &str, ctx: &mut ModelContext, ) { let command = command.trim(); let mut map = self.cli_agent_footer_enabled_commands.value().0.clone(); map.shift_remove(command); report_if_error!(self .cli_agent_footer_enabled_commands .set_value(ToolbarCommandMap::new(map), ctx)); } pub fn set_cli_agent_for_command( &mut self, pattern: &str, agent: Option, ctx: &mut ModelContext, ) { let mut map = self.cli_agent_footer_enabled_commands.value().0.clone(); if !map.contains_key(pattern) { return; } let value = agent.map(|a| a.to_serialized_name()).unwrap_or_default(); map.insert(pattern.to_string(), value); report_if_error!(self .cli_agent_footer_enabled_commands .set_value(ToolbarCommandMap::new(map), ctx)); } /// Whether the plugin install chip was dismissed for the given agent/host. pub fn is_plugin_install_chip_dismissed(&self, key: &str) -> bool { self.plugin_install_chip_dismissed_map .get(key) .copied() .unwrap_or(false) } /// Mark the plugin install chip as dismissed for the given agent/host. pub fn dismiss_plugin_install_chip(&mut self, key: &str, ctx: &mut ModelContext) { let mut map = self.plugin_install_chip_dismissed_map.clone(); map.insert(key.to_owned(), true); report_if_error!(self.plugin_install_chip_dismissed_map.set_value(map, ctx)); } /// Returns the minimum plugin version for which the update chip was dismissed /// for the given agent/host, or an empty string if not dismissed. pub fn plugin_update_chip_dismissed_version(&self, key: &str) -> &str { self.plugin_update_chip_dismissed_for_version_map .get(key) .map(String::as_str) .unwrap_or("") } /// Record that the user dismissed the update chip for the given agent/host at /// the specified minimum version. pub fn dismiss_plugin_update_chip( &mut self, key: &str, version: String, ctx: &mut ModelContext, ) { let mut map = self.plugin_update_chip_dismissed_for_version_map.clone(); map.insert(key.to_owned(), version); report_if_error!(self .plugin_update_chip_dismissed_for_version_map .set_value(map, ctx)); } pub fn has_seeded_predefined_rules(&self) -> bool { *self.has_seeded_predefined_rules } pub fn mark_predefined_rules_seeded(&mut self, ctx: &mut ModelContext) { report_if_error!(self.has_seeded_predefined_rules.set_value(true, ctx)); } } /// Singleton model that caches compiled regexes for the `cli_agent_footer_enabled_commands` /// setting. Each entry pairs a compiled regex with the CLI agent it maps to. pub struct CompiledCommandsForCodingAgentToolbar { regexes: Vec<(Regex, CLIAgent)>, } impl CompiledCommandsForCodingAgentToolbar { fn parse(app: &AppContext) -> Vec<(Regex, CLIAgent)> { AISettings::as_ref(app) .cli_agent_footer_enabled_commands .value() .iter() .filter_map(|(pattern, agent_name)| { let regex = Regex::new(pattern).ok()?; let agent = CLIAgent::from_serialized_name(agent_name); Some((regex, agent)) }) .collect() } fn register(app: &mut AppContext) { let handle = app.add_singleton_model(|ctx| Self { regexes: Self::parse(ctx), }); let ai_settings = AISettings::handle(app); app.subscribe_to_model(&ai_settings, move |_, event, ctx| { if matches!( event, AISettingsChangedEvent::CLIAgentToolbarEnabledCommands { .. } ) { let regexes = Self::parse(ctx); handle.update(ctx, |me, _| { me.regexes = regexes; }); } }); } /// Returns the CLI agent assigned to the first matching pattern, or `None` /// if no pattern matches the command. pub fn matched_agent(app: &AppContext, command: &str) -> Option { Self::as_ref(app) .regexes .iter() .find(|(regex, _)| regex.is_match(command)) .map(|(_, agent)| *agent) } } impl Entity for CompiledCommandsForCodingAgentToolbar { type Event = (); } impl SingletonEntity for CompiledCommandsForCodingAgentToolbar {} #[cfg(test)] #[path = "ai_tests.rs"] mod tests;