first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
use galaxyui::accessibility::AccessibilityVerbosity;
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
define_settings_group!(AccessibilitySettings, settings: [
a11y_verbosity: AccessibilityVerbosityState {
+373 -339
View File
@@ -6,29 +6,32 @@
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 indexmap::IndexMap;
use regex::Regex;
use serde::de::Deserializer;
use serde::{Deserialize, Serialize};
use settings::{
define_settings_group, RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud,
};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use galaxy_core::execution_mode::AppExecutionMode;
use galaxy_core::features::FeatureFlag;
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::auth::AuthStateProvider;
use crate::report_if_error;
use crate::settings::PrivacySettings;
use crate::terminal::CLIAgent;
use crate::workspaces::user_workspaces::UserWorkspaces;
use cfg_if::cfg_if;
use chrono::{DateTime, Utc};
use galaxyui::platform::OperatingSystem;
use galaxyui::{
platform::keyboard::KeyCode, AppContext, Entity, ModelContext, SingletonEntity, UpdateModel,
};
use lazy_static::lazy_static;
use regex::Regex;
use galaxy_core::execution_mode::AppExecutionMode;
use galaxy_core::features::FeatureFlag;
use settings::{define_settings_group, Setting, SupportedPlatforms, SyncToCloud};
use serde::{de::Deserializer, Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
pub enum FocusedTerminalInfoEvent {
TerminalInfoUpdated,
@@ -392,7 +395,7 @@ impl ThinkingDisplayMode {
}
}
/// Authentication method for AWS Bedrock.
/// Controls how child-agent message bodies are displayed.
#[derive(
Default,
Debug,
@@ -406,107 +409,193 @@ impl ThinkingDisplayMode {
settings_value::SettingsValue,
)]
#[schemars(
description = "Authentication method for AWS Bedrock.",
description = "Controls how child-agent messages are displayed.",
rename_all = "snake_case"
)]
pub enum BedrockAuthMethod {
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]
Profile,
StaticKeys,
Sso,
AlwaysCollapse,
}
settings::macros::implement_setting_for_enum!(
BedrockAuthMethod,
OrchestrationMessageDisplayMode,
AISettings,
SupportedPlatforms::DESKTOP,
SyncToCloud::Never,
SupportedPlatforms::ALL,
SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "ai.bedrock.auth_method",
description: "Authentication method for AWS Bedrock.",
toml_path: "agents.warp_agent.other.orchestration_message_display_mode",
description: "Controls how child-agent messages are displayed.",
);
impl BedrockAuthMethod {
impl OrchestrationMessageDisplayMode {
/// Display name for the settings dropdown.
pub fn display_name(&self) -> &'static str {
match self {
BedrockAuthMethod::Profile => "AWS Profile",
BedrockAuthMethod::StaticKeys => "Static Keys",
BedrockAuthMethod::Sso => "SSO",
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"
}
}
}
}
/// 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 = "default_context_size")]
#[schemars(description = "Maximum context window size in tokens.")]
pub context_size: u32,
}
fn default_context_size() -> u32 {
200_000
}
impl settings_value::SettingsValue for BedrockModelConfig {}
/// 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)]
#[schemars(description = "Whether the model supports image/vision input.")]
pub vision_supported: bool,
#[serde(default = "default_context_size")]
#[schemars(description = "Maximum context window size in tokens.")]
pub context_size: u32,
#[serde(default)]
#[schemars(
description = "Optional provider hint (e.g. anthropic, openai, google) for icon display."
)]
pub provider: Option<String>,
}
impl settings_value::SettingsValue for OpenAIModelConfig {}
/// Configuration for a single OpenAI-compatible provider endpoint.
/// What happens when a prompt is submitted while an agent controls an agent-requested
/// long-running command (LRC).
///
/// Multiple providers can be configured simultaneously (e.g. LiteLLM for cloud models,
/// Ollama for local models, etc.). Each provider has its own endpoint, credentials, and model list.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[schemars(
description = "Configuration for an OpenAI-compatible provider endpoint (e.g. LiteLLM, Ollama, vLLM)."
/// 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,
)]
pub struct OpenAIProviderConfig {
#[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<String>,
#[serde(default)]
#[schemars(description = "Models available from this provider.")]
pub models: Vec<OpenAIModelConfig>,
#[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,
}
impl settings_value::SettingsValue for OpenAIProviderConfig {}
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(
@@ -598,148 +687,6 @@ pub enum AgentModeCodingPermissionsType {
AllowReadingSpecificFiles,
}
/// Predicate types to match commands that can be executed by Agent Mode.
#[derive(Debug, Serialize, Deserialize, Clone)]
enum AgentModeCommandExecutionPredicateType {
/// A regex with start (`^`) and end (`$`) anchors.
///
/// We want regex rules to apply to the entire cmd string so we anchor them
/// (there isn't any efficient way to apply to the entire cmd string at match-time).
#[serde(with = "serde_regex")]
AnchoredRegex(Regex),
}
impl AgentModeCommandExecutionPredicateType {
fn new_regex(regex: &str) -> Result<Self, regex::Error> {
// Redundant anchors aren't a problem so we can unconditionally add them.
let anchored_regex = Regex::new(&format!("^{regex}$"))?;
Ok(Self::AnchoredRegex(anchored_regex))
}
fn matches(&self, cmd: &str) -> bool {
match self {
Self::AnchoredRegex(regex) => regex.is_match(cmd),
}
}
}
impl PartialEq for AgentModeCommandExecutionPredicateType {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::AnchoredRegex(a), Self::AnchoredRegex(b)) => {
// Indexing should be safe since they're guaranteed to have at least
// the anchors around them.
let a_unanchored = &a.as_str()[1..a.as_str().len() - 1];
let b_unanchored = &b.as_str()[1..b.as_str().len() - 1];
a_unanchored == b_unanchored
}
}
}
}
impl std::fmt::Display for AgentModeCommandExecutionPredicateType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AnchoredRegex(regex) => {
write!(f, "{}", &regex.as_str()[1..regex.as_str().len() - 1])
}
}
}
}
/// A wrapper around [`AgentModeCommandExecutionPredicateType`] to enforce
/// the use of the provided constructors rather than direct construction of the variants.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(transparent)]
pub struct AgentModeCommandExecutionPredicate(AgentModeCommandExecutionPredicateType);
impl schemars::JsonSchema for AgentModeCommandExecutionPredicate {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("AgentModeCommandExecutionPredicate")
}
fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
// In the settings file, predicates are serialized as plain regex strings.
gen.subschema_for::<String>()
}
}
impl AgentModeCommandExecutionPredicate {
pub fn new_regex(regex: &str) -> Result<Self, regex::Error> {
Ok(Self(AgentModeCommandExecutionPredicateType::new_regex(
regex,
)?))
}
pub fn matches(&self, cmd: &str) -> bool {
self.0.matches(cmd)
}
}
impl std::fmt::Display for AgentModeCommandExecutionPredicate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl settings_value::SettingsValue for AgentModeCommandExecutionPredicate {
fn to_file_value(&self) -> serde_json::Value {
serde_json::Value::String(self.to_string())
}
fn from_file_value(value: &serde_json::Value) -> Option<Self> {
value.as_str().and_then(|s| Self::new_regex(s).ok())
}
}
lazy_static! {
// Matches optional args / options for a top-level command.
static ref OPTIONAL_ARGS_REGEX: Regex = Regex::new(r"(\s.*)?").expect("Can parse optional args regex");
}
cfg_if! {
// Compiling the regexes for the default command execution allowlist/denylist can be slow
// in an unoptimized build, so we use empty lists in unit tests.
if #[cfg(test)] {
lazy_static! {
pub static ref DEFAULT_COMMAND_EXECUTION_ALLOWLIST: Vec<AgentModeCommandExecutionPredicate> = vec![];
pub static ref DEFAULT_COMMAND_EXECUTION_DENYLIST: Vec<AgentModeCommandExecutionPredicate> = vec![];
}
} else {
lazy_static! {
pub static ref DEFAULT_COMMAND_EXECUTION_ALLOWLIST: Vec<AgentModeCommandExecutionPredicate> = vec![
AgentModeCommandExecutionPredicate::new_regex(&format!("cat{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default cat rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("echo{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default echo rule into regex"),
AgentModeCommandExecutionPredicate::new_regex("find .*").expect("Can parse default find rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("grep{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default grep rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("ls{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default ls rule into regex"),
AgentModeCommandExecutionPredicate::new_regex("which .*").expect("Can parse default which rule into regex"),
];
pub static ref DEFAULT_COMMAND_EXECUTION_DENYLIST: Vec<AgentModeCommandExecutionPredicate> = vec![
AgentModeCommandExecutionPredicate::new_regex(&format!("bash{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default bash rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("fish{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default fish rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("pwsh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default pwsh rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("sh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default sh rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("zsh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default zsh rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("curl{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default curl rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("eval{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default eval rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("exec{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default exec rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("source{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default source rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("wget{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default wget rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("dig{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default dig rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("nslookup{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default nslookup rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("host{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default host rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("ssh{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default ssh rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("scp{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default scp rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("rsync{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default rsync rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("telnet{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default telnet rule into regex"),
AgentModeCommandExecutionPredicate::new_regex(&format!("rm{}", OPTIONAL_ARGS_REGEX.as_str())).expect("Can parse default rm rule into regex"),
];
}
}
}
/// 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).
@@ -1133,8 +1080,21 @@ define_settings_group!(AISettings, settings: [
sync_to_cloud: SyncToCloud::Never,
private: true,
}
// Whether direct Bedrock integration is enabled (client calls Bedrock API directly).
bedrock_enabled: BedrockEnabled {
// 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 to use locally loaded AWS credentials for Bedrock-enabled requests.
aws_bedrock_credentials_enabled: AwsBedrockCredentialsEnabled {
type: bool,
default: true,
supported_platforms: SupportedPlatforms::DESKTOP,
@@ -1229,70 +1189,19 @@ define_settings_group!(AISettings, settings: [
sync_to_cloud: SyncToCloud::Never,
private: true,
}
// Whether the OpenAI-compatible (LiteLLM) provider is enabled.
openai_enabled: OpenAIEnabled {
// 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::Never,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "ai.openai.enabled",
description: "Whether to use an OpenAI-compatible endpoint (e.g. LiteLLM) for AI requests.",
}
// Base URL for the OpenAI-compatible API endpoint.
// Never synced — machine-local infrastructure (e.g. localhost).
openai_base_url: OpenAIBaseUrl {
type: String,
default: "http://localhost:4000/v1".to_string(),
supported_platforms: SupportedPlatforms::DESKTOP,
sync_to_cloud: SyncToCloud::Never,
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.
// Never synced — tied to the specific endpoint configuration.
openai_model: OpenAIModel {
type: String,
default: String::new(),
supported_platforms: SupportedPlatforms::DESKTOP,
sync_to_cloud: SyncToCloud::Never,
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 from LiteLLM or manually configured).
// Never synced to cloud — these are machine-local (tied to local endpoints).
openai_models: OpenAIModels {
type: Vec<OpenAIModelConfig>,
default: Vec::new(),
supported_platforms: SupportedPlatforms::DESKTOP,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "ai.openai.models",
description: "Custom OpenAI-compatible model configurations (e.g. from LiteLLM).",
}
// Multiple OpenAI-compatible provider endpoints (LiteLLM, Ollama, vLLM, etc.).
// Each provider has its own name, base_url, api_key, and model list.
// Never synced to cloud — these are machine-local infrastructure configs.
openai_providers: OpenAIProviders {
type: Vec<OpenAIProviderConfig>,
default: Vec::new(),
supported_platforms: SupportedPlatforms::DESKTOP,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "ai.providers",
description: "Multiple OpenAI-compatible provider endpoints (e.g. LiteLLM, Ollama, local models).",
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 or not the user wants agent mode requests to use their saved rules.
memory_enabled: MemoryEnabled {
@@ -1397,6 +1306,42 @@ define_settings_group!(AISettings, settings: [
// 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,
@@ -1406,16 +1351,16 @@ define_settings_group!(AISettings, settings: [
// Whether or not the user has enabled the ability to use Warp credits even when providing
// their own LLM provider API key.
can_use_warp_credits_with_byok: CanUseWarpCreditsWithByok {
// Whether or not the user has enabled fallback to Warp credits for user-provided models.
can_use_warp_credits_for_fallback: CanUseWarpCreditsForFallback {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
storage_key: "CanUseWarpCreditsWithByok",
toml_path: "cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok",
description: "Whether Galaxy credits can be used even when providing your own API key.",
description: "Whether Warp credits can be used as a fallback for user-provided models.",
}
should_render_use_agent_footer_for_user_commands: ShouldRenderUseAgentToolbarForUserCommands {
@@ -1478,6 +1423,18 @@ define_settings_group!(AISettings, settings: [
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).
@@ -1507,31 +1464,6 @@ define_settings_group!(AISettings, settings: [
private: true,
}
// This is not a user-visible setting - it tracks whether the FTU model picker callout
// has been shown to the user. We set this to `true` as soon as the callout is first
// displayed (not when it's dismissed), so it never re-appears.
//
// Note: this setting was originally named "dismissed" but we now use it to mean "shown".
// We kept the same setting key so that users who already dismissed the callout on an
// older client don't see it again.
ftu_model_callout_dismissed: FtuModelCalloutDismissed {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: true,
}
// Tracks whether we've done the one-time auto-open of the conversation list for discoverability.
// Once set to true, the conversation list visibility will be restored from workspace state.
has_auto_opened_conversation_list: HasAutoOpenedConversationList {
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.
@@ -1559,6 +1491,18 @@ define_settings_group!(AISettings, settings: [
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
@@ -1576,6 +1520,19 @@ define_settings_group!(AISettings, settings: [
// 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.
@@ -1659,6 +1616,49 @@ define_settings_group!(AISettings, settings: [
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,
}
]);
impl AISettings {
@@ -1668,7 +1668,7 @@ impl AISettings {
CompiledCommandsForCodingAgentToolbar::register(app);
app.update_model(&Self::handle(app), |_me, ctx| {
ctx.subscribe_to_model(&FocusedTerminalInfo::handle(ctx), |_me, event, 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 {
@@ -1839,8 +1839,42 @@ impl AISettings {
*self.file_based_mcp_enabled
}
pub fn is_orchestration_enabled(&self, _app: &galaxyui::AppContext) -> bool {
false
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.
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.
@@ -1929,11 +1963,7 @@ impl AISettings {
}
pub fn is_command_denylist_editable(&self, app: &AppContext) -> bool {
let set_by_workspace = UserWorkspaces::as_ref(app)
.ai_autonomy_settings()
.has_override_for_execute_commands_denylist();
self.is_any_ai_enabled(app) && !set_by_workspace
self.is_any_ai_enabled(app)
}
pub fn is_command_allowlist_editable(&self, app: &AppContext) -> bool {
@@ -1999,6 +2029,10 @@ impl AISettings {
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
}
+33 -23
View File
@@ -1,12 +1,13 @@
use super::*;
use crate::{
ai::request_usage_model::{RequestLimitInfo, RequestLimitRefreshDuration},
test_util::settings::initialize_settings_for_tests,
};
use chrono::Utc;
use galaxy_graphql::scalars::time::ServerTimestamp;
use galaxyui::{App, SingletonEntity};
use super::*;
use crate::ai::request_usage_model::{RequestLimitInfo, RequestLimitRefreshDuration};
use crate::auth::AuthStateProvider;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::user_workspaces::UserWorkspaces;
fn create_test_request_limit_info(
limit: usize,
used: usize,
@@ -30,6 +31,11 @@ fn create_test_request_limit_info(
}
}
fn add_ai_enablement_dependencies_for_test(app: &mut App) {
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(UserWorkspaces::default_mock);
}
// FocusedTerminalInfo Tests
#[test]
@@ -40,11 +46,10 @@ fn test_update_both_values_changed() {
// Setup event tracking
let (sender, receiver) = async_channel::unbounded();
let model_handle_clone = model_handle.clone();
model_handle.update(&mut app, move |_, ctx| {
app.update(|ctx| {
let sender = sender.clone();
ctx.subscribe_to_model(
&model_handle_clone,
&model_handle,
move |_, event: &FocusedTerminalInfoEvent, _| match event {
FocusedTerminalInfoEvent::TerminalInfoUpdated => {
let _ = sender.try_send(());
@@ -81,11 +86,10 @@ fn test_update_additional_value_changed() {
// Setup event tracking
let (sender, receiver) = async_channel::unbounded();
let model_handle_clone = model_handle.clone();
model_handle.update(&mut app, move |_, ctx| {
app.update(|ctx| {
let sender = sender.clone();
ctx.subscribe_to_model(
&model_handle_clone,
&model_handle,
move |_, event: &FocusedTerminalInfoEvent, _| match event {
FocusedTerminalInfoEvent::TerminalInfoUpdated => {
let _ = sender.try_send(());
@@ -130,11 +134,10 @@ fn test_update_no_change() {
// Setup event tracking
let (sender, receiver) = async_channel::unbounded();
let model_handle_clone = model_handle.clone();
model_handle.update(&mut app, move |_, ctx| {
app.update(|ctx| {
let sender = sender.clone();
ctx.subscribe_to_model(
&model_handle_clone,
&model_handle,
move |_, event: &FocusedTerminalInfoEvent, _| match event {
FocusedTerminalInfoEvent::TerminalInfoUpdated => {
let _ = sender.try_send(());
@@ -179,11 +182,10 @@ fn test_update_only_remote_toggles() {
// Setup event tracking
let (sender, receiver) = async_channel::unbounded();
let model_handle_clone = model_handle.clone();
model_handle.update(&mut app, move |_, ctx| {
app.update(|ctx| {
let sender = sender.clone();
ctx.subscribe_to_model(
&model_handle_clone,
&model_handle,
move |_, event: &FocusedTerminalInfoEvent, _| match event {
FocusedTerminalInfoEvent::TerminalInfoUpdated => {
let _ = sender.try_send(());
@@ -228,11 +230,10 @@ fn test_update_only_restored_toggles() {
// Setup event tracking
let (sender, receiver) = async_channel::unbounded();
let model_handle_clone = model_handle.clone();
model_handle.update(&mut app, move |_, ctx| {
app.update(|ctx| {
let sender = sender.clone();
ctx.subscribe_to_model(
&model_handle_clone,
&model_handle,
move |_, event: &FocusedTerminalInfoEvent, _| match event {
FocusedTerminalInfoEvent::TerminalInfoUpdated => {
let _ = sender.try_send(());
@@ -314,7 +315,6 @@ fn test_toolbar_command_map_from_file_value_map_format() {
#[test]
fn test_toolbar_command_map_from_file_value_legacy_array() {
use settings_value::SettingsValue;
// Patterns are intentionally non-alphabetical to verify insertion order is preserved.
let value = serde_json::json!(["^zebra", "^alpha", "^middle"]);
@@ -329,7 +329,6 @@ fn test_toolbar_command_map_from_file_value_legacy_array() {
#[test]
fn test_toolbar_command_map_from_file_value_invalid() {
use settings_value::SettingsValue;
let value = serde_json::json!(42);
assert!(ToolbarCommandMap::from_file_value(&value).is_none());
@@ -337,7 +336,6 @@ fn test_toolbar_command_map_from_file_value_invalid() {
#[test]
fn test_toolbar_command_map_roundtrip() {
use settings_value::SettingsValue;
let mut inner = IndexMap::new();
inner.insert("^claude".to_string(), "Claude".to_string());
@@ -383,6 +381,18 @@ fn test_toolbar_command_map_matched_agent() {
});
}
#[test]
fn orchestration_is_enabled_when_ai_is_enabled() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
add_ai_enablement_dependencies_for_test(&mut app);
AISettings::handle(&app).read(&app, |settings, ctx| {
assert!(settings.is_orchestration_enabled(ctx));
});
});
}
#[test]
fn test_should_display_quota_reset_banner_with_empty_history() {
App::test((), |mut app| async move {
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
define_settings_group!(AliasExpansionSettings, settings: [
alias_expansion_enabled: AliasExpansionEnabled {
+13
View File
@@ -1,6 +1,9 @@
use enum_iterator::Sequence;
use galaxy_core::settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use serde::{Deserialize, Serialize};
use galaxy_core::channel::{Channel, ChannelState};
use galaxy_core::settings::macros::define_settings_group;
use galaxy_core::settings::{SupportedPlatforms, SyncToCloud};
/// The app icon to use (mac-only).
///
@@ -72,4 +75,14 @@ define_settings_group!(AppIconSettings, settings: [
toml_path: "appearance.icon.app_icon",
description: "The app icon displayed in the dock.",
},
show_dock_icon: ShowDockIconState {
type: bool,
default: true,
supported_platforms: SupportedPlatforms::MAC,
sync_to_cloud: SyncToCloud::Never,
private: false,
storage_key: "ShowDockIcon",
toml_path: "appearance.icon.show_dock_icon",
description: "Whether Warp is shown in the macOS Dock and Cmd-Tab switcher.",
},
]);
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
#[derive(
Clone,
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
// Settings for visibility of non-user command blocks like the bootstrap block
// and in-band command blocks.
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
define_settings_group!(ChangelogSettings, settings: [
show_changelog_after_update: ShowChangelogAfterUpdate {
+8 -122
View File
@@ -1,20 +1,13 @@
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use cloud_object_models::{CloudPreference, CloudPreferenceModel, Platform, Preference};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
use crate::{
cloud_object::{
model::{
generic_string_model::{GenericStringModel, GenericStringObjectId, StringModel},
json_model::{JsonModel, JsonSerializer},
},
GenericCloudObject, GenericStringObjectFormat, GenericStringObjectUniqueKey,
JsonObjectType, Revision, ServerCloudObject, UniquePer,
},
server::sync_queue::QueueItem,
use crate::cloud_object::model::generic_string_model::StringModel;
use crate::cloud_object::model::json_model::JsonModel;
use crate::cloud_object::{
GenericStringObjectFormat, GenericStringObjectUniqueKey, JsonObjectType, Revision, UniquePer,
};
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use crate::server::sync_queue::QueueItem;
define_settings_group!(CloudPreferencesSettings, settings: [
settings_sync_enabled: IsSettingsSyncEnabled {
type: bool,
@@ -27,106 +20,6 @@ define_settings_group!(CloudPreferencesSettings, settings: [
},
]);
pub type CloudPreference = GenericCloudObject<GenericStringObjectId, CloudPreferenceModel>;
pub type CloudPreferenceModel = GenericStringModel<Preference, JsonSerializer>;
/// Defines the platform that a preference was set on.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Platform {
Mac,
Linux,
Windows,
Web,
/// This implies the preference applies on all supported platforms
Global,
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Mac => write!(f, "Mac"),
Self::Linux => write!(f, "Linux"),
Self::Windows => write!(f, "Windows"),
Self::Web => write!(f, "Web"),
Self::Global => write!(f, "Global"),
}
}
}
impl Platform {
pub fn applies_to_current_platform(&self) -> bool {
*self == Platform::current_platform() || *self == Platform::Global
}
}
impl Platform {
pub fn current_platform() -> Self {
if cfg!(all(not(target_family = "wasm"), target_os = "macos")) {
return Self::Mac;
}
if cfg!(all(not(target_family = "wasm"), target_os = "linux")) {
return Self::Linux;
}
if cfg!(all(not(target_family = "wasm"), target_os = "windows")) {
return Self::Windows;
}
if cfg!(target_family = "wasm") {
return Self::Web;
}
panic!("Unsupported platform");
}
}
/// Defines the data model for a cloud synced user preference.
///
/// The expected usage is that each storage key is modeled as its own cloud preference object.
/// This allows users to edit individual cloud preferences with less fear of an offline
/// collision (e.g. if I change one preference on one machine and then update another while
/// offline on another machine, modeling them individually allows for both changes to be applied).
///
/// Note that I considered adding a concept of "preference group" as a higher level namespace
/// for preferences (in case users want to create groups of them), but decided to hold off on
/// this until we actually support that feature.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Preference {
/// The storage key (unique identifier for this preference).
pub storage_key: String,
/// The value of the preference, which can be any JSON value.
pub value: Value,
/// The platform that this preference was set on.
/// If the preference is global, this will be set to Platform::Global.
pub platform: Platform,
}
impl Preference {
/// Creates a new preference object with the given storage key and value and the appropriate
/// platform key for the given syncing mode.
/// Used when creating a new preference the first time. For preferences synced from the
/// cloud they will desererialize directly from JSON.
pub fn new(storage_key: String, value: &str, syncing_mode: SyncToCloud) -> Result<Self> {
let platform = match syncing_mode {
SyncToCloud::PerPlatform(_) => Platform::current_platform(),
SyncToCloud::Globally(_) => Platform::Global,
SyncToCloud::Never => Err(anyhow!(
"Cannot create a preference with SyncToCloud::Never"
))?,
};
match serde_json::from_str(value) {
Ok(value) => Ok(Self {
storage_key,
value,
platform,
}),
Err(err) => Err(anyhow!("Failed to parse preference value {}", err)),
}
}
}
/// Defines a based model for syncing cloud preferences.
impl StringModel for Preference {
type CloudObjectType = CloudPreference;
@@ -150,13 +43,6 @@ impl StringModel for Preference {
false
}
fn new_from_server_update(&self, server_cloud_object: &ServerCloudObject) -> Option<Self> {
if let ServerCloudObject::Preference(server_preference) = server_cloud_object {
return Some(server_preference.model.clone().string_model);
}
None
}
fn model_format() -> GenericStringObjectFormat {
GenericStringObjectFormat::Json(Self::json_object_type())
}
+36 -44
View File
@@ -1,51 +1,38 @@
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
sync::Arc,
};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use cloud_object_models::JsonSerializer;
use lazy_static::lazy_static;
use settings::{Setting as _, SyncToCloud};
use galaxy_core::execution_mode::AppExecutionMode;
use galaxy_core::r#async::debounce;
use galaxy_core::settings::ChangeEventReason;
use galaxy_core::user_preferences::GetUserPreferences;
use galaxyui::r#async::Timer;
use galaxyui::{Entity, ModelContext, SingletonEntity};
use galaxyui::{Entity, ModelContext, ModelHandle, SingletonEntity};
use galaxyui_extras::user_preferences::toml_backed::TomlBackedUserPreferences;
use lazy_static::lazy_static;
use settings::{Setting as _, SyncToCloud};
use std::time::Duration;
use crate::{
auth::auth_state::AuthState,
cloud_object::{
model::{
generic_string_model::GenericStringObjectId, json_model::JsonSerializer,
persistence::CloudModel,
},
CloudObjectEventEntrypoint, GenericStringObjectFormat, JsonObjectType,
},
debounce::debounce,
drive::CloudObjectTypeAndId,
report_if_error,
server::{
cloud_objects::update_manager::{
GenericStringObjectInput, InitiatedBy, UpdateManager, UpdateManagerEvent,
},
ids::{ClientId, SyncId},
sync_queue::{SyncQueue, SyncQueueEvent},
},
settings::{
cloud_preferences::{CloudPreference, CloudPreferenceModel, Platform, Preference},
manager::SettingsManager,
},
workspaces::user_workspaces::UserWorkspaces,
use super::cloud_preferences::{CloudPreferencesSettings, CloudPreferencesSettingsChangedEvent};
use super::manager::SettingsEvent;
use super::PrivacySettings;
use crate::auth::auth_state::AuthState;
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::{CloudObjectEventEntrypoint, GenericStringObjectFormat, JsonObjectType};
use crate::drive::CloudObjectTypeAndId;
use crate::report_if_error;
use crate::server::cloud_objects::update_manager::{
GenericStringObjectInput, InitiatedBy, UpdateManager, UpdateManagerEvent,
};
use galaxy_core::execution_mode::AppExecutionMode;
use super::{
cloud_preferences::{CloudPreferencesSettings, CloudPreferencesSettingsChangedEvent},
manager::SettingsEvent,
PrivacySettings,
use crate::server::ids::{ClientId, SyncId};
use crate::server::sync_queue::{SyncQueue, SyncQueueEvent};
use crate::settings::cloud_preferences::{
CloudPreference, CloudPreferenceModel, Platform, Preference,
};
use crate::settings::manager::SettingsManager;
use crate::workspaces::user_workspaces::UserWorkspaces;
/// Provides client ids for creating cloud preferences.
/// We define this as a trait so tests can track what client ids are created and use
@@ -216,7 +203,7 @@ impl CloudPreferencesSyncer {
// handle_initial_load. This prevents the CloudPreferencesUpdated event (which fires
// synchronously in on_changed_objects_fetched) from overwriting local settings
// before handle_initial_load has a chance to determine sync direction.
ctx.subscribe_to_model(&UpdateManager::handle(ctx), |syncer, event, ctx| {
ctx.subscribe_to_model(&UpdateManager::handle(ctx), |syncer, _, event, ctx| {
if let UpdateManagerEvent::CloudPreferencesUpdated { updated } = event {
// Defer cloud→local updates until `handle_initial_load`
// has determined the correct sync direction. The
@@ -243,7 +230,7 @@ impl CloudPreferencesSyncer {
);
ctx.subscribe_to_model(
&SettingsManager::handle(ctx),
|me, event, ctx| match event {
|me, _, event, ctx| match event {
SettingsEvent::LocalPreferencesUpdated { storage_key, .. } => {
me.handle_local_preference_updated(storage_key, ctx);
}
@@ -258,7 +245,7 @@ impl CloudPreferencesSyncer {
ctx.subscribe_to_model(&SyncQueue::handle(ctx), Self::handle_sync_queue_event);
ctx.subscribe_to_model(
&CloudPreferencesSettings::handle(ctx),
|me, event, ctx| match event {
|me, _, event, ctx| match event {
CloudPreferencesSettingsChangedEvent::IsSettingsSyncEnabled {
change_event_reason,
} => {
@@ -298,7 +285,12 @@ impl CloudPreferencesSyncer {
/// Handles SyncQueue success events by updating the stored
/// settings file hash when a cloud preference is successfully
/// created or updated on the server.
fn handle_sync_queue_event(&mut self, event: &SyncQueueEvent, ctx: &mut ModelContext<Self>) {
fn handle_sync_queue_event(
&mut self,
_: ModelHandle<SyncQueue>,
event: &SyncQueueEvent,
ctx: &mut ModelContext<Self>,
) {
let server_id = match event {
SyncQueueEvent::ObjectCreationSuccessful {
server_creation_info,
@@ -1,48 +1,37 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
sync::Arc,
sync::Mutex,
time::Duration,
};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use chrono::{DateTime, Utc};
use cloud_object_client::MockObjectClient;
use galaxy_core::settings::macros::define_settings_group;
use galaxy_core::settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
use galaxy_core::user_preferences::GetUserPreferences;
use galaxyui::{App, SingletonEntity};
use crate::{
auth::auth_state::AuthState,
cloud_object::{
model::generic_string_model::GenericStringObjectId, BulkCreateCloudObjectResult,
CreatedCloudObject, GenericStringObjectFormat, GenericStringObjectUniqueKey,
JsonObjectType, ObjectDeleteResult, ObjectIdType, Owner, Revision, RevisionAndLastEditor,
ServerMetadata, ServerObject, ServerPermissions, ServerPreference, UniquePer,
UpdateCloudObjectResult,
},
server::{
cloud_objects::{
fake_object_client::FakeObjectClient,
test_utils::{create_update_manager_struct, initialize_app, UpdateManagerStruct},
update_manager::{InitialLoadResponse, UpdateManager},
},
ids::{ClientId, ServerId, ServerIdAndType, SyncId},
server_api::object::MockObjectClient,
sync_queue::SyncQueue,
},
settings::cloud_preferences::{CloudPreferenceModel, CloudPreferencesSettings, Platform},
Assets,
};
use galaxy_core::{
settings::{
macros::define_settings_group, RespectUserSyncSetting, Setting, SupportedPlatforms,
SyncToCloud,
},
user_preferences::GetUserPreferences,
};
use super::{
initialize_cloud_preferences_syncer, ClientIdProvider, CloudPreferencesSyncer,
ForceCloudToMatchLocal, SETTINGS_FILE_LAST_SYNCED_HASH_KEY,
};
use crate::auth::auth_state::AuthState;
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
use crate::cloud_object::{
BulkCreateCloudObjectResult, CreatedCloudObject, GenericStringObjectFormat,
GenericStringObjectUniqueKey, JsonObjectType, ObjectDeleteResult, ObjectIdType, Owner,
Revision, RevisionAndLastEditor, ServerMetadata, ServerObject, ServerPermissions,
ServerPreference, UniquePer, UpdateCloudObjectResult,
};
use crate::server::cloud_objects::fake_object_client::FakeObjectClient;
use crate::server::cloud_objects::test_utils::{
create_update_manager_struct, initialize_app, UpdateManagerStruct,
};
use crate::server::cloud_objects::update_manager::{InitialLoadResponse, UpdateManager};
use crate::server::ids::{ClientId, ServerId, ServerIdAndType, SyncId};
use crate::server::sync_queue::SyncQueue;
use crate::settings::cloud_preferences::{
CloudPreferenceModel, CloudPreferencesSettings, Platform,
};
use crate::ASSETS;
define_settings_group!(TestSettings, settings: [
all_platforms_cloud_setting: AllPlatforms {
@@ -170,18 +159,18 @@ fn initial_load_response_with_cloud_settings(
current_editor_uid: None,
};
let cloud_setting = ServerPreference {
id: SyncId::ServerId(id.into()),
let cloud_setting = ServerPreference::new(
SyncId::ServerId(id.into()),
CloudPreferenceModel::deserialize_owned(&setting.serialized_preference)
.expect("error creating preference"),
metadata,
permissions: ServerPermissions {
ServerPermissions {
space: Owner::mock_current_user(),
guests: Vec::new(),
anyone_link_sharing: None,
permissions_last_updated_ts: Utc::now().into(),
},
model: CloudPreferenceModel::deserialize_owned(&setting.serialized_preference)
.expect("error creating preference"),
};
);
Box::new(cloud_setting) as Box<dyn ServerObject>
})
.collect::<Vec<Box<dyn ServerObject>>>();
@@ -207,9 +196,21 @@ async fn spawned_sync_queue_future_at_index(app: &mut App, index: usize) {
})
.await
}
async fn wait_for_num_spawned_futures(app: &mut App, expected_num: usize, message: &str) {
for _ in 0..50 {
let num_spawned_futures =
SyncQueue::handle(app).read(app, |sync_queue, _ctx| sync_queue.spawned_futures().len());
if num_spawned_futures == expected_num {
return;
}
warpui::r#async::Timer::after(Duration::from_millis(100)).await;
}
assert_num_spawned_futures(app, expected_num, message);
}
async fn await_spawned_futures(app: &mut App, num_futures: usize, message: &str) {
assert_num_spawned_futures(app, num_futures, message);
wait_for_num_spawned_futures(app, num_futures, message).await;
for _ in 0..num_futures {
spawned_sync_queue_future_at_index(app, 0).await;
}
@@ -281,7 +282,7 @@ fn expect_bulk_create_generic_string_objects(
#[test]
fn test_sync_local_pref_to_cloud_after_initial_sync_creates_prefs_setting() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -327,19 +328,22 @@ fn test_sync_local_pref_to_cloud_after_initial_sync_creates_prefs_setting() {
#[test]
fn test_sync_local_pref_to_cloud_after_initial_sync() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
let is_mac = cfg!(all(not(target_family = "wasm"), target_os = "macos"));
let is_linux = cfg!(all(not(target_family = "wasm"), target_os = "linux"));
let is_linux = cfg!(all(
not(target_family = "wasm"),
any(target_os = "linux", target_os = "freebsd")
));
let mut all_client_ids = expect_sync_preferences_setting(&mut server_api);
all_client_ids.append(&mut expect_sync_server_stored_privacy_settings(
&mut server_api,
));
// Expect the creation of one or two cloud settings in seperate requests depending on the platform
// Expect the creation of one or two cloud settings in separate requests depending on the platform
all_client_ids.append(&mut expect_bulk_create_generic_string_objects(
&mut server_api,
1,
@@ -429,7 +433,10 @@ fn test_sync_local_pref_to_cloud_after_initial_sync() {
.set_value(true, ctx);
if cfg!(all(not(target_family = "wasm"), target_os = "macos")) {
let _ = test_settings.mac_only_cloud_setting.set_value(true, ctx);
} else if cfg!(all(not(target_family = "wasm"), target_os = "linux")) {
} else if cfg!(all(
not(target_family = "wasm"),
any(target_os = "linux", target_os = "freebsd")
)) {
let _ = test_settings.linux_only_cloud_setting.set_value(true, ctx);
}
let _ = test_settings.non_cloud_setting.set_value(true, ctx);
@@ -450,12 +457,15 @@ fn test_sync_local_pref_to_cloud_after_initial_sync() {
}
fn run_initial_sync_test(is_onboarded: bool) {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
let is_mac = cfg!(all(not(target_family = "wasm"), target_os = "macos"));
let is_linux = cfg!(all(not(target_family = "wasm"), target_os = "linux"));
let is_linux = cfg!(all(
not(target_family = "wasm"),
any(target_os = "linux", target_os = "freebsd")
));
let mut all_client_ids = expect_sync_preferences_setting(&mut server_api);
@@ -589,7 +599,7 @@ fn test_sync_local_pref_to_cloud_on_initial_sync_for_returning_user() {
#[test]
fn test_sync_local_pref_to_cloud_updates_existing_pref() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -676,7 +686,7 @@ fn test_sync_local_pref_to_cloud_updates_existing_pref() {
#[test]
fn test_sync_cloud_pref_to_local_on_initial_load_or_collab_update() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -748,7 +758,10 @@ fn test_sync_cloud_pref_to_local_on_initial_load_or_collab_update() {
.await;
let is_mac = cfg!(all(not(target_family = "wasm"), target_os = "macos"));
let is_linux = cfg!(all(not(target_family = "wasm"), target_os = "linux"));
let is_linux = cfg!(all(
not(target_family = "wasm"),
any(target_os = "linux", target_os = "freebsd")
));
app.read(|ctx| {
let settings = TestSettings::as_ref(ctx);
assert!(
@@ -788,7 +801,7 @@ fn test_sync_cloud_pref_to_local_on_initial_load_or_collab_update() {
#[test]
fn test_cloud_preferences_setting_initial_load_skipped_when_setting_is_off() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -856,7 +869,10 @@ fn test_cloud_preferences_setting_initial_load_skipped_when_setting_is_off() {
});
let is_mac = cfg!(all(not(target_family = "wasm"), target_os = "macos"));
let is_linux = cfg!(all(not(target_family = "wasm"), target_os = "linux"));
let is_linux = cfg!(all(
not(target_family = "wasm"),
any(target_os = "linux", target_os = "freebsd")
));
app.read(|ctx| {
let settings = TestSettings::as_ref(ctx);
assert!(
@@ -910,7 +926,7 @@ fn test_cloud_preferences_setting_initial_load_skipped_when_setting_is_off() {
#[test]
fn test_sync_local_pref_to_cloud_doesnt_update_equal_pref() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -1014,7 +1030,7 @@ fn test_sync_local_pref_to_cloud_doesnt_update_equal_pref() {
#[test]
fn test_cloud_preferences_setting_enabling_setting_syncs_prefs() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
// Start with cloud prefs disabled
initialize_settings(&mut app);
@@ -1075,7 +1091,7 @@ fn test_cloud_preferences_setting_enabling_setting_syncs_prefs() {
#[test]
fn test_cloud_pref_not_synced_when_current_value_not_syncable() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -1137,7 +1153,7 @@ fn test_cloud_pref_not_synced_when_current_value_not_syncable() {
#[test]
fn test_ensure_no_duplicate_cloud_prefs() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
let mut server_api = mock_object_client_with_base_expectations();
@@ -1298,7 +1314,7 @@ fn write_stored_hash(app: &App, value: &str) {
#[test]
fn test_force_local_wins_on_startup_uploads_local_to_cloud() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
// Step 1: create a real temp settings.toml. The file's hash is
@@ -1389,7 +1405,7 @@ fn test_force_local_wins_on_startup_uploads_local_to_cloud() {
#[test]
fn test_no_force_local_when_hashes_match() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
// Create a file and seed the stored hash with its exact value
@@ -1447,7 +1463,7 @@ fn test_no_force_local_when_hashes_match() {
#[test]
fn test_force_local_suppressed_when_file_is_broken() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
// Create a file whose contents happen to hash to something,
@@ -1518,7 +1534,7 @@ fn test_force_local_suppressed_when_file_is_broken() {
#[test]
fn test_file_missing_with_stored_hash_lets_cloud_win() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
// Use a path that doesn't exist: the user has deleted their
@@ -1571,7 +1587,7 @@ fn test_file_missing_with_stored_hash_lets_cloud_win() {
#[test]
fn test_first_launch_with_no_stored_hash_lets_cloud_win() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
// Fresh install: a settings.toml exists but there is no
@@ -1627,7 +1643,7 @@ fn test_first_launch_with_no_stored_hash_lets_cloud_win() {
#[test]
fn test_offline_ui_change_does_not_update_hash_until_sync_succeeds() {
App::test(Assets, |mut app| async move {
App::test(ASSETS, |mut app| async move {
initialize_settings(&mut app);
// Phase 1: normal startup. File and stored hash match, cloud
+22 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
define_settings_group!(CodeSettings, settings: [
code_as_default_editor: CodeAsDefaultEditor {
@@ -58,4 +59,24 @@ define_settings_group!(CodeSettings, settings: [
toml_path: "code.editor.show_global_search",
description: "Whether global file search is shown in the tools panel.",
},
// Controls whether hidden files (dotfiles) are shown in the project explorer.
show_hidden_files: ShowHiddenFiles {
type: bool,
default: true,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "code.editor.show_hidden_files",
description: "Whether hidden files (dotfiles) are shown in the project explorer.",
},
// Controls whether the language server reformats the file on save.
format_on_save: FormatOnSave {
type: bool,
default: true,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "code.editor.format_on_save",
description: "Whether the language server automatically formats the file on save. Other LSP features (hover, go-to-definition, references, diagnostics) are unaffected.",
},
]);
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, Setting, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{Setting, SupportedPlatforms, SyncToCloud};
// Debug mode settings.
//
+43 -1
View File
@@ -3,7 +3,8 @@ use std::fmt::{Display, Formatter};
use enum_iterator::{all, Sequence};
use galaxyui::ModelContext;
use serde::{Deserialize, Serialize};
use settings::{macros::define_settings_group, Setting as _, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, Setting as _, SupportedPlatforms, SyncToCloud};
#[derive(
Clone,
@@ -77,6 +78,38 @@ impl Display for CursorDisplayType {
}
}
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
PartialEq,
Deserialize,
Serialize,
Sequence,
schemars::JsonSchema,
settings_value::SettingsValue,
)]
#[schemars(
description = "How line numbers are displayed in code editors.",
rename_all = "snake_case"
)]
pub enum CodeEditorLineNumberMode {
#[default]
Absolute,
Relative,
}
impl CodeEditorLineNumberMode {
pub fn dropdown_item_label(&self) -> &'static str {
match self {
Self::Absolute => "Absolute",
Self::Relative => "Relative",
}
}
}
#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq)]
pub enum TabBehavior {
#[default]
@@ -193,6 +226,15 @@ define_settings_group!(AppEditorSettings, settings: [
toml_path: "text_editing.vim_status_bar",
description: "Whether the Vim status bar is displayed.",
},
code_editor_line_number_mode: CodeEditorLineNumberModeSetting {
type: CodeEditorLineNumberMode,
default: CodeEditorLineNumberMode::default(),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "text_editing.code_editor_line_number_mode",
description: "How line numbers are displayed in code editors.",
},
autocomplete_symbols: AutocompleteSymbols {
type: bool,
default: true,
+3 -1
View File
@@ -1,5 +1,7 @@
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
use crate::banner::BannerState;
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
// This isn't exactly a setting, but rather a record of a
// user action that should be persisted the same way we would a setting.
+5 -3
View File
@@ -1,8 +1,10 @@
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
use galaxy_core::ui::builder::MIN_FONT_SIZE;
use galaxyui::{fonts::Weight, rendering::ThinStrokes, AppContext, SingletonEntity};
use galaxyui::elements::DEFAULT_UI_LINE_HEIGHT_RATIO;
use settings::{macros::define_settings_group, Setting, SupportedPlatforms, SyncToCloud};
use galaxyui::fonts::Weight;
use galaxyui::rendering::ThinStrokes;
use galaxyui::{AppContext, SingletonEntity};
use super::EnforceMinimumContrast as EnforceMinimumContrastEnum;
+3 -2
View File
@@ -1,12 +1,13 @@
use settings::macros::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
use galaxyui::platform::GraphicsBackend;
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
define_settings_group!(GPUSettings, settings: [
prefer_low_power_gpu: PreferLowPowerGPU {
type: bool,
// Opt for the low power (integrated) GPU on Windows / Linux since discrete GPUs tend to be
// more unstable.
default: cfg!(any(target_os = "linux", windows)),
default: cfg!(any(target_os = "linux", target_os = "freebsd", windows)),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
+9 -8
View File
@@ -1,19 +1,20 @@
use crate::settings::import::config::ThemeError;
use std::env;
use std::io::ErrorKind;
use std::path::PathBuf;
use async_recursion::async_recursion;
use async_trait::async_trait;
use galaxy_core::ui::{
color::hex_color::coloru_from_hex_string,
theme::{AnsiColor, AnsiColors, GalaxyTheme, TerminalColors},
};
use galaxyui::fonts::FontInfo;
use pathfinder_color::ColorU;
use serde::Deserialize;
use std::{env, io::ErrorKind, path::PathBuf};
use galaxy_core::ui::color::hex_color::coloru_from_hex_string;
use galaxy_core::ui::theme::{AnsiColor, AnsiColors, TerminalColors, WarpTheme};
use galaxyui::fonts::FontInfo;
use super::config::{
calculate_accent_color, Config, ConfigError, ImportableSetting, ParseableConfig, SettingType,
ThemeType,
};
use pathfinder_color::ColorU;
use crate::settings::import::config::ThemeError;
type AlacrittyColor = String;
const CONFIG_DEPTH_LIMIT: u8 = 5;
@@ -1,12 +1,13 @@
use async_io::block_on;
use galaxy_core::ui::{color::hex_color::coloru_from_hex_string, theme::AnsiColor};
use virtual_fs::{Stub, VirtualFS};
use crate::settings::import::config::{ParseableConfig, ThemeType};
use galaxy_core::ui::color::hex_color::coloru_from_hex_string;
use galaxy_core::ui::theme::AnsiColor;
use super::{
AlacrittyColors, AlacrittyConfig, AlacrittyTheme, PrimaryAlacrittyColors, RecursivelyParseable,
};
use crate::settings::import::config::{ParseableConfig, ThemeType};
#[test]
fn test_parse_cobalt2() {
+17 -20
View File
@@ -1,31 +1,28 @@
use std::{path::PathBuf, sync::Arc};
use std::path::PathBuf;
use std::sync::Arc;
use galaxy_core::ui::{
color::hex_color::HexColorError as UiHexColorError,
theme::{AnsiColors, GalaxyTheme},
};
use async_trait::async_trait;
use pathfinder_color::ColorU;
use serde::Serialize;
use strum_macros::EnumIter;
use async_trait::async_trait;
use galaxyui::{fonts::FontInfo, keymap::Keystroke, DisplayIdx};
use thiserror::Error;
use galaxy_core::ui::color::hex_color::HexColorError as UiHexColorError;
use galaxy_core::ui::theme::{AnsiColors, WarpTheme};
use galaxyui::fonts::FontInfo;
use galaxyui::keymap::Keystroke;
use galaxyui::DisplayIdx;
use crate::{
interval_timer::IntervalTimer,
root_view::QuakeModePinPosition,
settings::ExtraMetaKeys,
terminal::session_settings::{StartupShell, WorkingDirectoryConfig},
themes::theme_creator::pick_accent_color_from_options,
};
#[cfg(feature = "local_fs")]
use crate::{themes::theme_creator_body::ThemeCreatorBody, user_config};
use super::{alacritty_parser::AlacrittyConfig, model::TerminalType};
use super::alacritty_parser::AlacrittyConfig;
#[cfg(target_os = "macos")]
use super::iterm_parser::ITermProfile;
use super::model::TerminalType;
use crate::interval_timer::IntervalTimer;
use crate::root_view::QuakeModePinPosition;
use crate::settings::ExtraMetaKeys;
use crate::terminal::session_settings::{StartupShell, WorkingDirectoryConfig};
use crate::themes::theme_creator::pick_accent_color_from_options;
#[cfg(feature = "local_fs")]
use crate::{themes::theme_creator_body::ThemeCreatorBody, user_config};
#[derive(Debug)]
pub enum ThemeType {
+12 -15
View File
@@ -10,27 +10,24 @@ use itertools::Itertools;
use palette::Srgba;
use pathfinder_color::ColorU;
use plist::{Dictionary, Value};
use crate::{
root_view::QuakeModePinPosition,
settings::{
import::config::HotkeyError, ExtraMetaKeys, DEFAULT_MONOSPACE_FONT_NAME,
DEFAULT_MONOSPACE_FONT_SIZE,
},
terminal::{
local_tty::shell::is_valid_path_or_command_for_supported_shell,
session_settings::{
StartupShell, WorkingDirectoryConfig, WorkingDirectoryMode,
WorkingDirectoryPerSourceConfig,
},
},
};
use galaxy_core::ui::theme::{AnsiColors, TerminalColors, WarpTheme};
use galaxyui::fonts::FontInfo;
use galaxyui::keymap::Keystroke;
use galaxyui::platform::mac::utils::unicode_char_to_key;
use galaxyui::DisplayIdx;
use super::config::{
calculate_accent_color, Config, ConfigError, GlobalHotkey, ImportableSetting, ImportedFont,
MouseAndScrollReporting, OpacitySettings, ParseableConfig, QuakeModeWindow, SettingType,
ThemeError, ThemeType,
};
use crate::root_view::QuakeModePinPosition;
use crate::settings::import::config::HotkeyError;
use crate::settings::{ExtraMetaKeys, DEFAULT_MONOSPACE_FONT_NAME, DEFAULT_MONOSPACE_FONT_SIZE};
use crate::terminal::local_tty::shell::is_valid_path_or_command_for_supported_shell;
use crate::terminal::session_settings::{
StartupShell, WorkingDirectoryConfig, WorkingDirectoryMode, WorkingDirectoryPerSourceConfig,
};
extern crate plist;
@@ -4,13 +4,17 @@ use galaxyui::{fonts::FontInfo, keymap::Keystroke};
use pathfinder_color::ColorU;
use plist::{Dictionary, Value};
use virtual_fs::{Stub, VirtualFS};
use crate::settings::import::{
config::{GlobalHotkey, HotkeyError, ImportedFont, ParseableConfig, ThemeType},
iterm_parser::{default_dark_theme, default_light_theme, Flags, ITermKeystroke, ITermProfile},
};
use galaxy_core::ui::theme::{Fill, WarpTheme};
use galaxyui::fonts::FontInfo;
use galaxyui::keymap::Keystroke;
use super::{color_dictionary_to_coloru, ITermTheme, ITermThemeType};
use crate::settings::import::config::{
GlobalHotkey, HotkeyError, ImportedFont, ParseableConfig, ThemeType,
};
use crate::settings::import::iterm_parser::{
default_dark_theme, default_light_theme, Flags, ITermKeystroke, ITermProfile,
};
fn courier_new() -> Vec<FontInfo> {
vec![FontInfo {
+8 -12
View File
@@ -1,21 +1,17 @@
use std::collections::HashMap;
use crate::interval_timer::IntervalTimer;
use crate::settings::import::config::{Config, ConfigError};
use crate::{send_telemetry_from_ctx, TelemetryEvent};
use galaxy_core::features::FeatureFlag;
use galaxyui::Entity;
use galaxyui::ModelContext;
use galaxyui::SingletonEntity;
use serde::Serialize;
use strum::IntoEnumIterator;
use strum_macros::{EnumDiscriminants, EnumIter};
use galaxy_core::features::FeatureFlag;
use galaxyui::{Entity, ModelContext, SingletonEntity};
#[cfg(target_os = "macos")]
use super::config::HotkeyError;
use super::config::SettingType;
use super::config::ThemeType;
use super::config::{SettingType, ThemeType};
use crate::interval_timer::IntervalTimer;
use crate::settings::import::config::{Config, ConfigError};
use crate::{send_telemetry_from_ctx, TelemetryEvent};
#[derive(Clone, Copy, Debug, EnumDiscriminants, Eq, Hash, PartialEq)]
#[strum_discriminants(derive(EnumIter, Hash, Serialize))]
@@ -45,9 +41,9 @@ impl ImportedConfigModel {
#[cfg(feature = "local_fs")]
pub fn search_for_settings_to_import(&mut self, ctx: &mut ModelContext<Self>) {
use itertools::Itertools;
use std::sync::Arc;
use strum::IntoEnumIterator;
use itertools::Itertools;
self.started = true;
let loaded_system_fonts = galaxyui::fonts::Cache::handle(ctx)
+28 -37
View File
@@ -1,46 +1,37 @@
use galaxy_core::{settings::Setting, ui::appearance::Appearance};
use itertools::Itertools;
use galaxyui::{
elements::{
Border, Container, CornerRadius, Flex, Hoverable, MainAxisAlignment, MainAxisSize,
MouseStateHandle, ParentElement, Radius, Shrinkable, Text,
},
fonts::{Properties, Weight},
keymap::Keystroke,
ui_components::{
button::ButtonVariant,
components::{Coords, UiComponent, UiComponentStyles},
radio_buttons::{self, RadioButtonItem},
},
Element, Entity, ModelContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext,
use galaxy_core::settings::Setting;
use galaxy_core::ui::appearance::Appearance;
use galaxyui::elements::{
Border, Container, CornerRadius, Flex, Hoverable, MainAxisAlignment, MainAxisSize,
MouseStateHandle, ParentElement, Radius, Shrinkable, Text,
};
use galaxyui::ui_components::radio_buttons::RadioButtonStateHandle;
use crate::{
report_if_error, send_telemetry_from_ctx,
settings::{
import::{
config::{Config, ParsedTerminalSetting, SettingType},
model::{ImportedConfigModel, TerminalTypeAndProfile},
},
AppEditorSettings, CursorBlink, FontSettings, GlobalHotkeyMode, SelectionSettings,
ThemeSettings,
},
terminal::{
alt_screen_reporting::AltScreenReporting, keys_settings::KeysSettings,
session_settings::SessionSettings,
},
themes::theme::{CustomTheme, SelectedSystemThemes, ThemeKind},
ui_components::blended_colors,
user_config::{self, GalaxyConfig},
window_settings::WindowSettings,
GlobalResourceHandlesProvider, TelemetryEvent,
use galaxyui::fonts::{Properties, Weight};
use galaxyui::keymap::Keystroke;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::ui_components::radio_buttons::{self, RadioButtonItem, RadioButtonStateHandle};
use galaxyui::{
Element, Entity, ModelContext, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
};
use super::config::{QuakeModeWindow, ThemeType};
use crate::settings::import::config::{Config, ParsedTerminalSetting, SettingType};
use crate::settings::import::model::{ImportedConfigModel, TerminalTypeAndProfile};
use crate::settings::{
AppEditorSettings, CursorBlink, FontSettings, GlobalHotkeyMode, SelectionSettings,
ThemeSettings,
};
use crate::terminal::alt_screen_reporting::AltScreenReporting;
use crate::terminal::keys_settings::KeysSettings;
use crate::terminal::session_settings::SessionSettings;
use crate::themes::theme::{CustomTheme, SelectedSystemThemes, ThemeKind};
use crate::ui_components::blended_colors;
use crate::user_config::{self, WarpConfig};
use crate::window_settings::WindowSettings;
use crate::{
report_if_error, send_telemetry_from_ctx, GlobalResourceHandlesProvider, TelemetryEvent,
};
// UI does not scale, so we set a fixed size for all text.
const FONT_SIZE: f32 = 14.;
+46 -43
View File
@@ -1,46 +1,44 @@
use galaxy_core::features::FeatureFlag;
use galaxyui::{rendering::GPUPowerPreference, AppContext, SingletonEntity};
use galaxyui_extras::user_preferences;
use std::path::Path;
use settings::{Setting as _, SettingsManager};
use crate::{
ai::cloud_agent_settings::CloudAgentSettings,
appearance,
banner::BannerState,
drive::settings::WarpDriveSettings,
report_if_error,
resource_center::TipsCompleted,
search::command_search::settings::CommandSearchSettings,
terminal::{
alt_screen_reporting::AltScreenReporting,
general_settings::GeneralSettings,
keys_settings::KeysSettings,
ligature_settings::LigatureSettings,
safe_mode_settings::SafeModeSettings,
session_settings::{SessionSettings, SessionSettingsChangedEvent},
settings::TerminalSettings,
shared_session::settings::SharedSessionSettings,
warpify::settings::WarpifySettings,
BlockListSettings,
},
undo_close::UndoCloseSettings,
window_settings::WindowSettings,
workflows::aliases::WorkflowAliases,
workspace::tab_settings::TabSettings,
};
use galaxy_core::features::FeatureFlag;
use galaxy_core::semantic_selection::SemanticSelection;
use galaxyui::rendering::GPUPowerPreference;
use galaxyui::{AppContext, SingletonEntity};
use galaxyui_extras::user_preferences;
use super::app_icon::AppIconSettings;
use super::app_installation_detection::UserAppInstallDetectionSettings;
use super::cloud_preferences::CloudPreferencesSettings;
use super::initializer::SettingsInitializer;
use super::native_preference::NativePreferenceSettings;
use super::{
app_icon::AppIconSettings, app_installation_detection::UserAppInstallDetectionSettings,
cloud_preferences::CloudPreferencesSettings, initializer::SettingsInitializer,
native_preference::NativePreferenceSettings, AISettings, AccessibilitySettings,
AliasExpansionSettings, AppEditorSettings, BlockVisibilitySettings, ChangelogSettings,
CodeSettings, DebugSettings, EmacsBindingsSettings, FontSettings, FontSettingsChangedEvent,
GPUSettings, InputBoxType, InputModeSettings, InputSettings, PaneSettings,
SameLinePromptBlockSettings, ScrollSettings, SelectionSettings, SshSettings, ThemeSettings,
VimBannerSettings, WarpDrivePrivacySettings,
AISettings, AccessibilitySettings, AliasExpansionSettings, AppEditorSettings,
BlockVisibilitySettings, ChangelogSettings, CodeSettings, DebugSettings, EmacsBindingsSettings,
FontSettings, FontSettingsChangedEvent, GPUSettings, InputBoxType, InputModeSettings,
InputSettings, LocalControlSettings, PaneSettings, SameLinePromptBlockSettings, ScrollSettings,
SelectionSettings, SshSettings, ThemeSettings, VimBannerSettings, WarpDrivePrivacySettings,
};
use crate::ai::cloud_agent_settings::CloudAgentSettings;
use crate::banner::BannerState;
use crate::drive::settings::WarpDriveSettings;
use crate::resource_center::TipsCompleted;
use crate::search::command_search::settings::CommandSearchSettings;
use crate::terminal::alt_screen_reporting::AltScreenReporting;
use crate::terminal::general_settings::GeneralSettings;
use crate::terminal::keys_settings::KeysSettings;
use crate::terminal::ligature_settings::LigatureSettings;
use crate::terminal::safe_mode_settings::SafeModeSettings;
use crate::terminal::session_settings::{SessionSettings, SessionSettingsChangedEvent};
use crate::terminal::settings::TerminalSettings;
use crate::terminal::shared_session::settings::SharedSessionSettings;
use crate::terminal::warpify::settings::WarpifySettings;
use crate::terminal::BlockListSettings;
use crate::undo_close::UndoCloseSettings;
use crate::window_settings::WindowSettings;
use crate::workflows::aliases::WorkflowAliases;
use crate::workspace::tab_settings::TabSettings;
use crate::{appearance, report_if_error};
pub struct UserDefaultsOnStartup {
pub should_restore_session: bool,
@@ -98,8 +96,11 @@ pub fn register_all_settings(ctx: &mut AppContext) {
EmacsBindingsSettings::register(ctx);
SameLinePromptBlockSettings::register(ctx);
SemanticSelection::register(ctx);
if FeatureFlag::WarpControlCli.is_enabled() {
LocalControlSettings::register(ctx);
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
super::LinuxAppConfiguration::register(ctx);
#[cfg(feature = "local_fs")]
@@ -261,7 +262,7 @@ fn init_platform_native_preferences() -> user_preferences::Model {
cfg_if::cfg_if! {
if #[cfg(test)] {
Box::<user_preferences::in_memory::InMemoryPreferences>::default()
} else if #[cfg(any(target_os = "linux", feature = "integration_tests"))] {
} else if #[cfg(any(target_os = "linux", target_os = "freebsd", feature = "integration_tests"))] {
match user_preferences::file_backed::FileBackedUserPreferences::new(super::user_preferences_file_path()) {
Ok(prefs) => Box::new(prefs) as user_preferences::Model,
Err(err) => {
@@ -334,11 +335,14 @@ pub fn init_public_user_preferences() -> (user_preferences::Model, Option<user_p
/// 3. The migration-complete marker is absent from the native store
/// (handles the case where a user deletes `settings.toml` to reset).
fn needs_settings_file_migration(ctx: &AppContext) -> bool {
needs_settings_file_migration_for_path(ctx, &super::user_preferences_toml_file_path())
}
fn needs_settings_file_migration_for_path(ctx: &AppContext, settings_file_path: &Path) -> bool {
if !FeatureFlag::SettingsFile.is_enabled() {
return false;
}
if super::user_preferences_toml_file_path().exists() {
if settings_file_path.exists() {
return false;
}
@@ -359,7 +363,6 @@ fn needs_settings_file_migration(ctx: &AppContext) -> bool {
/// the in-memory setting, and writes to the TOML file with the correct
/// hierarchy, `serialize_for_file` transforms, and `max_table_depth`.
fn migrate_native_settings_to_settings_file(ctx: &mut AppContext) {
use galaxy_core::user_preferences::GetUserPreferences as _;
log::info!("Migrating public settings from native store to settings.toml");
+29 -41
View File
@@ -4,18 +4,16 @@ use galaxy_core::user_preferences::GetUserPreferences as _;
use galaxyui::SingletonEntity;
use galaxyui_extras::user_preferences;
use instant::Duration;
use settings::{
is_settings_file_enabled, set_settings_file_enabled, PrivatePreferences, PublicPreferences,
Setting, SettingsManager,
};
use settings::{PrivatePreferences, PublicPreferences, Setting, SettingsManager};
use settings_value::SettingsValue;
use crate::terminal::session_settings::{NotificationsMode, NotificationsSettings};
use galaxy_core::settings::macros::define_settings_group;
use galaxy_core::settings::{SupportedPlatforms, SyncToCloud};
use super::{
migrate_native_settings_to_settings_file, needs_settings_file_migration,
migrate_native_settings_to_settings_file, needs_settings_file_migration_for_path,
SETTINGS_FILE_MIGRATION_COMPLETE_KEY,
};
use crate::terminal::session_settings::{NotificationsMode, NotificationsSettings};
// A minimal settings group with one public and one private setting, used to
// verify that migration only copies public settings.
@@ -58,33 +56,12 @@ fn init_test_app(ctx: &mut galaxyui::AppContext) {
MigrationTestSettings::register(ctx);
}
struct SettingsFileEnabledGuard(bool);
impl SettingsFileEnabledGuard {
fn new(enabled: bool) -> Self {
let previous = is_settings_file_enabled();
set_settings_file_enabled(enabled);
Self(previous)
}
}
impl Drop for SettingsFileEnabledGuard {
fn drop(&mut self) {
set_settings_file_enabled(self.0);
}
}
// Only tests that toggle the process-global SettingsFile routing flag need to
// run serially.
#[test]
#[serial_test::serial]
fn test_migration_copies_public_settings_from_native_store() {
galaxyui::App::test((), |mut app| async move {
// Enable the settings file so `preferences_for_setting` routes
// public setting writes to the Model singleton (not the private store).
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let _settings_file_enabled = SettingsFileEnabledGuard::new(true);
app.update(init_test_app);
@@ -172,11 +149,9 @@ fn test_migration_writes_marker_to_native_store() {
}
#[test]
#[serial_test::serial]
fn test_migration_skips_settings_absent_from_native_store() {
galaxyui::App::test((), |mut app| async move {
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let _settings_file_enabled = SettingsFileEnabledGuard::new(true);
app.update(init_test_app);
// Don't seed anything in the native store — all settings are absent.
@@ -247,6 +222,8 @@ fn test_migration_handles_string_setting() {
fn test_migration_does_not_rerun_when_marker_present() {
galaxyui::App::test((), |mut app| async move {
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let temp_dir = tempfile::tempdir().unwrap();
let settings_file_path = temp_dir.path().join("settings.toml");
app.update(init_test_app);
@@ -261,7 +238,7 @@ fn test_migration_does_not_rerun_when_marker_present() {
// Before migration, the guard should allow migration.
app.read(|ctx| {
assert!(
needs_settings_file_migration(ctx),
needs_settings_file_migration_for_path(ctx, &settings_file_path),
"migration should be needed before first run"
);
});
@@ -274,7 +251,7 @@ fn test_migration_does_not_rerun_when_marker_present() {
// After migration, the marker should prevent re-migration.
app.read(|ctx| {
assert!(
!needs_settings_file_migration(ctx),
!needs_settings_file_migration_for_path(ctx, &settings_file_path),
"migration should not be needed after marker is written"
);
});
@@ -282,11 +259,28 @@ fn test_migration_does_not_rerun_when_marker_present() {
}
#[test]
#[serial_test::serial]
fn test_migration_not_needed_when_settings_file_exists() {
warpui::App::test((), |mut app| async move {
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let temp_dir = tempfile::tempdir().unwrap();
let settings_file_path = temp_dir.path().join("settings.toml");
std::fs::write(&settings_file_path, "").unwrap();
app.update(init_test_app);
app.read(|ctx| {
assert!(
!needs_settings_file_migration_for_path(ctx, &settings_file_path),
"migration should not be needed when settings.toml exists"
);
});
});
}
#[test]
fn test_migration_with_multiple_setting_types() {
galaxyui::App::test((), |mut app| async move {
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let _settings_file_enabled = SettingsFileEnabledGuard::new(true);
app.update(init_test_app);
@@ -379,8 +373,6 @@ fn test_migration_with_multiple_setting_types() {
// serde fallback is never reached and values are lost.
mod notifications_migration {
use galaxy_core::settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use galaxyui_extras::user_preferences;
use settings::{PrivatePreferences, PublicPreferences, SettingsManager};
use crate::terminal::session_settings::NotificationsSettings;
@@ -422,7 +414,7 @@ use notifications_migration::{
fn test_notifications_from_file_value_rejects_serde_format_enum() {
// serde serializes NotificationsMode::Enabled as "Enabled" (PascalCase),
// but from_file_value expects "enabled" (snake_case). When the field is
// present but unparseable, from_file_value should return None — not
// present but unparsable, from_file_value should return None — not
// silently fall back to the #[serde(default)] value (Unset).
let serde_json_value = serde_json::to_value(NotificationsSettings {
mode: NotificationsMode::Enabled,
@@ -462,11 +454,9 @@ fn test_notifications_from_file_value_rejects_serde_format_duration() {
// -- Migration integration tests: these demonstrate end-to-end data loss -----
#[test]
#[serial_test::serial]
fn test_migration_preserves_notifications_mode() {
galaxyui::App::test((), |mut app| async move {
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let _settings_file_enabled = SettingsFileEnabledGuard::new(true);
app.update(init_notifications_migration_test_app);
@@ -501,11 +491,9 @@ fn test_migration_preserves_notifications_mode() {
}
#[test]
#[serial_test::serial]
fn test_migration_preserves_custom_long_running_threshold() {
galaxyui::App::test((), |mut app| async move {
let _guard = FeatureFlag::SettingsFile.override_enabled(true);
let _settings_file_enabled = SettingsFileEnabledGuard::new(true);
app.update(init_notifications_migration_test_app);
+9 -9
View File
@@ -1,17 +1,17 @@
use std::sync::Arc;
use galaxy_core::{features::FeatureFlag, settings::Setting};
use galaxy_core::features::FeatureFlag;
use galaxy_core::settings::Setting;
use galaxyui::{Entity, ModelContext, SingletonEntity};
use crate::settings::{AISettings, FontSettings, ThinkingDisplayMode};
use crate::{
auth::auth_state::AuthState,
report_if_error,
settings::input::InputBoxType,
settings::{InputSettings, PrivacySettings, ThemeSettings},
terminal::session_settings::SessionSettings,
themes::theme::ThemeKind,
use crate::auth::auth_state::AuthState;
use crate::report_if_error;
use crate::settings::input::InputBoxType;
use crate::settings::{
AISettings, FontSettings, InputSettings, PrivacySettings, ThemeSettings, ThinkingDisplayMode,
};
use crate::terminal::session_settings::SessionSettings;
use crate::themes::theme::ThemeKind;
pub struct SettingsInitializer;
+6 -5
View File
@@ -1,12 +1,13 @@
use galaxyui::{AppContext, SingletonEntity};
use serde::{Deserialize, Serialize};
/// TODO: move alias_expansion setting into this group.
use settings::{define_settings_group, SupportedPlatforms, SyncToCloud};
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use settings::Setting as _;
/// TODO: move alias_expansion setting into this group.
use settings::{define_settings_group, RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
use galaxyui::{AppContext, SingletonEntity};
use crate::terminal::input::inline_menu::InlineMenuType;
use crate::terminal::session_settings::SessionSettings;
use settings::Setting as _;
pub const MAX_TIMES_TO_SHOW_AUTOSUGGESTION_HINT: i8 = 2;
+4 -2
View File
@@ -1,10 +1,12 @@
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
use crate::terminal::block_list_viewport::InputMode;
use settings::{macros::define_settings_group, Setting, SupportedPlatforms, SyncToCloud};
define_settings_group!(InputModeSettings, settings: [
input_mode: InputModeState {
type: InputMode,
// Note that for new users, we now overrride this default value in SettingsInitializer
// Note that for new users, we now override this default value in SettingsInitializer
// to set it to InputMode::Waterfall.
default: InputMode::PinnedToBottom,
supported_platforms: SupportedPlatforms::ALL,
+2 -1
View File
@@ -1,5 +1,6 @@
use settings::macros::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
use galaxyui::platform::linux;
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
define_settings_group!(LinuxAppConfiguration,
settings: [
+217
View File
@@ -0,0 +1,217 @@
//! Secure local setting that gates local control.
//!
//! This setting is local-only, kept out of the user-visible settings file, and
//! persisted through Warp's secure storage provider. It is the authoritative
//! enablement bit for local control.
use anyhow::Result;
use serde::{Deserialize, Serialize};
use settings::macros::define_settings_group;
use settings::{SecureSetting, Setting, SupportedPlatforms, SyncToCloud};
use galaxy_core::channel::{Channel, ChannelState};
use warpui::{AppContext, ModelContext};
use galaxyui_extras::secure_storage;
const LOCAL_CONTROL_MODE_STORAGE_KEY: &str = "LocalControlMode";
/// User-selected local-control availability.
#[derive(
Clone,
Copy,
Debug,
Default,
Deserialize,
Eq,
PartialEq,
schemars::JsonSchema,
Serialize,
settings_value::SettingsValue,
)]
#[schemars(
description = "Whether local control is enabled.",
rename_all = "snake_case"
)]
pub enum LocalControlMode {
#[default]
Disabled,
Enabled,
}
/// Channel-based default: local control is on for internal dogfood builds and
/// off for public channels, where users must opt in through Settings > Scripting.
fn default_mode_for_channel(channel: Channel) -> LocalControlMode {
if channel.is_dogfood() {
LocalControlMode::Enabled
} else {
LocalControlMode::Disabled
}
}
impl LocalControlMode {
pub const ALL: [Self; 2] = [Self::Disabled, Self::Enabled];
pub fn is_enabled(self) -> bool {
matches!(self, Self::Enabled)
}
pub fn as_dropdown_label(self) -> &'static str {
match self {
Self::Disabled => "Disabled",
Self::Enabled => "Enabled",
}
}
}
define_settings_group!(LocalControlSettings, settings: [
local_control_mode: LocalControlModeSetting,
]);
/// Setting wrapper for the authoritative local-control mode.
pub struct LocalControlModeSetting {
inner: LocalControlMode,
is_explicitly_set: bool,
}
impl LocalControlModeSetting {
fn emit_changed(
ctx: &mut ModelContext<LocalControlSettings>,
change_event_reason: settings::ChangeEventReason,
) {
ctx.emit(LocalControlSettingsChangedEvent::LocalControlModeSetting {
change_event_reason,
});
}
}
impl SecureSetting for LocalControlModeSetting {
fn write_secure_storage_value(
storage: &dyn secure_storage::SecureStorage,
key: &str,
value: &str,
) -> Result<(), secure_storage::Error> {
storage.write_value_with_owner_only_fallback(key, value)
}
}
impl Setting for LocalControlModeSetting {
type Group = LocalControlSettings;
type Value = LocalControlMode;
fn new(value: Option<Self::Value>) -> Self {
match value {
Some(value) => Self {
inner: value,
is_explicitly_set: true,
},
None => Self {
inner: Self::default_value(),
is_explicitly_set: false,
},
}
}
fn setting_name() -> &'static str {
"LocalControlModeSetting"
}
fn storage_key() -> &'static str {
LOCAL_CONTROL_MODE_STORAGE_KEY
}
fn supported_platforms() -> SupportedPlatforms {
SupportedPlatforms::DESKTOP
}
fn sync_to_cloud() -> SyncToCloud {
SyncToCloud::Never
}
fn is_private() -> bool {
true
}
fn value(&self) -> &Self::Value {
&self.inner
}
fn clear_value(&mut self, ctx: &mut ModelContext<Self::Group>) -> Result<()> {
Self::clear_from_secure_storage(ctx)?;
self.inner = self.validate(Self::default_value());
self.is_explicitly_set = false;
Self::emit_changed(ctx, settings::ChangeEventReason::Clear);
Ok(())
}
fn load_value(
&mut self,
new_value: Self::Value,
explicitly_set: bool,
ctx: &mut ModelContext<Self::Group>,
) -> Result<()> {
let validated = self.validate(new_value);
if self.value() != &validated || self.is_explicitly_set != explicitly_set {
self.inner = validated;
self.is_explicitly_set = explicitly_set;
Self::emit_changed(ctx, settings::ChangeEventReason::LocalChange);
}
Ok(())
}
fn set_value_from_cloud_sync(
&mut self,
_: Self::Value,
_: &mut ModelContext<Self::Group>,
) -> Result<()> {
Ok(())
}
fn set_value(
&mut self,
new_value: Self::Value,
ctx: &mut ModelContext<Self::Group>,
) -> Result<()> {
let changed_in_storage = Self::write_to_secure_storage(&new_value, ctx)?;
if self.value() != &new_value || changed_in_storage {
self.inner = self.validate(new_value);
self.is_explicitly_set = true;
Self::emit_changed(ctx, settings::ChangeEventReason::LocalChange);
}
Ok(())
}
fn default_value() -> Self::Value {
default_mode_for_channel(ChannelState::channel())
}
fn new_from_storage(ctx: &mut AppContext) -> Self {
Self::new(Self::read_from_secure_storage(ctx))
}
fn is_supported_on_current_platform(&self) -> bool {
SupportedPlatforms::DESKTOP.matches_current_platform()
}
fn is_value_explicitly_set(&self) -> bool {
self.is_explicitly_set
}
}
impl std::ops::Deref for LocalControlModeSetting {
type Target = LocalControlMode;
fn deref(&self) -> &Self::Target {
self.value()
}
}
impl LocalControlSettings {
pub fn mode(&self) -> LocalControlMode {
*self.local_control_mode
}
pub fn is_enabled(&self) -> bool {
self.mode().is_enabled()
}
}
#[cfg(test)]
#[path = "local_control_tests.rs"]
mod tests;
+241
View File
@@ -0,0 +1,241 @@
use std::collections::HashMap;
use std::sync::Mutex;
use settings::{PrivatePreferences, PublicPreferences, Setting as _, SettingsManager, SyncToCloud};
use galaxy_core::channel::{Channel, ChannelState};
use warpui::SingletonEntity as _;
use galaxyui_extras::secure_storage::{self, AppContextExt as _};
use galaxyui_extras::user_preferences;
use super::{
default_mode_for_channel, LocalControlMode, LocalControlModeSetting, LocalControlSettings,
};
#[derive(Default)]
struct InMemorySecureStorage {
values: Mutex<HashMap<String, String>>,
}
impl secure_storage::SecureStorage for InMemorySecureStorage {
fn write_value(&self, key: &str, value: &str) -> Result<(), secure_storage::Error> {
match self.values.lock() {
Ok(mut values) => {
values.insert(key.to_owned(), value.to_owned());
Ok(())
}
Err(err) => Err(secure_storage::Error::Unknown(anyhow::anyhow!(
err.to_string()
))),
}
}
fn read_value(&self, key: &str) -> Result<String, secure_storage::Error> {
match self.values.lock() {
Ok(values) => values
.get(key)
.cloned()
.ok_or(secure_storage::Error::NotFound),
Err(err) => Err(secure_storage::Error::Unknown(anyhow::anyhow!(
err.to_string()
))),
}
}
fn remove_value(&self, key: &str) -> Result<(), secure_storage::Error> {
match self.values.lock() {
Ok(mut values) => {
values.remove(key);
Ok(())
}
Err(err) => Err(secure_storage::Error::Unknown(anyhow::anyhow!(
err.to_string()
))),
}
}
}
fn default_settings() -> LocalControlSettings {
LocalControlSettings {
local_control_mode: LocalControlModeSetting::new(None),
}
}
#[test]
fn default_mode_is_enabled_only_on_dogfood_channels() {
assert_eq!(
default_mode_for_channel(Channel::Dev),
LocalControlMode::Enabled
);
assert_eq!(
default_mode_for_channel(Channel::Local),
LocalControlMode::Enabled
);
for channel in [
Channel::Stable,
Channel::Preview,
Channel::Oss,
Channel::Integration,
] {
assert_eq!(
default_mode_for_channel(channel),
LocalControlMode::Disabled,
"{channel} must require explicit opt-in"
);
}
}
#[test]
fn unset_mode_follows_channel_default() {
let settings = default_settings();
assert_eq!(LocalControlMode::default(), LocalControlMode::Disabled);
assert_eq!(
settings.mode(),
default_mode_for_channel(ChannelState::channel())
);
}
#[test]
fn mode_is_persisted_to_secure_storage() {
warpui::App::test((), |mut app| async move {
app.update(|ctx| {
ctx.add_singleton_model(|_| {
PublicPreferences::new(
Box::<user_preferences::in_memory::InMemoryPreferences>::default(),
)
});
ctx.add_singleton_model(|_| {
PrivatePreferences::new(
Box::<user_preferences::in_memory::InMemoryPreferences>::default(),
)
});
ctx.add_singleton_model(|_| SettingsManager::default());
ctx.add_singleton_model(|_| -> secure_storage::Model {
Box::<InMemorySecureStorage>::default()
});
LocalControlSettings::register(ctx);
});
app.update(|ctx| {
LocalControlSettings::handle(ctx).update(ctx, |settings, ctx| {
settings
.local_control_mode
.set_value(LocalControlMode::Enabled, ctx)
})
})
.expect("setting update should succeed");
app.read(|ctx| {
let stored = ctx
.secure_storage()
.read_value(LocalControlModeSetting::storage_key())
.expect("local-control mode should be stored securely");
let mode = serde_json::from_str::<LocalControlMode>(&stored)
.expect("stored local-control mode should deserialize");
assert_eq!(mode, LocalControlMode::Enabled);
let private_value = LocalControlModeSetting::preferences_for_setting(ctx)
.read_value(LocalControlModeSetting::storage_key())
.expect("private preferences should be readable");
assert!(private_value.is_none());
});
});
}
#[test]
fn mode_does_not_migrate_from_private_preferences() {
warpui::App::test((), |mut app| async move {
app.update(|ctx| {
ctx.add_singleton_model(|_| {
PublicPreferences::new(
Box::<user_preferences::in_memory::InMemoryPreferences>::default(),
)
});
ctx.add_singleton_model(|_| {
PrivatePreferences::new(
Box::<user_preferences::in_memory::InMemoryPreferences>::default(),
)
});
ctx.add_singleton_model(|_| SettingsManager::default());
ctx.add_singleton_model(|_| -> secure_storage::Model {
Box::<InMemorySecureStorage>::default()
});
LocalControlModeSetting::preferences_for_setting(ctx)
.write_value(
LocalControlModeSetting::storage_key(),
serde_json::to_string(&LocalControlMode::Enabled).expect("mode serializes"),
)
.expect("private preference is writable");
LocalControlSettings::register(ctx);
});
app.read(|ctx| {
assert_eq!(
LocalControlSettings::as_ref(ctx).mode(),
default_mode_for_channel(ChannelState::channel())
);
let private_value = LocalControlModeSetting::preferences_for_setting(ctx)
.read_value(LocalControlModeSetting::storage_key())
.expect("private preference is readable");
assert!(private_value.is_some());
});
});
}
#[test]
fn mode_is_private_and_never_cloud_synced() {
assert_eq!(LocalControlModeSetting::sync_to_cloud(), SyncToCloud::Never);
assert!(LocalControlModeSetting::is_private());
}
#[test]
fn cloud_sync_cannot_disable_local_control() {
warpui::App::test((), |mut app| async move {
app.update(|ctx| {
ctx.add_singleton_model(|_| {
PublicPreferences::new(
Box::<user_preferences::in_memory::InMemoryPreferences>::default(),
)
});
ctx.add_singleton_model(|_| {
PrivatePreferences::new(
Box::<user_preferences::in_memory::InMemoryPreferences>::default(),
)
});
ctx.add_singleton_model(|_| SettingsManager::default());
ctx.add_singleton_model(|_| -> secure_storage::Model {
Box::<InMemorySecureStorage>::default()
});
LocalControlSettings::register(ctx);
});
app.update(|ctx| {
LocalControlSettings::handle(ctx).update(ctx, |settings, ctx| {
settings
.local_control_mode
.set_value(LocalControlMode::Enabled, ctx)
})
})
.expect("local control should enable");
app.update(|ctx| {
LocalControlSettings::handle(ctx).update(ctx, |settings, ctx| {
settings
.local_control_mode
.set_value_from_cloud_sync(LocalControlMode::Disabled, ctx)
})
})
.expect("cloud sync update should be ignored without error");
app.read(|ctx| {
let settings = LocalControlSettings::as_ref(ctx);
assert_eq!(settings.mode(), LocalControlMode::Enabled);
let stored = ctx
.secure_storage()
.read_value(LocalControlModeSetting::storage_key())
.expect("explicitly enabled mode should remain stored securely");
let mode = serde_json::from_str::<LocalControlMode>(&stored)
.expect("stored local-control mode should deserialize");
assert_eq!(mode, LocalControlMode::Enabled);
});
});
}
+23 -17
View File
@@ -18,8 +18,9 @@ mod init;
pub mod initializer;
mod input;
mod input_mode;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux;
mod local_control;
pub mod macros;
pub mod manager;
pub mod native_preference;
@@ -53,8 +54,9 @@ pub use gpu::*;
pub use init::*;
pub use input::*;
pub use input_mode::*;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub use linux::*;
pub use local_control::*;
pub use native_preference::*;
pub use onboarding::*;
pub use pane::*;
@@ -115,23 +117,25 @@ impl SettingsFileError {
}
}
use crate::{
root_view::QuakeModePinPosition,
terminal::{BlockListSettings, BlockPadding},
themes::theme::{GalaxyTheme, ThemeKind},
user_config::GalaxyConfig,
};
use galaxy_core::features::FeatureFlag;
use galaxyui::{
elements::DEFAULT_UI_LINE_HEIGHT_RATIO, keymap::Keystroke, AppContext, DisplayIdx,
SingletonEntity,
};
use std::collections::HashMap;
use std::ops::Mul;
use std::path::PathBuf;
use lazy_static::lazy_static;
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::Vector2F;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use settings::Setting as _;
use std::{collections::HashMap, ops::Mul, path::PathBuf};
use galaxy_core::features::FeatureFlag;
use galaxyui::elements::DEFAULT_UI_LINE_HEIGHT_RATIO;
use galaxyui::keymap::Keystroke;
use galaxyui::{AppContext, DisplayIdx, SingletonEntity};
use crate::root_view::QuakeModePinPosition;
use crate::terminal::{BlockListSettings, BlockPadding};
use crate::themes::theme::{ThemeKind, WarpTheme};
use crate::user_config::WarpConfig;
// The following are user preferences keys.
pub const CHANGELOG_VERSIONS: &str = "ChangelogVersions";
@@ -214,6 +218,7 @@ pub enum CtrlTabBehavior {
#[default]
ActivatePrevNextTab,
CycleMostRecentSession,
CycleMostRecentTab,
}
impl CtrlTabBehavior {
@@ -221,6 +226,7 @@ impl CtrlTabBehavior {
match self {
Self::ActivatePrevNextTab => "Activate previous/next tab",
Self::CycleMostRecentSession => "Cycle most recent session",
Self::CycleMostRecentTab => "Cycle most recent tab",
}
}
}
@@ -254,7 +260,7 @@ pub struct Settings;
/// later allow users to have both quake mode and activation mode enabled simultaneously. If/when
/// that happens we'll remove this enum. These options are not modeled as a ternary option in the
/// serialized user-defaults, but as independent options.
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum GlobalHotkeyMode {
#[default]
Disabled,
@@ -488,7 +494,7 @@ impl Settings {
match res {
Ok(versions) => versions[&changelog_version].as_bool().unwrap_or(false),
Err(e) => {
log::warn!("Error deserializing changlog user default {e}");
log::warn!("Error deserializing changelog user default {e}");
false
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
#[derive(
Clone,
+20 -8
View File
@@ -1,3 +1,9 @@
use onboarding::slides::{AgentAutonomy, AgentDevelopmentSettings};
use onboarding::{SelectedSettings, SessionDefault, UICustomizationSettings};
use settings::Setting as _;
use galaxy_core::features::FeatureFlag;
use warpui::{AppContext, SingletonEntity as _};
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::{ActionPermission, WriteToPtyPermission};
use crate::drive::settings::WarpDriveSettings;
@@ -6,14 +12,17 @@ use crate::settings::ai::DefaultSessionMode;
use crate::settings::{AISettings, CodeSettings};
use crate::workspace::tab_settings::TabSettings;
use crate::workspaces::user_workspaces::UserWorkspaces;
use galaxy_core::features::FeatureFlag;
use galaxyui::{AppContext, SingletonEntity as _};
use onboarding::slides::{AgentAutonomy, AgentDevelopmentSettings};
use onboarding::{SelectedSettings, SessionDefault, UICustomizationSettings};
use settings::Setting as _;
/// Applies onboarding settings based on the user's selected mode.
pub fn apply_onboarding_settings(selected_settings: &SelectedSettings, app: &mut AppContext) {
///
/// `has_account` indicates whether the user has (or is creating) a real Warp
/// account. Warp's AI features run on a Warp account, so agent intent only
/// enables AI when `has_account` is true; skipping login leaves AI off.
pub fn apply_onboarding_settings(
selected_settings: &SelectedSettings,
has_account: bool,
app: &mut AppContext,
) {
let is_ai_enabled = match selected_settings {
SelectedSettings::AgentDrivenDevelopment {
agent_settings,
@@ -21,11 +30,14 @@ pub fn apply_onboarding_settings(selected_settings: &SelectedSettings, app: &mut
..
} => {
apply_agent_settings(agent_settings, app);
let is_ai_enabled = !agent_settings.disable_oz;
if let Some(ui) = ui_customization {
apply_ui_customization_settings(ui, true, app);
}
is_ai_enabled
// Agent intent means the user wants AI, but Warp's AI features run
// on a Warp account, so AI is only enabled once they have one.
// Skipping login leaves AI off even for agent intent (including the
// bring-your-own-agents `disable_oz` path).
has_account
}
SelectedSettings::Terminal {
ui_customization,
+65 -8
View File
@@ -3,6 +3,7 @@ use chrono::{DateTime, Utc};
use galaxyui::{App, SingletonEntity};
use onboarding::slides::{AgentAutonomy, AgentDevelopmentSettings, ProjectOnboardingSettings};
use onboarding::SelectedSettings;
use galaxy_core::features::FeatureFlag;
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::{
@@ -16,7 +17,7 @@ use crate::network::NetworkStatus;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::{ServerId, SyncId};
use crate::server::sync_queue::SyncQueue;
use crate::settings::{apply_onboarding_settings, PrivacySettings};
use crate::settings::{apply_onboarding_settings, AISettings, PrivacySettings};
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_workspaces::UserWorkspaces;
@@ -86,12 +87,12 @@ fn apply_onboarding_settings_preserves_existing_cloud_profile_on_existing_user_l
mcp_permissions: ActionPermission::AlwaysAllow,
..Default::default()
};
let server_object = ServerAIExecutionProfile {
id: cloud_sync_id,
model: CloudAIExecutionProfileModel::new(cloud_profile),
metadata: mock_server_metadata(cloud_uid),
permissions: ServerPermissions::mock_personal(),
};
let server_object = ServerAIExecutionProfile::new(
cloud_sync_id,
CloudAIExecutionProfileModel::new(cloud_profile),
mock_server_metadata(cloud_uid),
ServerPermissions::mock_personal(),
);
// Insert the existing user's cloud profile via the initial-load
// path (no per-object events) and emit `InitialLoadCompleted` so
@@ -127,7 +128,7 @@ fn apply_onboarding_settings_preserves_existing_cloud_profile_on_existing_user_l
};
app.update(|ctx| {
apply_onboarding_settings(&onboarding_settings, ctx);
apply_onboarding_settings(&onboarding_settings, true, ctx);
});
// Post-condition: the cloud profile retains its stored values.
@@ -168,3 +169,59 @@ fn apply_onboarding_settings_preserves_existing_cloud_profile_on_existing_user_l
});
})
}
/// Warp's AI features run on a Warp account. For third-party agent intent
/// (`disable_oz = true`), AI is therefore off when the user skips creating an
/// account and on once they have one.
#[test]
fn apply_onboarding_settings_gates_third_party_ai_on_account() {
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(SyncQueue::mock);
app.add_singleton_model(|_| NetworkStatus::new());
app.add_singleton_model(TeamTesterStatus::mock);
app.add_singleton_model(UpdateManager::mock);
app.add_singleton_model(CloudModel::mock);
app.add_singleton_model(|_| TemplatableMCPServerManager::default());
app.add_singleton_model(PrivacySettings::mock);
app.add_singleton_model(UserWorkspaces::default_mock);
app.add_singleton_model(|ctx| {
AIExecutionProfilesModel::new(&LaunchMode::new_for_unit_test(), ctx)
});
let onboarding_settings = SelectedSettings::AgentDrivenDevelopment {
agent_settings: AgentDevelopmentSettings {
selected_model_id: LLMId::from("auto"),
autonomy: None,
cli_agent_toolbar_enabled: true,
session_default: onboarding::SessionDefault::Agent,
disable_oz: true,
show_agent_notifications: true,
},
project_settings: ProjectOnboardingSettings::default(),
ui_customization: None,
};
// Skipping login (no account) leaves AI off, even for agent intent.
app.update(|ctx| {
apply_onboarding_settings(&onboarding_settings, false, ctx);
});
let ai_disabled = app.read(|ctx| !*AISettings::as_ref(ctx).is_any_ai_enabled);
assert!(
ai_disabled,
"skipping login must disable AI even for agent intent"
);
// Creating an account turns AI on, including for third-party agents.
app.update(|ctx| {
apply_onboarding_settings(&onboarding_settings, true, ctx);
});
let ai_enabled = app.read(|ctx| *AISettings::as_ref(ctx).is_any_ai_enabled);
assert!(
ai_enabled,
"creating an account must enable AI for third-party agent intent"
);
})
}
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
define_settings_group!(PaneSettings, settings: [
should_dim_inactive_panes: ShouldDimInactivePanes {
+50 -74
View File
@@ -7,7 +7,12 @@ use galaxy_core::report_if_error;
use galaxy_core::user_preferences::GetUserPreferences as _;
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity, UpdateModel};
use regex::Regex;
use serde::{Deserialize, Serialize};
use settings::macros::{define_settings_group, maybe_define_setting, register_settings_events};
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
use galaxy_graphql::mutations::update_user_settings::UpdateUserSettingsInput;
use super::cloud_preferences_syncer::CloudPreferencesSyncer;
use crate::ai::blocklist::telemetry_banner::should_collect_ai_ugc_telemetry;
use crate::auth::auth_state::AuthState;
use crate::auth::AuthStateProvider;
@@ -19,15 +24,6 @@ use crate::server::server_api::auth::MockAuthClient;
use crate::server::server_api::auth::{AuthClient, SyncedUserSettings};
use crate::server::server_api::ServerApiProvider;
use crate::terminal::safe_mode_settings::SafeModeSettings;
use settings::{
macros::{define_settings_group, maybe_define_setting, register_settings_events},
Setting, SupportedPlatforms, SyncToCloud,
};
use serde::{Deserialize, Serialize};
use super::cloud_preferences_syncer::CloudPreferencesSyncer;
use crate::workspaces::workspace::EnterpriseSecretRegex;
pub trait RegexDisplayInfo {
@@ -248,73 +244,46 @@ impl PrivacySettings {
let auth_state = AuthStateProvider::as_ref(ctx).get().clone();
let auth_client = ServerApiProvider::as_ref(ctx).get_auth_client();
let is_telemetry_enabled: bool = ctx
.private_user_preferences()
.read_value(TELEMETRY_ENABLED_DEFAULTS_KEY)
.unwrap_or_default()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(true);
let is_crash_reporting_enabled: bool = ctx
.private_user_preferences()
.read_value(CRASH_REPORTING_ENABLED_DEFAULTS_KEY)
.unwrap_or_default()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(true);
let is_cloud_conversation_storage_enabled: bool = ctx
.private_user_preferences()
.read_value(CLOUD_CONVERSATION_STORAGE_ENABLED_DEFAULTS_KEY)
.unwrap_or_default()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(true);
// Make sure the user-preferences stores match what's in memory.
// Needed for warp drive preferences to work and no harm in doing in general.
let _ = ctx.private_user_preferences().write_value(
TELEMETRY_ENABLED_DEFAULTS_KEY,
serde_json::to_string(&is_telemetry_enabled)
.expect("is_telemetry_enabled is a boolean."),
);
let _ = ctx.private_user_preferences().write_value(
CRASH_REPORTING_ENABLED_DEFAULTS_KEY,
serde_json::to_string(&is_crash_reporting_enabled)
.expect("is_crash_reporting_enabled is a boolean."),
);
let _ = ctx.private_user_preferences().write_value(
CLOUD_CONVERSATION_STORAGE_ENABLED_DEFAULTS_KEY,
serde_json::to_string(&is_cloud_conversation_storage_enabled)
.expect("is_cloud_conversation_storage_enabled is a boolean."),
);
// Initialize from `WarpDrivePrivacySettings`, which is the source of truth for these
// booleans.
let warp_drive_privacy = WarpDrivePrivacySettings::as_ref(ctx);
let is_telemetry_enabled = *warp_drive_privacy.is_telemetry_enabled.value();
let is_crash_reporting_enabled = *warp_drive_privacy.is_crash_reporting_enabled.value();
let is_cloud_conversation_storage_enabled = *warp_drive_privacy
.is_cloud_conversation_storage_enabled
.value();
// Listen for changes to the cloud model and update ourselves when they happen.
ctx.subscribe_to_model(&WarpDrivePrivacySettings::handle(ctx), |me, event, ctx| {
let privacy_settings = WarpDrivePrivacySettings::as_ref(ctx);
match event {
WarpDrivePrivacySettingsChangedEvent::IsTelemetryEnabled { .. } => {
me.set_is_telemetry_enabled(
*privacy_settings.is_telemetry_enabled.value(),
ctx,
);
ctx.subscribe_to_model(
&WarpDrivePrivacySettings::handle(ctx),
|me, _, event, ctx| {
let privacy_settings = WarpDrivePrivacySettings::as_ref(ctx);
match event {
WarpDrivePrivacySettingsChangedEvent::IsTelemetryEnabled { .. } => {
me.set_is_telemetry_enabled(
*privacy_settings.is_telemetry_enabled.value(),
ctx,
);
}
WarpDrivePrivacySettingsChangedEvent::IsCrashReportingEnabled { .. } => {
me.set_is_crash_reporting_enabled(
*privacy_settings.is_crash_reporting_enabled.value(),
ctx,
);
}
WarpDrivePrivacySettingsChangedEvent::IsCloudConversationStorageEnabled {
..
} => {
me.set_is_cloud_conversation_storage_enabled(
*privacy_settings
.is_cloud_conversation_storage_enabled
.value(),
ctx,
);
}
}
WarpDrivePrivacySettingsChangedEvent::IsCrashReportingEnabled { .. } => {
me.set_is_crash_reporting_enabled(
*privacy_settings.is_crash_reporting_enabled.value(),
ctx,
);
}
WarpDrivePrivacySettingsChangedEvent::IsCloudConversationStorageEnabled {
..
} => {
me.set_is_cloud_conversation_storage_enabled(
*privacy_settings
.is_cloud_conversation_storage_enabled
.value(),
ctx,
);
}
}
});
},
);
let user_secret_regex_list: CustomSecretRegexList =
CustomSecretRegexList::new_from_storage(ctx);
@@ -710,7 +679,14 @@ impl PrivacySettings {
let snapshot = self.get_snapshot(ctx);
let _ = ctx.spawn(
async move {
let result = auth_client.update_user_settings(snapshot).await;
let result = auth_client
.update_user_settings(UpdateUserSettingsInput {
telemetry_enabled: Some(snapshot.is_telemetry_enabled()),
crash_reporting_enabled: Some(snapshot.is_crash_reporting_enabled()),
cloud_conversation_storage_enabled: snapshot
.cloud_conversation_storage_enabled(),
})
.await;
if let Err(err) = result {
report_error!(
err.context("Failed to update server with local privacy settings.")
+2 -3
View File
@@ -1,7 +1,6 @@
use galaxy_core::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
use serde::{Deserialize, Serialize};
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
use galaxy_core::define_settings_group;
#[derive(
Debug,
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
define_settings_group!(ScrollSettings, settings: [
mouse_scroll_multiplier: MouseScrollMultiplier {
+5 -4
View File
@@ -1,8 +1,9 @@
use std::ops::Not;
use galaxyui::{clipboard::ClipboardContent, AppContext};
use settings::{macros::define_settings_group, Setting, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
use galaxyui::clipboard::ClipboardContent;
use galaxyui::AppContext;
define_settings_group!(SelectionSettings, settings: [
copy_on_select: CopyOnSelect {
@@ -89,7 +90,7 @@ impl SelectionSettings {
/// lack this separate clipboard, and so we map middle-click to the normal clipboard on those
/// platforms.
pub fn read_for_middle_click_paste(&self, ctx: &mut AppContext) -> Option<ClipboardContent> {
if cfg!(target_os = "linux") {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
return self.maybe_read_from_linux_selection_clipboard(ctx);
}
(self
+7 -6
View File
@@ -1,16 +1,17 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
define_settings_group!(SshSettings,
settings: [
enable_legacy_ssh_wrapper: EnableSshWrapper {
reuse_existing_control_master: ReuseExistingSshControlMaster {
type: bool,
default: true,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
storage_key: "EnableSSHWrapper",
toml_path: "warpify.ssh.enable_legacy_ssh_wrapper",
description: "Whether the legacy SSH wrapper is enabled for SSH sessions.",
storage_key: "ReuseExistingSshControlMaster",
toml_path: "warpify.ssh.reuse_existing_control_master",
description: "Whether the legacy SSH wrapper attaches to an existing SSH ControlMaster for the destination host instead of always creating its own.",
},
]
);
+17 -5
View File
@@ -1,7 +1,9 @@
use galaxyui::{platform::SystemTheme, AppContext};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud};
use galaxyui::platform::SystemTheme;
use galaxyui::AppContext;
use crate::themes::theme::{RespectSystemTheme, SelectedSystemThemes, ThemeKind};
use settings::{macros::define_settings_group, Setting, SupportedPlatforms, SyncToCloud};
// Settings group for themes related settings.
// Note that we store just the information needed to derive the current
@@ -47,9 +49,15 @@ define_settings_group!(ThemeSettings, settings: [
impl Theme {
fn current_value_is_syncable(&self) -> bool {
let current_value = self.value();
// Don't sync custom themes because they reference local files that aren't synced to the cloud.
!matches!(current_value, ThemeKind::Custom(_))
self.value().is_custom_theme_reference_syncable()
}
}
impl SystemThemes {
fn current_value_is_syncable(&self) -> bool {
let selected = self.value();
selected.light.is_custom_theme_reference_syncable()
&& selected.dark.is_custom_theme_reference_syncable()
}
}
@@ -79,3 +87,7 @@ pub fn derived_theme_kind(theme_settings: &ThemeSettings, system_theme: SystemTh
pub fn active_theme_kind(theme_settings: &ThemeSettings, app: &AppContext) -> ThemeKind {
derived_theme_kind(theme_settings, app.system_theme())
}
#[cfg(test)]
#[path = "theme_tests.rs"]
mod tests;
+68
View File
@@ -0,0 +1,68 @@
use std::path::PathBuf;
use super::*;
use crate::themes::theme::CustomTheme;
use crate::user_config;
fn custom(path: PathBuf) -> ThemeKind {
ThemeKind::Custom(CustomTheme::new("Custom".to_string(), path))
}
fn custom_base16(path: PathBuf) -> ThemeKind {
ThemeKind::CustomBase16(CustomTheme::new("Base16 Custom".to_string(), path))
}
#[test]
fn theme_kind_syncs_custom_theme_under_theme_root() {
let setting = Theme::new(Some(custom(user_config::themes_dir().join("custom.yml"))));
assert!(setting.current_value_is_syncable());
}
#[test]
fn theme_kind_does_not_sync_custom_theme_outside_theme_root() {
let setting = Theme::new(Some(custom(std::env::temp_dir().join("custom.yml"))));
assert!(!setting.current_value_is_syncable());
}
#[test]
fn theme_kind_syncs_custom_base16_theme_under_theme_root() {
let setting = Theme::new(Some(custom_base16(
user_config::themes_dir().join("base16/custom.yml"),
)));
assert!(setting.current_value_is_syncable());
}
#[test]
fn selected_system_themes_sync_when_custom_paths_are_under_theme_root() {
let setting = SystemThemes::new(Some(SelectedSystemThemes {
light: custom(user_config::themes_dir().join("light.yml")),
dark: custom_base16(user_config::themes_dir().join("dark.yml")),
}));
assert!(setting.current_value_is_syncable());
}
#[test]
fn selected_system_themes_do_not_sync_when_any_custom_path_is_outside_theme_root() {
let setting = SystemThemes::new(Some(SelectedSystemThemes {
light: custom(user_config::themes_dir().join("light.yml")),
dark: custom(std::env::temp_dir().join("dark.yml")),
}));
assert!(!setting.current_value_is_syncable());
}
#[test]
fn built_in_theme_settings_remain_syncable() {
let theme = Theme::new(Some(ThemeKind::Dark));
let system_themes = SystemThemes::new(Some(SelectedSystemThemes {
light: ThemeKind::Light,
dark: ThemeKind::Dark,
}));
assert!(theme.current_value_is_syncable());
assert!(system_themes.current_value_is_syncable());
}
+3 -2
View File
@@ -1,6 +1,7 @@
use crate::banner::BannerState;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
use galaxy_core::define_settings_group;
use settings::{SupportedPlatforms, SyncToCloud};
use crate::banner::BannerState;
// This isn't exactly a setting, but rather a record of a
// user action that should be persisted the same way we would a setting.