first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -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
@@ -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, "{}", ®ex.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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user