Polish provider setup and local-first settings

This commit is contained in:
2026-08-21 20:06:50 -05:00
parent be1dbb600a
commit 1f1d0737a9
21 changed files with 388 additions and 523 deletions
-1
View File
@@ -620,7 +620,6 @@ default = [
"supergrok", "supergrok",
"remote_code_review", "remote_code_review",
"git_operations_in_code_review", "git_operations_in_code_review",
"galaxy_control_cli",
] ]
# Enable this feature to automatically perform heap profiling. NOTE: This will # Enable this feature to automatically perform heap profiling. NOTE: This will
# substantially slow down program execution. # substantially slow down program execution.
@@ -0,0 +1,47 @@
{
"fill-specializations" : [
{
"value" : {
"linear-gradient" : [
"extended-srgb:0.00784,0.04706,0.12549,1.00000",
"extended-srgb:0.00000,0.01569,0.05490,1.00000"
]
}
},
{
"appearance" : "dark",
"value" : {
"linear-gradient" : [
"extended-srgb:0.00392,0.03137,0.09020,1.00000",
"extended-srgb:0.00000,0.00784,0.03137,1.00000"
]
}
}
],
"groups" : [
{
"layers" : [
{
"blend-mode" : "normal",
"glass" : false,
"image-name" : "Galaxy.png",
"name" : "Galaxy"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.25
},
"translucency" : {
"enabled" : false,
"value" : 0
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
+62 -48
View File
@@ -7,10 +7,13 @@
use aws_config::BehaviorVersion; use aws_config::BehaviorVersion;
use aws_sdk_bedrock::Client; use aws_sdk_bedrock::Client;
use aws_sdk_bedrockruntime::config::Region; use aws_sdk_bedrockruntime::config::Region;
use futures::{stream, StreamExt};
use super::client::{BedrockClientConfig, BedrockError}; use super::client::{BedrockClientConfig, BedrockError};
use crate::settings::ai::BedrockModelConfig; use crate::settings::ai::BedrockModelConfig;
const AVAILABILITY_CHECK_CONCURRENCY: usize = 8;
pub async fn discover_available_models( pub async fn discover_available_models(
config: BedrockClientConfig, config: BedrockClientConfig,
) -> Result<Vec<BedrockModelConfig>, String> { ) -> Result<Vec<BedrockModelConfig>, String> {
@@ -24,61 +27,72 @@ pub async fn discover_available_models(
.await .await
.map_err(|error| format!("Could not list AWS Bedrock foundation models: {error}"))?; .map_err(|error| format!("Could not list AWS Bedrock foundation models: {error}"))?;
let mut models = Vec::new(); // AWS exposes agreement/authorization state only through an individual
for summary in catalog.model_summaries() { // GetFoundationModelAvailability call. Keep using that control-plane API,
let model_id = summary.model_id(); // but bound the independent checks so a large catalog does not serialize
let availability = match client // startup discovery one model at a time.
.get_foundation_model_availability() let checks = catalog.model_summaries().iter().cloned().map(|summary| {
.model_id(model_id) let client = client.clone();
.send() async move {
.await let model_id = summary.model_id();
{ let availability = match client
Ok(availability) => availability, .get_foundation_model_availability()
Err(error) => { .model_id(model_id)
log::debug!( .send()
"[bedrock] Availability check failed for {model_id}; excluding model: {error}" .await
); {
continue; Ok(availability) => availability,
} Err(error) => {
}; log::debug!(
"[bedrock] Availability check failed for {model_id}; excluding model: {error}"
);
return None;
}
};
if !model_availability_is_usable( if !model_availability_is_usable(
availability
.agreement_availability()
.map(|agreement| agreement.status().as_str()),
availability.authorization_status().as_str(),
availability.entitlement_availability().as_str(),
availability.region_availability().as_str(),
) {
log::debug!(
"[bedrock] Excluding {model_id}: agreement={}, authorization={}, entitlement={}, region={}",
availability availability
.agreement_availability() .agreement_availability()
.map(|agreement| agreement.status().as_str()) .map(|agreement| agreement.status().as_str()),
.unwrap_or("MISSING"),
availability.authorization_status().as_str(), availability.authorization_status().as_str(),
availability.entitlement_availability().as_str(), availability.entitlement_availability().as_str(),
availability.region_availability().as_str(), availability.region_availability().as_str(),
); ) {
continue; log::debug!(
"[bedrock] Excluding {model_id}: agreement={}, authorization={}, entitlement={}, region={}",
availability
.agreement_availability()
.map(|agreement| agreement.status().as_str())
.unwrap_or("MISSING"),
availability.authorization_status().as_str(),
availability.entitlement_availability().as_str(),
availability.region_availability().as_str(),
);
return None;
}
let display_name = summary
.model_name()
.map(str::to_owned)
.unwrap_or_else(|| prettify_model_id(model_id));
let vision_supported = summary
.input_modalities()
.iter()
.any(|modality| modality.as_str() == "IMAGE");
Some(BedrockModelConfig {
model_id: model_id.to_owned(),
display_name,
vision_supported,
use_rig: false,
})
} }
});
let display_name = summary let mut models = stream::iter(checks)
.model_name() .buffer_unordered(AVAILABILITY_CHECK_CONCURRENCY)
.map(str::to_owned) .filter_map(futures::future::ready)
.unwrap_or_else(|| prettify_model_id(model_id)); .collect::<Vec<_>>()
let vision_supported = summary .await;
.input_modalities()
.iter()
.any(|modality| modality.as_str() == "IMAGE");
models.push(BedrockModelConfig {
model_id: model_id.to_owned(),
display_name,
vision_supported,
use_rig: false,
});
}
models.sort_by(|left, right| left.display_name.cmp(&right.display_name)); models.sort_by(|left, right| left.display_name.cmp(&right.display_name));
if models.is_empty() { if models.is_empty() {
@@ -166,26 +166,24 @@ impl ResponseStream {
// Check if this specific model has an OpenAI-compatible routing entry. // Check if this specific model has an OpenAI-compatible routing entry.
// This allows OpenAI/LiteLLM models to coexist with Bedrock models — // This allows OpenAI/LiteLLM models to coexist with Bedrock models —
// only models fetched from the OpenAI endpoint route through it. // only models fetched from the OpenAI endpoint route through it.
if *settings.openai_enabled.value() { let llm_prefs = LLMPreferences::as_ref(ctx);
let llm_prefs = LLMPreferences::as_ref(ctx); if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) {
if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) { return ProviderConfig::OpenAI(OpenAIClientConfig {
return ProviderConfig::OpenAI(OpenAIClientConfig { kind: client_config.kind,
kind: client_config.kind, base_url: client_config.base_url.clone(),
base_url: client_config.base_url.clone(), api_key: client_config.api_key.clone(),
api_key: client_config.api_key.clone(), project_id: client_config.project_id.clone(),
project_id: client_config.project_id.clone(), location: client_config.location.clone(),
location: client_config.location.clone(), model: client_config
model: client_config .model
.model .clone()
.clone() .or_else(|| Some(model_id.to_string())),
.or_else(|| Some(model_id.to_string())), reasoning_effort: client_config.reasoning_effort.clone(),
reasoning_effort: client_config.reasoning_effort.clone(), max_input_tokens: client_config.max_input_tokens,
max_input_tokens: client_config.max_input_tokens, max_output_tokens: client_config.max_output_tokens,
max_output_tokens: client_config.max_output_tokens, use_rig: client_config.use_rig,
use_rig: client_config.use_rig, supports_system_messages: client_config.supports_system_messages,
supports_system_messages: client_config.supports_system_messages, });
});
}
} }
// Fall back to Bedrock // Fall back to Bedrock
+10 -1
View File
@@ -46,8 +46,17 @@ pub(crate) struct ChatGPTAuthModel {
impl ChatGPTAuthModel { impl ChatGPTAuthModel {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let state = match load_or_import_auth_credentials() {
Ok(_) => ChatGPTAuthState::Connected,
Err(error) => {
log::debug!(
"[chatgpt/auth] No usable persisted ChatGPT credentials at startup: {error}"
);
ChatGPTAuthState::NotConnected
}
};
Self { Self {
state: ChatGPTAuthState::NotConnected, state,
pending_code_verifier: None, pending_code_verifier: None,
pending_state: None, pending_state: None,
} }
+18 -20
View File
@@ -160,26 +160,24 @@ impl CrosscheckReviewer {
let settings = AISettings::as_ref(ctx); let settings = AISettings::as_ref(ctx);
// Check if this model has an OpenAI-compatible routing entry // Check if this model has an OpenAI-compatible routing entry
if *settings.openai_enabled.value() { let llm_prefs = LLMPreferences::as_ref(ctx);
let llm_prefs = LLMPreferences::as_ref(ctx); if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) {
if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) { return ProviderConfig::OpenAI(OpenAIClientConfig {
return ProviderConfig::OpenAI(OpenAIClientConfig { kind: client_config.kind,
kind: client_config.kind, base_url: client_config.base_url.clone(),
base_url: client_config.base_url.clone(), api_key: client_config.api_key.clone(),
api_key: client_config.api_key.clone(), project_id: client_config.project_id.clone(),
project_id: client_config.project_id.clone(), location: client_config.location.clone(),
location: client_config.location.clone(), model: client_config
model: client_config .model
.model .clone()
.clone() .or_else(|| Some(model_id.to_string())),
.or_else(|| Some(model_id.to_string())), reasoning_effort: client_config.reasoning_effort.clone(),
reasoning_effort: client_config.reasoning_effort.clone(), max_input_tokens: client_config.max_input_tokens,
max_input_tokens: client_config.max_input_tokens, max_output_tokens: Some(REVIEWER_MAX_OUTPUT_TOKENS),
max_output_tokens: Some(REVIEWER_MAX_OUTPUT_TOKENS), use_rig: client_config.use_rig,
use_rig: client_config.use_rig, supports_system_messages: client_config.supports_system_messages,
supports_system_messages: client_config.supports_system_messages, });
});
}
} }
// Fall back to Bedrock via external config // Fall back to Bedrock via external config
+56 -18
View File
@@ -634,6 +634,27 @@ impl LLMPreferences {
} }
}); });
#[cfg(not(target_family = "wasm"))]
if ctx
.try_get_singleton_model_as_ref::<super::chatgpt_auth::ChatGPTAuthModel>()
.is_some()
{
ctx.subscribe_to_model(
&super::chatgpt_auth::ChatGPTAuthModel::handle(ctx),
|me, _, event, ctx| {
if matches!(
event,
super::chatgpt_auth::ChatGPTAuthModelEvent::StateChanged
) && matches!(
super::chatgpt_auth::ChatGPTAuthModel::as_ref(ctx).state(),
super::chatgpt_auth::ChatGPTAuthState::Connected
) {
me.refresh_chatgpt_subscription_models(ctx);
}
},
);
}
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _, event, ctx| { ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _, event, ctx| {
if let UserWorkspacesEvent::TeamsChanged = event { if let UserWorkspacesEvent::TeamsChanged = event {
me.sanitize_disabled_custom_model_preferences(ctx); me.sanitize_disabled_custom_model_preferences(ctx);
@@ -735,6 +756,10 @@ impl LLMPreferences {
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
{ {
// Make the persisted Bedrock catalog available immediately. Agreement
// revalidation stays in the background and replaces this cache when it
// completes, so startup never waits on the per-model AWS checks.
me.inject_bedrock_models(ctx);
me.refresh_bedrock_models(ctx); me.refresh_bedrock_models(ctx);
me.inject_openai_models(ctx); me.inject_openai_models(ctx);
me.ensure_default_model_present(); me.ensure_default_model_present();
@@ -978,7 +1003,7 @@ impl LLMPreferences {
let settings = AISettings::as_ref(ctx); let settings = AISettings::as_ref(ctx);
self.inject_acp_models(ctx); self.inject_acp_models(ctx);
if !*settings.openai_enabled.value() { if !settings.is_openai_provider_enabled() {
return; return;
} }
@@ -1163,7 +1188,7 @@ impl LLMPreferences {
}, },
)]), )]),
discount_percentage: None, discount_percentage: None,
context_window: openai_model_context_window(model), context_window: openai_model_context_window(model, provider_kind),
}; };
self.models_by_feature self.models_by_feature
.agent_mode .agent_mode
@@ -1447,7 +1472,7 @@ impl LLMPreferences {
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
pub fn fetch_openai_models_from_endpoint(&mut self, ctx: &mut ModelContext<Self>) { pub fn fetch_openai_models_from_endpoint(&mut self, ctx: &mut ModelContext<Self>) {
let settings = AISettings::as_ref(ctx); let settings = AISettings::as_ref(ctx);
if !*settings.openai_enabled.value() { if !settings.is_openai_provider_enabled() {
return; return;
} }
@@ -1509,7 +1534,7 @@ impl LLMPreferences {
ctx: &mut ModelContext<Self>, ctx: &mut ModelContext<Self>,
) { ) {
let settings = AISettings::as_ref(ctx); let settings = AISettings::as_ref(ctx);
if !*settings.openai_enabled.value() { if !settings.is_openai_provider_enabled() {
return; return;
} }
@@ -1716,7 +1741,7 @@ impl LLMPreferences {
} }
let settings = AISettings::as_ref(ctx); let settings = AISettings::as_ref(ctx);
if !*settings.openai_enabled.value() if !settings.is_openai_provider_enabled()
|| !settings.openai_providers.value().iter().any(|provider| { || !settings.openai_providers.value().iter().any(|provider| {
provider.enabled && provider.kind == OpenAIProviderKind::ChatGPTSubscription provider.enabled && provider.kind == OpenAIProviderKind::ChatGPTSubscription
}) })
@@ -2682,13 +2707,21 @@ fn openai_model_variant_id(model_id: &str, reasoning_effort: &str) -> String {
} }
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
fn openai_model_context_window(model: &OpenAIModelConfig) -> LLMContextWindow { fn openai_model_context_window(
let context_size = openai_model_context_size(model); model: &OpenAIModelConfig,
provider_kind: OpenAIProviderKind,
) -> LLMContextWindow {
let default_context_size = openai_model_context_size(model);
let max_context_size = if provider_kind == OpenAIProviderKind::ChatGPTSubscription {
model.context_size.max(default_context_size)
} else {
default_context_size
};
LLMContextWindow { LLMContextWindow {
is_configurable: false, is_configurable: max_context_size > default_context_size,
min: context_size, min: default_context_size,
max: context_size, max: max_context_size,
default_max: context_size, default_max: default_context_size,
} }
} }
@@ -2822,18 +2855,23 @@ fn chatgpt_models_from_codex_response(body: &serde_json::Value) -> Vec<OpenAIMod
return None; return None;
} }
let context_size = u32_from_json_any(model, &["context_window", "max_context_window"]) let default_context_size = u32_from_json_any(model, &["context_window"])
.or_else(|| u32_from_json_any(model, &["max_context_window"]))
.unwrap_or(DEFAULT_DISCOVERED_MODEL_CONTEXT_SIZE); .unwrap_or(DEFAULT_DISCOVERED_MODEL_CONTEXT_SIZE);
let max_context_size = u32_from_json_any(model, &["max_context_window"])
.unwrap_or(default_context_size)
.max(default_context_size);
let effective_context_percent = model["effective_context_window_percent"] let effective_context_percent = model["effective_context_window_percent"]
.as_u64() .as_u64()
.and_then(|value| u32::try_from(value).ok()) .and_then(|value| u32::try_from(value).ok())
.filter(|value| (1..=100).contains(value))
.unwrap_or(100); .unwrap_or(100);
let max_input_tokens = Some( let effective_context_size = |context_size: u32| {
context_size u32::try_from(u64::from(context_size) * u64::from(effective_context_percent) / 100)
.checked_mul(effective_context_percent) .unwrap_or(context_size)
.map(|tokens| tokens / 100) };
.unwrap_or(context_size), let max_input_tokens = Some(effective_context_size(default_context_size));
); let context_size = effective_context_size(max_context_size);
let vision_supported = model["input_modalities"] let vision_supported = model["input_modalities"]
.as_array() .as_array()
+19 -3
View File
@@ -760,7 +760,7 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
"display_name": "GPT-5.6-Sol", "display_name": "GPT-5.6-Sol",
"visibility": "list", "visibility": "list",
"context_window": 272000, "context_window": 272000,
"max_context_window": 272000, "max_context_window": 872000,
"effective_context_window_percent": 95, "effective_context_window_percent": 95,
"input_modalities": ["text", "image"], "input_modalities": ["text", "image"],
"supported_reasoning_levels": [ "supported_reasoning_levels": [
@@ -795,7 +795,7 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
assert_eq!(models[0].model_id, "gpt-5.6-sol"); assert_eq!(models[0].model_id, "gpt-5.6-sol");
assert_eq!(models[0].display_name, "GPT-5.6-Sol"); assert_eq!(models[0].display_name, "GPT-5.6-Sol");
assert!(models[0].vision_supported); assert!(models[0].vision_supported);
assert_eq!(models[0].context_size, 272_000); assert_eq!(models[0].context_size, 828_400);
assert_eq!(models[0].max_input_tokens, Some(258_400)); assert_eq!(models[0].max_input_tokens, Some(258_400));
assert_eq!(models[0].reasoning_efforts, ["low", "xhigh", "ultra"]); assert_eq!(models[0].reasoning_efforts, ["low", "xhigh", "ultra"]);
assert!(models[0].use_rig); assert!(models[0].use_rig);
@@ -804,8 +804,24 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
assert_eq!(models[1].model_id, "gpt-text-only"); assert_eq!(models[1].model_id, "gpt-text-only");
assert!(!models[1].vision_supported); assert!(!models[1].vision_supported);
assert_eq!(models[1].context_size, 128_000); assert_eq!(models[1].context_size, 115_200);
assert_eq!(models[1].max_input_tokens, Some(115_200)); assert_eq!(models[1].max_input_tokens, Some(115_200));
let configurable = openai_model_context_window(
&models[0],
crate::settings::OpenAIProviderKind::ChatGPTSubscription,
);
assert!(configurable.is_configurable);
assert_eq!(configurable.min, 258_400);
assert_eq!(configurable.default_max, 258_400);
assert_eq!(configurable.max, 828_400);
let fixed =
openai_model_context_window(&models[0], crate::settings::OpenAIProviderKind::LiteLLM);
assert!(!fixed.is_configurable);
assert_eq!(fixed.min, 258_400);
assert_eq!(fixed.default_max, 258_400);
assert_eq!(fixed.max, 258_400);
} }
#[test] #[test]
+38 -15
View File
@@ -1167,15 +1167,16 @@ impl settings_value::SettingsValue for AcpAgentSettings {}
// Nested ACP discovery data is intentionally persisted as one setting so refreshes are atomic. // Nested ACP discovery data is intentionally persisted as one setting so refreshes are atomic.
define_settings_group!(AISettings, settings: [ define_settings_group!(AISettings, settings: [
// If `false`, all AI features are disabled. // Legacy compatibility value. Effective AI availability is derived from configured,
// enabled providers rather than this historical global switch.
is_any_ai_enabled: IsAnyAIEnabled { is_any_ai_enabled: IsAnyAIEnabled {
type: bool, type: bool,
default: false, default: true,
supported_platforms: SupportedPlatforms::ALL, supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never, sync_to_cloud: SyncToCloud::Never,
private: false, private: false,
toml_path: "agents.warp_agent.is_any_ai_enabled", toml_path: "agents.warp_agent.is_any_ai_enabled",
description: "Controls whether all AI features are enabled.", description: "Legacy global AI enablement value retained for settings compatibility.",
}, },
// This field should not be referenced directly to lookup active AI enablement -- use the // This field should not be referenced directly to lookup active AI enablement -- use the
// `is_active_ai_enabled()` getter. // `is_active_ai_enabled()` getter.
@@ -1206,7 +1207,7 @@ define_settings_group!(AISettings, settings: [
// This is only used when `FeatureFlag::AgentView` is enabled. // This is only used when `FeatureFlag::AgentView` is enabled.
nld_in_terminal_enabled_internal: NLDInTerminalEnabled { nld_in_terminal_enabled_internal: NLDInTerminalEnabled {
type: bool, type: bool,
default: false, default: true,
supported_platforms: SupportedPlatforms::ALL, supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never, sync_to_cloud: SyncToCloud::Never,
private: false, private: false,
@@ -2338,11 +2339,35 @@ impl AISettings {
pub fn is_any_ai_enabled(&self, app: &AppContext) -> bool { pub fn is_any_ai_enabled(&self, app: &AppContext) -> bool {
// Galaxy does not require Warp authentication for AI. // Galaxy does not require Warp authentication for AI.
// AI is enabled only when the user hasn't explicitly disabled it, at least one local // Configuring and enabling a provider is the single source of truth for AI availability.
// runtime is enabled, and there's no org policy blocking it. self.has_enabled_ai_runtime() && !self.is_ai_disabled_due_to_remote_session_org_policy(app)
*self.is_any_ai_enabled }
&& self.has_enabled_ai_runtime()
&& !self.is_ai_disabled_due_to_remote_session_org_policy(app) /// Returns whether an OpenAI-compatible provider is enabled. The legacy global switch is
/// consulted only for the legacy single-endpoint configuration, which has no per-provider
/// enablement field.
pub fn is_openai_provider_enabled(&self) -> bool {
if !self.openai_providers.value().is_empty() {
return self
.openai_providers
.value()
.iter()
.any(|provider| provider.enabled);
}
*self.openai_enabled.value()
}
/// Returns whether an OpenAI-compatible provider is enabled and has at least one enabled model.
pub fn has_enabled_openai_provider(&self) -> bool {
if !self.openai_providers.value().is_empty() {
return self.openai_providers.value().iter().any(|provider| {
provider.enabled && provider.models.iter().any(|model| model.enabled)
});
}
self.is_openai_provider_enabled()
&& self.openai_models.value().iter().any(|model| model.enabled)
} }
pub(crate) fn configured_acp_providers(&self) -> Vec<AcpProviderConfig> { pub(crate) fn configured_acp_providers(&self) -> Vec<AcpProviderConfig> {
@@ -2409,11 +2434,11 @@ impl AISettings {
/// Returns whether Galaxy has a local model provider or agent runtime enabled. /// Returns whether Galaxy has a local model provider or agent runtime enabled.
pub fn has_enabled_ai_runtime(&self) -> bool { pub fn has_enabled_ai_runtime(&self) -> bool {
*self.bedrock_enabled.value() (*self.bedrock_enabled.value() && !self.bedrock_models.value().is_empty())
|| *self.openai_enabled.value() || self.has_enabled_openai_provider()
|| (cfg!(unix) || (cfg!(unix)
&& FeatureFlag::AgentClientProtocol.is_enabled() && FeatureFlag::AgentClientProtocol.is_enabled()
&& *self.acp_enabled.value()) && !self.enabled_acp_providers().is_empty())
} }
pub fn default_session_mode(&self, app: &AppContext) -> DefaultSessionMode { pub fn default_session_mode(&self, app: &AppContext) -> DefaultSessionMode {
@@ -2465,9 +2490,7 @@ impl AISettings {
} }
pub fn is_active_ai_enabled(&self, app: &galaxyui::AppContext) -> bool { pub fn is_active_ai_enabled(&self, app: &galaxyui::AppContext) -> bool {
self.is_any_ai_enabled(app) self.is_any_ai_enabled(app) && AppExecutionMode::as_ref(app).allows_active_ai()
&& *self.is_active_ai_enabled_internal
&& AppExecutionMode::as_ref(app).allows_active_ai()
} }
pub fn is_prompt_suggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { pub fn is_prompt_suggestions_enabled(&self, app: &galaxyui::AppContext) -> bool {
+15
View File
@@ -433,6 +433,21 @@ fn orchestration_is_enabled_when_ai_is_enabled() {
initialize_settings_for_tests(&mut app); initialize_settings_for_tests(&mut app);
add_ai_enablement_dependencies_for_test(&mut app); add_ai_enablement_dependencies_for_test(&mut app);
AISettings::handle(&app).update(&mut app, |settings, ctx| {
settings
.bedrock_models
.set_value(
vec![BedrockModelConfig {
model_id: "test-model".to_string(),
display_name: "Test model".to_string(),
vision_supported: false,
use_rig: false,
}],
ctx,
)
.expect("Bedrock models should update");
});
AISettings::handle(&app).read(&app, |settings, ctx| { AISettings::handle(&app).read(&app, |settings, ctx| {
assert!(settings.is_orchestration_enabled(ctx)); assert!(settings.is_orchestration_enabled(ctx));
}); });
-32
View File
@@ -86,38 +86,6 @@ impl SettingsInitializer {
} }
} }
// Migrate NLD settings when AgentView is enabled.
//
// Explicitly set `nld_in_terminal_enabled_internal` for all users if
// it has not previously been set.
//
// For existing users, when the old, previously-global autodetection setting
// (`ai_autodetection_enabled_internal`) true, set `nld_in_terminal_enabled_internal` to
// true. Otherwise, explicitly set to `false`.
//
// Any further user modification of the setting will be via explicit update, so it'll
// be exempt from this logic, which is effectively one-time upon first startup of a binary
// containing this logic.
//
// TODO(zachbai): Remove this approximately 6 weeks from 2/5/26.
if FeatureFlag::AgentView.is_enabled() {
AISettings::handle(ctx).update(ctx, |ai_settings, ctx| {
if ai_settings
.nld_in_terminal_enabled_internal
.is_value_explicitly_set()
{
return;
}
let is_existing_user = auth_state.is_onboarded() == Some(true);
let was_global_autodetection_enabled_for_existing_user =
*ai_settings.ai_autodetection_enabled_internal && is_existing_user;
report_if_error!(ai_settings
.nld_in_terminal_enabled_internal
.set_value(was_global_autodetection_enabled_for_existing_user, ctx));
});
}
// Migrate the old `KeepThinkingExpanded` bool setting to the new // Migrate the old `KeepThinkingExpanded` bool setting to the new
// `ThinkingDisplayMode` enum setting. // `ThinkingDisplayMode` enum setting.
// //
+1 -2
View File
@@ -14,8 +14,7 @@ use crate::themes::theme::{RespectSystemTheme, SelectedSystemThemes, ThemeKind};
define_settings_group!(ThemeSettings, settings: [ define_settings_group!(ThemeSettings, settings: [
theme_kind: Theme { theme_kind: Theme {
type: ThemeKind, type: ThemeKind,
// Note that for new users, we now override this default value in SettingsInitializer // New installations start with Galaxy's built-in brand theme.
// to set the default theme to Phenomenon.
default: ThemeKind::default(), default: ThemeKind::default(),
supported_platforms: SupportedPlatforms::ALL, supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never, sync_to_cloud: SyncToCloud::Never,
+50 -7
View File
@@ -1,6 +1,11 @@
use galaxyui::elements::{
Align, CacheOption, ConstrainedBox, Container, CrossAxisAlignment, Element, Flex,
FormattedTextElement, Image, Padding, ParentElement, Text,
};
use galaxyui::fonts::Weight;
use galaxyui::text_layout::TextAlignment;
use galaxyui::{AppContext, Entity, SingletonEntity, View, ViewContext, ViewHandle}; use galaxyui::{AppContext, Entity, SingletonEntity, View, ViewContext, ViewHandle};
use warpui::assets::asset_cache::AssetSource; use warpui::assets::asset_cache::AssetSource;
use warpui::elements::{Align, CacheOption, ConstrainedBox, Element, Image};
use super::settings_page::{ use super::settings_page::{
MatchData, PageType, SettingsPageEvent, SettingsPageMeta, SettingsPageViewHandle, MatchData, PageType, SettingsPageEvent, SettingsPageMeta, SettingsPageViewHandle,
@@ -48,7 +53,7 @@ impl SettingsWidget for AboutPageWidget {
fn render( fn render(
&self, &self,
_view: &AboutPageView, _view: &AboutPageView,
_appearance: &Appearance, appearance: &Appearance,
app: &AppContext, app: &AppContext,
) -> Box<dyn Element> { ) -> Box<dyn Element> {
let icon_file = let icon_file =
@@ -62,16 +67,54 @@ impl SettingsWidget for AboutPageWidget {
_ => "bundled/png/galaxy.png", _ => "bundled/png/galaxy.png",
}; };
let icon = ConstrainedBox::new(
Image::new(
AssetSource::Bundled { path: image_path },
CacheOption::BySize,
)
.finish(),
)
.with_max_height(144.)
.with_max_width(144.)
.finish();
let title = FormattedTextElement::from_str("Galaxy", appearance.ui_font_family(), 28.)
.with_weight(Weight::Bold)
.with_color(appearance.theme().active_ui_text_color().into_solid())
.with_alignment(TextAlignment::Center)
.finish();
let version = Text::new(
format!("Version {}", env!("CARGO_PKG_VERSION")),
appearance.ui_font_family(),
12.,
)
.with_color(appearance.theme().nonactive_ui_text_color().into())
.finish();
let description = FormattedTextElement::from_str(
"Galaxy is a local-first terminal and AI workspace built for developers. Your settings, terminal data, conversations, and Galaxy Drive content stay on this machine in your Galaxy directory. Galaxy sends request data only to the AI providers you configure.",
appearance.ui_font_family(),
14.,
)
.with_color(appearance.theme().nonactive_ui_text_color().into_solid())
.with_alignment(TextAlignment::Center)
.with_line_height_ratio(1.35)
.finish();
Align::new( Align::new(
ConstrainedBox::new( ConstrainedBox::new(
Image::new( Container::new(
AssetSource::Bundled { path: image_path }, Flex::column()
CacheOption::BySize, .with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_spacing(12.)
.with_child(icon)
.with_child(title)
.with_child(version)
.with_child(description)
.finish(),
) )
.with_padding(Padding::uniform(24.))
.finish(), .finish(),
) )
.with_max_height(144.) .with_max_width(560.)
.with_max_width(144.)
.finish(), .finish(),
) )
.finish() .finish()
+6 -147
View File
@@ -101,7 +101,7 @@ use crate::settings::{
BedrockEnabled, BedrockModelConfig, CodeSettings, CodebaseContextEnabled, CrosscheckEnabled, BedrockEnabled, BedrockModelConfig, CodeSettings, CodebaseContextEnabled, CrosscheckEnabled,
FileBasedMcpEnabled, GitOperationsAutogenEnabled, IncludeAgentCommandsInHistory, InputSettings, FileBasedMcpEnabled, GitOperationsAutogenEnabled, IncludeAgentCommandsInHistory, InputSettings,
IntelligentAutosuggestionsEnabled, LongRunningCommandSubmissionMode, MemoryEnabled, IntelligentAutosuggestionsEnabled, LongRunningCommandSubmissionMode, MemoryEnabled,
NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, OpenAIEnabled, OpenAIModelConfig, NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, OpenAIModelConfig,
OpenAIProviderConfig, OrchestrationMessageDisplayMode, PromptSubmissionMode, OpenAIProviderConfig, OrchestrationMessageDisplayMode, PromptSubmissionMode,
RuleSuggestionsEnabled, SharedBlockTitleGenerationEnabled, ShouldRenderCLIAgentToolbar, RuleSuggestionsEnabled, SharedBlockTitleGenerationEnabled, ShouldRenderCLIAgentToolbar,
ShouldRenderUseAgentToolbarForUserCommands, ShowAgentTips, ShowConversationHistory, ShouldRenderUseAgentToolbarForUserCommands, ShowAgentTips, ShowConversationHistory,
@@ -129,8 +129,6 @@ use crate::{
}; };
const CONTENT_FONT_SIZE: f32 = 12.; const CONTENT_FONT_SIZE: f32 = 12.;
const PRIMARY_HEADER_FONT_SIZE: f32 = 24.;
const AI_SETTINGS_DROPDOWN_WIDTH: f32 = 250.; const AI_SETTINGS_DROPDOWN_WIDTH: f32 = 250.;
const AI_SETTINGS_DROPDOWN_MAX_HEIGHT: f32 = 250.; const AI_SETTINGS_DROPDOWN_MAX_HEIGHT: f32 = 250.;
const CONTEXT_WINDOW_SLIDER_WIDTH: f32 = 220.; const CONTEXT_WINDOW_SLIDER_WIDTH: f32 = 220.;
@@ -151,7 +149,7 @@ const WISPR_FLOW_URL: &str = "https://wisprflow.ai/";
/// When `None`, the page shows all widgets (legacy/full view). /// When `None`, the page shows all widgets (legacy/full view).
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AISubpage { pub enum AISubpage {
/// The main Galaxy Agent page: global AI toggle + Active AI + Input + Other sections. /// The main Galaxy Agent page: suggestions, input, and other agent settings.
WarpAgent, WarpAgent,
/// Agent profiles and permissions. /// Agent profiles and permissions.
Profiles, Profiles,
@@ -217,28 +215,6 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
app, app,
); );
ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(
vec![ToggleSettingActionPair::new(
"AI",
builder(SettingsAction::AI(AISettingsPageAction::ToggleGlobalAI)),
context,
flags::IS_ANY_AI_ENABLED,
)
.with_group(bindings::BindingGroup::WarpAi)],
app,
);
ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(
vec![ToggleSettingActionPair::new(
"Active AI",
builder(SettingsAction::AI(AISettingsPageAction::ToggleActiveAI)),
&(context.clone() & id!(flags::IS_ANY_AI_ENABLED)),
flags::IS_ACTIVE_AI_ENABLED,
)
.with_group(bindings::BindingGroup::WarpAi)],
app,
);
ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(
vec![ToggleSettingActionPair::new( vec![ToggleSettingActionPair::new(
if FeatureFlag::AgentView.is_enabled() { if FeatureFlag::AgentView.is_enabled() {
@@ -2115,7 +2091,6 @@ impl AISettingsPageView {
match subpage { match subpage {
None => { None => {
// Full page: all widgets (legacy behavior) // Full page: all widgets (legacy behavior)
widgets.push(Box::new(GlobalAIWidget::default()));
if ai_settings if ai_settings
.intelligent_autosuggestions_enabled_internal .intelligent_autosuggestions_enabled_internal
.is_supported_on_current_platform() .is_supported_on_current_platform()
@@ -2158,8 +2133,7 @@ impl AISettingsPageView {
widgets.push(Box::new(OtherAIWidget::default())); widgets.push(Box::new(OtherAIWidget::default()));
} }
Some(AISubpage::WarpAgent) => { Some(AISubpage::WarpAgent) => {
// Galaxy Agent page: global toggle + Active AI + Input + Other // Galaxy Agent page: suggestions, input, and other agent settings.
widgets.push(Box::new(GlobalAIWidget::default()));
if ai_settings if ai_settings
.intelligent_autosuggestions_enabled_internal .intelligent_autosuggestions_enabled_internal
.is_supported_on_current_platform() .is_supported_on_current_platform()
@@ -4127,85 +4101,8 @@ fn render_ai_list(
.finish() .finish()
} }
#[derive(Default)]
struct GlobalAIWidget {
switch_state: SwitchStateHandle,
}
impl SettingsWidget for GlobalAIWidget {
type View = AISettingsPageView;
fn search_terms(&self) -> &str {
"galaxy agent global ai a.i. active next command prompt code diffs suggestion suggested suggestions \
agent mode natural language detection input hint"
}
fn render(
&self,
_view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let ui_builder = appearance.ui_builder();
let is_ai_disabled_due_to_remote_session_org_policy =
AISettings::as_ref(app).is_ai_disabled_due_to_remote_session_org_policy(app);
let mut row = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Text::new_inline(
"Galaxy Agent",
appearance.ui_font_family(),
PRIMARY_HEADER_FONT_SIZE,
)
.with_style(Properties::default().weight(Weight::Bold))
.with_color(appearance.theme().active_ui_text_color().into())
.finish(),
);
if is_ai_disabled_due_to_remote_session_org_policy {
row.add_child(
ConstrainedBox::new(
Container::new(
Text::new("Your organization disallows AI when the active pane contains content from a remote session", appearance.ui_font_family(), 12.)
.with_color(appearance.theme().ui_warning_color())
.finish()
)
.with_padding_left(8.)
.with_padding_right(8.)
.finish()
)
.with_max_width(400.)
.finish()
);
}
row.add_child(
Container::new(
ui_builder
.switch(self.switch_state.clone())
.check(AISettings::as_ref(app).is_any_ai_enabled(app))
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(AISettingsPageAction::ToggleGlobalAI);
})
.finish(),
)
.with_padding_right(TOGGLE_BUTTON_RIGHT_PADDING)
.finish(),
);
Container::new(row.finish())
.with_padding_bottom(15.)
.finish()
}
}
#[derive(Default)] #[derive(Default)]
struct ActiveAIWidget { struct ActiveAIWidget {
active_ai_toggle: SwitchStateHandle,
intelligent_autosuggestions_toggle: SwitchStateHandle, intelligent_autosuggestions_toggle: SwitchStateHandle,
prompt_suggestions_toggle: SwitchStateHandle, prompt_suggestions_toggle: SwitchStateHandle,
code_suggestions_toggle: SwitchStateHandle, code_suggestions_toggle: SwitchStateHandle,
@@ -4452,38 +4349,12 @@ impl SettingsWidget for ActiveAIWidget {
appearance: &Appearance, appearance: &Appearance,
app: &AppContext, app: &AppContext,
) -> Box<dyn Element> { ) -> Box<dyn Element> {
let ai_settings = AISettings::as_ref(app);
let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app);
let mut column = Flex::column() let mut column = Flex::column()
.with_child(render_separator(appearance)) .with_child(render_separator(appearance))
.with_child( .with_child(
Container::new( Container::new(build_sub_header(appearance, "AI suggestions", None).finish())
Flex::row() .with_padding_bottom(HEADER_PADDING)
.with_main_axis_size(MainAxisSize::Max) .finish(),
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_child(
build_sub_header(
appearance,
"Active AI",
Some(styles::header_font_color(is_any_ai_enabled, app)),
)
.finish(),
)
.with_child(
Container::new(render_ai_feature_switch(
self.active_ai_toggle.clone(),
*ai_settings.is_active_ai_enabled_internal,
is_any_ai_enabled,
AISettingsPageAction::ToggleActiveAI,
app,
))
.with_padding_right(TOGGLE_BUTTON_RIGHT_PADDING)
.finish(),
)
.finish(),
)
.with_padding_bottom(HEADER_PADDING)
.finish(),
); );
if self.is_next_command_toggleable(app) { if self.is_next_command_toggleable(app) {
@@ -7230,7 +7101,6 @@ struct AcpProviderCardState {
struct ProviderSettingsWidget { struct ProviderSettingsWidget {
provider_type: ProviderSetupProviderType, provider_type: ProviderSetupProviderType,
enabled_toggle: SwitchStateHandle,
bedrock_enabled_toggle: SwitchStateHandle, bedrock_enabled_toggle: SwitchStateHandle,
add_openai_provider_button: ViewHandle<ActionButton>, add_openai_provider_button: ViewHandle<ActionButton>,
add_litellm_provider_button: ViewHandle<ActionButton>, add_litellm_provider_button: ViewHandle<ActionButton>,
@@ -7342,7 +7212,6 @@ impl ProviderSettingsWidget {
}); });
Self { Self {
provider_type, provider_type,
enabled_toggle: SwitchStateHandle::default(),
bedrock_enabled_toggle: SwitchStateHandle::default(), bedrock_enabled_toggle: SwitchStateHandle::default(),
add_openai_provider_button, add_openai_provider_button,
add_litellm_provider_button, add_litellm_provider_button,
@@ -7990,16 +7859,6 @@ impl SettingsWidget for ProviderSettingsWidget {
ProviderSetupProviderType::OpenAI ProviderSetupProviderType::OpenAI
| ProviderSetupProviderType::LiteLLM | ProviderSetupProviderType::LiteLLM
| ProviderSetupProviderType::ChatGPTSubscription => { | ProviderSetupProviderType::ChatGPTSubscription => {
column.add_child(render_ai_setting_toggle::<OpenAIEnabled>(
"Enable direct providers",
AISettingsPageAction::ToggleOpenAIEnabled,
*settings.openai_enabled.value(),
true,
self.enabled_toggle.clone(),
&RefCell::new(HashMap::new()),
app,
));
if is_setup_visible { if is_setup_visible {
column.add_child(Self::render_inline_setup(appearance, view)); column.add_child(Self::render_inline_setup(appearance, view));
} }
+2 -28
View File
@@ -104,8 +104,8 @@ pub use code_page::CodeSettingsPageView;
pub use features_page::FeaturesPageAction; pub use features_page::FeaturesPageAction;
pub use privacy_page::PrivacyPageAction; pub use privacy_page::PrivacyPageAction;
pub use settings_page::{ pub use settings_page::{
render_body_item_label, render_info_icon, render_input_list, render_separator, AdditionalInfo, render_body_item_label, render_input_list, render_separator, AdditionalInfo, InputListItem,
InputListItem, LocalOnlyIconState, ToggleState, LocalOnlyIconState, ToggleState,
}; };
pub use teams_page::{OpenTeamsSettingsModalArgs, TeamsInviteOption}; pub use teams_page::{OpenTeamsSettingsModalArgs, TeamsInviteOption};
@@ -1216,11 +1216,6 @@ impl SettingsView {
let warp_drive_page_handle = let warp_drive_page_handle =
ctx.add_typed_action_view(warp_drive_page::WarpDriveSettingsPageView::new); ctx.add_typed_action_view(warp_drive_page::WarpDriveSettingsPageView::new);
let platform_page_handle = ctx.add_typed_action_view(platform_page::PlatformPageView::new);
ctx.subscribe_to_view(&platform_page_handle, |me, _, event, ctx| {
me.handle_platform_page_event(event, ctx);
});
// MCP Servers page // MCP Servers page
let mcp_servers_page_handle = ctx.add_typed_action_view(MCPServersSettingsPageView::new); let mcp_servers_page_handle = ctx.add_typed_action_view(MCPServersSettingsPageView::new);
ctx.subscribe_to_view(&mcp_servers_page_handle, |me, _, event, ctx| { ctx.subscribe_to_view(&mcp_servers_page_handle, |me, _, event, ctx| {
@@ -1261,7 +1256,6 @@ impl SettingsView {
SettingsPage::new(appearance_page_handle), SettingsPage::new(appearance_page_handle),
SettingsPage::new(features_page_handle), SettingsPage::new(features_page_handle),
SettingsPage::new(keybindings_handle), SettingsPage::new(keybindings_handle),
SettingsPage::new(platform_page_handle),
SettingsPage::new(warpify_page_handle), SettingsPage::new(warpify_page_handle),
SettingsPage::new(warp_drive_page_handle), SettingsPage::new(warp_drive_page_handle),
]; ];
@@ -1730,9 +1724,6 @@ impl SettingsView {
ctx: &mut ViewContext<Self>, ctx: &mut ViewContext<Self>,
) { ) {
match event { match event {
PrivacyPageViewEvent::LaunchNetworkLogging => {
ctx.emit(SettingsViewEvent::LaunchNetworkLogging);
}
PrivacyPageViewEvent::ShowAddRegexModal => { PrivacyPageViewEvent::ShowAddRegexModal => {
// Modal rendering is handled in get_modal_content_for_page // Modal rendering is handled in get_modal_content_for_page
ctx.notify(); ctx.notify();
@@ -1744,23 +1735,6 @@ impl SettingsView {
} }
} }
fn handle_platform_page_event(
&mut self,
event: &platform_page::PlatformPageViewEvent,
ctx: &mut ViewContext<Self>,
) {
match event {
platform_page::PlatformPageViewEvent::ShowCreateApiKeyModal => {
// Modal rendering is handled in get_modal_content_for_page
ctx.notify();
}
platform_page::PlatformPageViewEvent::HideCreateApiKeyModal => {
// Modal rendering is handled in get_modal_content_for_page
ctx.notify();
}
}
}
fn handle_mcp_servers_page_event( fn handle_mcp_servers_page_event(
&mut self, &mut self,
event: &MCPServersSettingsPageEvent, event: &MCPServersSettingsPageEvent,
+20 -98
View File
@@ -4,13 +4,12 @@ use std::collections::{HashMap, HashSet};
use std::sync::LazyLock; use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::features::FeatureFlag; use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::theme::color::internal_colors; use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::GalaxyTheme; use galaxy_core::ui::theme::GalaxyTheme;
use galaxyui::elements::{ use galaxyui::elements::{
Align, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty,
Empty, Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect, Shrinkable, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect, Shrinkable,
Stack, Text, Stack, Text,
}; };
@@ -79,9 +78,7 @@ const TELEMETRY_DESCRIPTION_OLD: &str =
const TELEMETRY_TITLE: &str = "Help improve Galaxy"; const TELEMETRY_TITLE: &str = "Help improve Galaxy";
const TELEMETRY_DESCRIPTION: &str = const TELEMETRY_DESCRIPTION: &str =
"App analytics help us make the product better for you. We may collect \ "App analytics help us make the product better for you. We may collect \
certain console interactions to improve Warp's AI capabilities."; certain app interactions to improve Galaxy.";
const TELEMETRY_DOCS_URL: &str =
"https://docs.warp.dev/support-and-community/privacy-and-security/privacy#what-telemetry-data-does-warp-collect-and-why";
pub struct PrivacyPageView { pub struct PrivacyPageView {
page: PageType<Self>, page: PageType<Self>,
@@ -102,7 +99,6 @@ pub struct PrivacyPageView {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum PrivacyPageViewEvent { pub enum PrivacyPageViewEvent {
LaunchNetworkLogging,
ShowAddRegexModal, ShowAddRegexModal,
HideAddRegexModal, HideAddRegexModal,
} }
@@ -203,14 +199,12 @@ impl PrivacyPageView {
} }
fn build_page() -> PageType<Self> { fn build_page() -> PageType<Self> {
let mut widgets: Vec<Box<dyn SettingsWidget<View = Self>>> = vec![ let widgets: Vec<Box<dyn SettingsWidget<View = Self>>> = vec![
Box::new(AIProviderPrivacyWidget),
Box::new(SecretRedactionWidget::default()), Box::new(SecretRedactionWidget::default()),
Box::new(AppAnalyticsWidget::default()), Box::new(AppAnalyticsWidget::default()),
Box::new(CrashReportsWidget::default()), Box::new(CrashReportsWidget::default()),
]; ];
if ContextFlag::NetworkLogConsole.is_enabled() {
widgets.push(Box::new(NetworkLogWidget::default()));
}
PageType::new_uncategorized(widgets, Some("Privacy")) PageType::new_uncategorized(widgets, Some("Privacy"))
} }
@@ -343,10 +337,6 @@ impl PrivacyPageView {
ctx.notify(); ctx.notify();
} }
fn launch_network_logging(&mut self, ctx: &mut ViewContext<Self>) {
ctx.emit(PrivacyPageViewEvent::LaunchNetworkLogging);
}
fn show_add_regex_modal(&mut self, ctx: &mut ViewContext<Self>) { fn show_add_regex_modal(&mut self, ctx: &mut ViewContext<Self>) {
self.add_regex_modal_state.open(ctx); self.add_regex_modal_state.open(ctx);
ctx.emit(PrivacyPageViewEvent::ShowAddRegexModal); ctx.emit(PrivacyPageViewEvent::ShowAddRegexModal);
@@ -443,7 +433,6 @@ pub enum PrivacyPageAction {
ToggleHideSecretsInBlockList, ToggleHideSecretsInBlockList,
SetSecretDisplayMode(SecretDisplayMode), SetSecretDisplayMode(SecretDisplayMode),
ToggleTelemetry, ToggleTelemetry,
LaunchNetworkLogging,
RemoveCustomRegex(usize), RemoveCustomRegex(usize),
AddAllRecommendedRegexes, AddAllRecommendedRegexes,
ShowAddRegexModal, ShowAddRegexModal,
@@ -532,7 +521,6 @@ impl TypedActionView for PrivacyPageView {
}); });
ctx.notify(); ctx.notify();
} }
PrivacyPageAction::LaunchNetworkLogging => self.launch_network_logging(ctx),
PrivacyPageAction::RemoveCustomRegex(idx) => { PrivacyPageAction::RemoveCustomRegex(idx) => {
self.queue_regex_removal(*idx, ctx); self.queue_regex_removal(*idx, ctx);
} }
@@ -1321,7 +1309,6 @@ impl SettingsWidget for SecretRedactionWidget {
#[derive(Default)] #[derive(Default)]
struct AppAnalyticsWidget { struct AppAnalyticsWidget {
switch_state: SwitchStateHandle, switch_state: SwitchStateHandle,
docs_link_mouse_state: MouseStateHandle,
zdr_badge_mouse_state: MouseStateHandle, zdr_badge_mouse_state: MouseStateHandle,
} }
@@ -1491,24 +1478,6 @@ impl SettingsWidget for AppAnalyticsWidget {
.finish(), .finish(),
); );
column.add_child(
Align::new(
ui_builder
.link(
"Read more about Galaxy's use of data".into(),
Some(TELEMETRY_DOCS_URL.into()),
None,
self.docs_link_mouse_state.clone(),
)
.soft_wrap(false)
.build()
.with_margin_bottom(styles::DESCRIPTION_MARGIN_BOTTOM)
.finish(),
)
.left()
.finish(),
);
column.finish() column.finish()
} }
} }
@@ -1565,16 +1534,13 @@ impl SettingsWidget for CrashReportsWidget {
} }
} }
#[derive(Default)] struct AIProviderPrivacyWidget;
struct NetworkLogWidget {
link_mouse_state: MouseStateHandle,
}
impl SettingsWidget for NetworkLogWidget { impl SettingsWidget for AIProviderPrivacyWidget {
type View = PrivacyPageView; type View = PrivacyPageView;
fn search_terms(&self) -> &str { fn search_terms(&self) -> &str {
"network log audit console data collection" "local privacy data ai providers storage conversations terminal drive"
} }
fn render( fn render(
@@ -1583,62 +1549,18 @@ impl SettingsWidget for NetworkLogWidget {
appearance: &Appearance, appearance: &Appearance,
_app: &AppContext, _app: &AppContext,
) -> Box<dyn Element> { ) -> Box<dyn Element> {
let ui_builder = appearance.ui_builder(); render_body_item::<PrivacyPageAction>(
Flex::column() "Local-first data".into(),
.with_child(render_body_item::<PrivacyPageAction>( None,
"Network log console".into(), LocalOnlyIconState::Hidden,
None, ToggleState::Enabled,
// Not rendering a setting, so no need to show local only icon state. appearance,
LocalOnlyIconState::Hidden, Empty::new().finish(),
ToggleState::Enabled, Some(
appearance, "Galaxy keeps your settings, terminal data, conversations, and Galaxy Drive content on this machine. When you use AI, Galaxy sends only the request context needed to the AI providers you have configured."
Empty::new().finish(), .into(),
None, ),
)) )
.with_child(
ui_builder
.paragraph(
"This tool uses AWS Bedrock and is subject to its data collection practices. \
No data is stored locally by Galaxy."
.to_owned(),
)
.with_style(UiComponentStyles {
font_color: Some(
appearance
.theme()
.sub_text_color(appearance.theme().surface_2())
.into_solid(),
),
margin: Some(
Coords::default()
.top(styles::DESCRIPTION_NEGATIVE_MARGIN_OFFSET)
.bottom(styles::DESCRIPTION_LINE_MARGIN_BOTTOM),
),
..Default::default()
})
.build()
.finish(),
)
.with_child(
Align::new(
ui_builder
.link(
"View network logging".to_owned(),
None,
Some(Box::new(|ctx| {
ctx.dispatch_typed_action(PrivacyPageAction::LaunchNetworkLogging);
})),
self.link_mouse_state.clone(),
)
.soft_wrap(false)
.build()
.with_margin_bottom(styles::DESCRIPTION_MARGIN_BOTTOM)
.finish(),
)
.left()
.finish(),
)
.finish()
} }
} }
+1 -60
View File
@@ -59,7 +59,6 @@ const ALTERNATING_LIST_ITEM_PADDING: f32 = 8.0;
const GREY_TEXT_OPACITY: u8 = 60; const GREY_TEXT_OPACITY: u8 = 60;
const MIN_PAGE_WIDTH: f32 = 520.; const MIN_PAGE_WIDTH: f32 = 520.;
const MAX_PAGE_WIDTH: f32 = 800.; const MAX_PAGE_WIDTH: f32 = 800.;
const INFO_TOOLTIP_MAX_WIDTH: f32 = 320.;
/// Left margin for top-level sidebar nav items (pages and umbrella labels). /// Left margin for top-level sidebar nav items (pages and umbrella labels).
pub(super) const NAV_ITEM_LEFT_MARGIN: f32 = 12.; pub(super) const NAV_ITEM_LEFT_MARGIN: f32 = 12.;
@@ -529,61 +528,6 @@ impl LocalOnlyIconState {
} }
} }
pub fn render_info_icon<T: Clone + Action>(
appearance: &Appearance,
additional_info: AdditionalInfo<T>,
) -> Box<dyn Element> {
let tooltip_text = additional_info
.tooltip_override_text
.unwrap_or("Click to learn more in docs".to_owned());
let icon = Container::new(
ConstrainedBox::new(
Icon::Info
.to_warpui_icon(appearance.theme().active_ui_text_color())
.finish(),
)
.with_width(13.)
.with_height(13.)
.finish(),
)
.finish();
let mut info_button = Hoverable::new(additional_info.mouse_state.clone(), move |state| {
let mut stack = Stack::new().with_child(icon);
if state.is_hovered() {
let tool_tip = ConstrainedBox::new(
appearance
.ui_builder()
.tool_tip(tooltip_text)
.build()
.finish(),
)
.with_max_width(INFO_TOOLTIP_MAX_WIDTH)
.finish();
stack.add_positioned_child(
tool_tip,
OffsetPositioning::offset_from_parent(
vec2f(0., -3.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::TopMiddle,
ChildAnchor::BottomMiddle,
),
);
}
stack.finish()
})
.with_cursor(Cursor::PointingHand);
if let Some(on_click_action) = additional_info.on_click_action {
info_button = info_button
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(on_click_action.clone()));
}
Container::new(Box::new(info_button))
.with_margin_left(4.)
.finish()
}
pub fn render_local_only_icon( pub fn render_local_only_icon(
appearance: &Appearance, appearance: &Appearance,
mouse_state: MouseStateHandle, mouse_state: MouseStateHandle,
@@ -675,8 +619,6 @@ pub fn render_body_item_label_internal<T: Clone + Action>(
let label = label.finish(); let label = label.finish();
if let Some(additional_info) = additional_info { if let Some(additional_info) = additional_info {
// Construct a child element for the secondary text, if necessary, before
// `additional_info` gets moved into `render_info_icon()`.
let secondary_text_child = let secondary_text_child =
if let Some(secondary_text) = additional_info.secondary_text.clone() { if let Some(secondary_text) = additional_info.secondary_text.clone() {
let warp_theme = appearance.theme(); let warp_theme = appearance.theme();
@@ -705,8 +647,7 @@ pub fn render_body_item_label_internal<T: Clone + Action>(
let mut row = Flex::row() let mut row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center) .with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(label) .with_child(label);
.with_child(render_info_icon(appearance, additional_info));
if let LocalOnlyIconState::Visible { if let LocalOnlyIconState::Visible {
mouse_state, mouse_state,
custom_tooltip, custom_tooltip,
+4 -17
View File
@@ -7,11 +7,10 @@ use galaxyui::ui_components::switch::SwitchStateHandle;
use galaxyui::{ use galaxyui::{
id, Action, AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, id, Action, AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
}; };
use warpui::elements::{Element, MouseStateHandle}; use warpui::elements::Element;
use super::settings_page::{ use super::settings_page::{
render_body_item, AdditionalInfo, MatchData, PageType, SettingsPageMeta, render_body_item, MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget,
SettingsPageViewHandle, SettingsWidget,
}; };
use super::{ use super::{
flags, LocalOnlyIconState, SettingActionPairContexts, SettingActionPairDescriptions, flags, LocalOnlyIconState, SettingActionPairContexts, SettingActionPairDescriptions,
@@ -23,7 +22,6 @@ use crate::drive::settings::WarpDriveSettings;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum WarpDriveSettingsPageAction { pub enum WarpDriveSettingsPageAction {
ToggleShowWarpDrive, ToggleShowWarpDrive,
OpenUrl(String),
} }
pub fn init_actions_from_parent_view<T: Action + Clone>( pub fn init_actions_from_parent_view<T: Action + Clone>(
@@ -78,9 +76,6 @@ impl TypedActionView for WarpDriveSettingsPageView {
}); });
ctx.notify(); ctx.notify();
} }
WarpDriveSettingsPageAction::OpenUrl(url) => {
ctx.open_url(url.as_str());
}
} }
} }
} }
@@ -126,7 +121,6 @@ impl From<ViewHandle<WarpDriveSettingsPageView>> for SettingsPageViewHandle {
#[derive(Default)] #[derive(Default)]
struct WarpDriveToggleWidget { struct WarpDriveToggleWidget {
switch_state: SwitchStateHandle, switch_state: SwitchStateHandle,
info_icon_mouse_state: MouseStateHandle,
} }
impl SettingsWidget for WarpDriveToggleWidget { impl SettingsWidget for WarpDriveToggleWidget {
@@ -146,14 +140,7 @@ impl SettingsWidget for WarpDriveToggleWidget {
render_body_item::<WarpDriveSettingsPageAction>( render_body_item::<WarpDriveSettingsPageAction>(
"Galaxy Drive".into(), "Galaxy Drive".into(),
Some(AdditionalInfo { None,
mouse_state: self.info_icon_mouse_state.clone(),
on_click_action: Some(WarpDriveSettingsPageAction::OpenUrl(
"https://docs.warp.dev/knowledge-and-collaboration/warp-drive".to_string(),
)),
secondary_text: None,
tooltip_override_text: None,
}),
LocalOnlyIconState::Hidden, LocalOnlyIconState::Hidden,
ToggleState::Enabled, ToggleState::Enabled,
appearance, appearance,
@@ -166,7 +153,7 @@ impl SettingsWidget for WarpDriveToggleWidget {
ctx.dispatch_typed_action(WarpDriveSettingsPageAction::ToggleShowWarpDrive); ctx.dispatch_typed_action(WarpDriveSettingsPageAction::ToggleShowWarpDrive);
}) })
.finish(), .finish(),
Some("Galaxy Drive is a workspace in your terminal where you can save Workflows, Notebooks, Prompts, and Environment Variables for personal use or to share with a team.".into()), Some("Galaxy Drive is a local workspace for Workflows, Notebooks, Prompts, and Environment Variables. Its contents are stored on this machine in your ~/.galaxy directory.".into()),
) )
} }
} }
@@ -124,9 +124,11 @@ fn test_non_ai_commands_remain_active_when_ai_is_disabled() {
let slash_command_data_source = let slash_command_data_source =
input.read(&app, |input, _| input.slash_command_data_source.clone()); input.read(&app, |input, _| input.slash_command_data_source.clone());
// Disable AI globally. // AI is unavailable when no provider runtime is enabled.
AISettings::handle(&app).update(&mut app, |settings, ctx| { AISettings::handle(&app).update(&mut app, |settings, ctx| {
report_if_error!(settings.is_any_ai_enabled.set_value(false, ctx)); report_if_error!(settings.bedrock_enabled.set_value(false, ctx));
report_if_error!(settings.openai_enabled.set_value(false, ctx));
report_if_error!(settings.acp_enabled.set_value(false, ctx));
}); });
slash_command_data_source.read(&app, |data_source, _| { slash_command_data_source.read(&app, |data_source, _| {
+3 -3
View File
@@ -47,13 +47,13 @@ pub enum ThemeKind {
ReceivedReferralReward, ReceivedReferralReward,
#[schemars(description = "Adeberry")] #[schemars(description = "Adeberry")]
Adeberry, Adeberry,
#[default]
#[schemars(description = "Galaxy Dark")] #[schemars(description = "Galaxy Dark")]
GalaxyDark, GalaxyDark,
#[schemars(description = "Galaxy Day")] #[schemars(description = "Galaxy Day")]
GalaxyDay, GalaxyDay,
#[schemars(description = "Phenomenon")] #[schemars(description = "Phenomenon")]
Phenomenon, Phenomenon,
#[default]
#[schemars(description = "Dark")] #[schemars(description = "Dark")]
Dark, Dark,
#[schemars(description = "Dracula")] #[schemars(description = "Dracula")]
@@ -566,8 +566,8 @@ impl RespectSystemTheme {
impl Default for SelectedSystemThemes { impl Default for SelectedSystemThemes {
fn default() -> Self { fn default() -> Self {
Self { Self {
light: ThemeKind::Light, light: ThemeKind::GalaxyDay,
dark: ThemeKind::Dark, dark: ThemeKind::GalaxyDark,
} }
} }
} }
+14 -1
View File
@@ -36,6 +36,19 @@ echo "Compiling .icon bundle for $CHANNEL channel"
BUNDLED_RESOURCES_DIR="$APP_BUNDLE_PATH/Contents/Resources" BUNDLED_RESOURCES_DIR="$APP_BUNDLE_PATH/Contents/Resources"
PARTIAL_INFO_PLIST="$(dirname "$APP_BUNDLE_PATH")/partial-icon-info.plist" PARTIAL_INFO_PLIST="$(dirname "$APP_BUNDLE_PATH")/partial-icon-info.plist"
ACTOOL_ICON_BUNDLE_PATH="$ICON_BUNDLE_PATH"
# The OSS icon artwork is already shared with the in-app icon picker. Assemble its adaptive
# icon package in a temporary directory so we do not need to keep a second large PNG in Git.
if [[ "$CHANNEL" = "oss" ]]; then
TEMP_ICON_ROOT="$(mktemp -d)"
trap 'rm -rf "$TEMP_ICON_ROOT"' EXIT
ACTOOL_ICON_BUNDLE_PATH="$TEMP_ICON_ROOT/AppIcon.icon"
mkdir -p "$ACTOOL_ICON_BUNDLE_PATH/Assets"
cp "$ICON_BUNDLE_PATH/icon.json" "$ACTOOL_ICON_BUNDLE_PATH/icon.json"
cp "$REPO_ROOT/app/assets/bundled/png/galaxy.png" \
"$ACTOOL_ICON_BUNDLE_PATH/Assets/Galaxy.png"
fi
# Compile the .icon bundle using actool # Compile the .icon bundle using actool
xcrun actool \ xcrun actool \
@@ -44,7 +57,7 @@ xcrun actool \
--minimum-deployment-target 10.14 \ --minimum-deployment-target 10.14 \
--app-icon AppIcon \ --app-icon AppIcon \
--output-partial-info-plist "$PARTIAL_INFO_PLIST" \ --output-partial-info-plist "$PARTIAL_INFO_PLIST" \
"$ICON_BUNDLE_PATH" "$ACTOOL_ICON_BUNDLE_PATH"
# Earlier XCode versions won't build the correct asset format for adaptive icons # Earlier XCode versions won't build the correct asset format for adaptive icons
if [[ ! -f "$BUNDLED_RESOURCES_DIR/Assets.car" ]]; then if [[ ! -f "$BUNDLED_RESOURCES_DIR/Assets.car" ]]; then