diff --git a/app/Cargo.toml b/app/Cargo.toml index 30f3259c..91d8e0ce 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -620,7 +620,6 @@ default = [ "supergrok", "remote_code_review", "git_operations_in_code_review", - "galaxy_control_cli", ] # Enable this feature to automatically perform heap profiling. NOTE: This will # substantially slow down program execution. diff --git a/app/channels/oss/icon/AppIcon.icon/icon.json b/app/channels/oss/icon/AppIcon.icon/icon.json new file mode 100644 index 00000000..11c0bc23 --- /dev/null +++ b/app/channels/oss/icon/AppIcon.icon/icon.json @@ -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" + } +} diff --git a/app/src/ai/bedrock/discovery.rs b/app/src/ai/bedrock/discovery.rs index 91523fd2..e4676e32 100644 --- a/app/src/ai/bedrock/discovery.rs +++ b/app/src/ai/bedrock/discovery.rs @@ -7,10 +7,13 @@ use aws_config::BehaviorVersion; use aws_sdk_bedrock::Client; use aws_sdk_bedrockruntime::config::Region; +use futures::{stream, StreamExt}; use super::client::{BedrockClientConfig, BedrockError}; use crate::settings::ai::BedrockModelConfig; +const AVAILABILITY_CHECK_CONCURRENCY: usize = 8; + pub async fn discover_available_models( config: BedrockClientConfig, ) -> Result, String> { @@ -24,61 +27,72 @@ pub async fn discover_available_models( .await .map_err(|error| format!("Could not list AWS Bedrock foundation models: {error}"))?; - let mut models = Vec::new(); - for summary in catalog.model_summaries() { - let model_id = summary.model_id(); - let availability = match client - .get_foundation_model_availability() - .model_id(model_id) - .send() - .await - { - Ok(availability) => availability, - Err(error) => { - log::debug!( - "[bedrock] Availability check failed for {model_id}; excluding model: {error}" - ); - continue; - } - }; + // AWS exposes agreement/authorization state only through an individual + // GetFoundationModelAvailability call. Keep using that control-plane API, + // but bound the independent checks so a large catalog does not serialize + // startup discovery one model at a time. + let checks = catalog.model_summaries().iter().cloned().map(|summary| { + let client = client.clone(); + async move { + let model_id = summary.model_id(); + let availability = match client + .get_foundation_model_availability() + .model_id(model_id) + .send() + .await + { + Ok(availability) => availability, + Err(error) => { + log::debug!( + "[bedrock] Availability check failed for {model_id}; excluding model: {error}" + ); + return None; + } + }; - 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={}", + if !model_availability_is_usable( availability .agreement_availability() - .map(|agreement| agreement.status().as_str()) - .unwrap_or("MISSING"), + .map(|agreement| agreement.status().as_str()), availability.authorization_status().as_str(), availability.entitlement_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 - .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"); - - models.push(BedrockModelConfig { - model_id: model_id.to_owned(), - display_name, - vision_supported, - use_rig: false, - }); - } + }); + let mut models = stream::iter(checks) + .buffer_unordered(AVAILABILITY_CHECK_CONCURRENCY) + .filter_map(futures::future::ready) + .collect::>() + .await; models.sort_by(|left, right| left.display_name.cmp(&right.display_name)); if models.is_empty() { diff --git a/app/src/ai/blocklist/controller/response_stream.rs b/app/src/ai/blocklist/controller/response_stream.rs index 5dddbec1..a03c5795 100644 --- a/app/src/ai/blocklist/controller/response_stream.rs +++ b/app/src/ai/blocklist/controller/response_stream.rs @@ -166,26 +166,24 @@ impl ResponseStream { // Check if this specific model has an OpenAI-compatible routing entry. // This allows OpenAI/LiteLLM models to coexist with Bedrock models — // only models fetched from the OpenAI endpoint route through it. - if *settings.openai_enabled.value() { - let llm_prefs = LLMPreferences::as_ref(ctx); - if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) { - return ProviderConfig::OpenAI(OpenAIClientConfig { - kind: client_config.kind, - base_url: client_config.base_url.clone(), - api_key: client_config.api_key.clone(), - project_id: client_config.project_id.clone(), - location: client_config.location.clone(), - model: client_config - .model - .clone() - .or_else(|| Some(model_id.to_string())), - reasoning_effort: client_config.reasoning_effort.clone(), - max_input_tokens: client_config.max_input_tokens, - max_output_tokens: client_config.max_output_tokens, - use_rig: client_config.use_rig, - supports_system_messages: client_config.supports_system_messages, - }); - } + let llm_prefs = LLMPreferences::as_ref(ctx); + if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) { + return ProviderConfig::OpenAI(OpenAIClientConfig { + kind: client_config.kind, + base_url: client_config.base_url.clone(), + api_key: client_config.api_key.clone(), + project_id: client_config.project_id.clone(), + location: client_config.location.clone(), + model: client_config + .model + .clone() + .or_else(|| Some(model_id.to_string())), + reasoning_effort: client_config.reasoning_effort.clone(), + max_input_tokens: client_config.max_input_tokens, + max_output_tokens: client_config.max_output_tokens, + use_rig: client_config.use_rig, + supports_system_messages: client_config.supports_system_messages, + }); } // Fall back to Bedrock diff --git a/app/src/ai/chatgpt_auth.rs b/app/src/ai/chatgpt_auth.rs index 1fe1dcae..b74ef59c 100644 --- a/app/src/ai/chatgpt_auth.rs +++ b/app/src/ai/chatgpt_auth.rs @@ -46,8 +46,17 @@ pub(crate) struct ChatGPTAuthModel { impl ChatGPTAuthModel { 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 { - state: ChatGPTAuthState::NotConnected, + state, pending_code_verifier: None, pending_state: None, } diff --git a/app/src/ai/crosscheck/reviewer.rs b/app/src/ai/crosscheck/reviewer.rs index f40112c1..98ff169e 100644 --- a/app/src/ai/crosscheck/reviewer.rs +++ b/app/src/ai/crosscheck/reviewer.rs @@ -160,26 +160,24 @@ impl CrosscheckReviewer { let settings = AISettings::as_ref(ctx); // Check if this model has an OpenAI-compatible routing entry - if *settings.openai_enabled.value() { - let llm_prefs = LLMPreferences::as_ref(ctx); - if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) { - return ProviderConfig::OpenAI(OpenAIClientConfig { - kind: client_config.kind, - base_url: client_config.base_url.clone(), - api_key: client_config.api_key.clone(), - project_id: client_config.project_id.clone(), - location: client_config.location.clone(), - model: client_config - .model - .clone() - .or_else(|| Some(model_id.to_string())), - reasoning_effort: client_config.reasoning_effort.clone(), - max_input_tokens: client_config.max_input_tokens, - max_output_tokens: Some(REVIEWER_MAX_OUTPUT_TOKENS), - use_rig: client_config.use_rig, - supports_system_messages: client_config.supports_system_messages, - }); - } + let llm_prefs = LLMPreferences::as_ref(ctx); + if let Some(client_config) = llm_prefs.openai_client_config_for_model(model_id) { + return ProviderConfig::OpenAI(OpenAIClientConfig { + kind: client_config.kind, + base_url: client_config.base_url.clone(), + api_key: client_config.api_key.clone(), + project_id: client_config.project_id.clone(), + location: client_config.location.clone(), + model: client_config + .model + .clone() + .or_else(|| Some(model_id.to_string())), + reasoning_effort: client_config.reasoning_effort.clone(), + max_input_tokens: client_config.max_input_tokens, + max_output_tokens: Some(REVIEWER_MAX_OUTPUT_TOKENS), + use_rig: client_config.use_rig, + supports_system_messages: client_config.supports_system_messages, + }); } // Fall back to Bedrock via external config diff --git a/app/src/ai/llms.rs b/app/src/ai/llms.rs index 545a920e..185b78dd 100644 --- a/app/src/ai/llms.rs +++ b/app/src/ai/llms.rs @@ -634,6 +634,27 @@ impl LLMPreferences { } }); + #[cfg(not(target_family = "wasm"))] + if ctx + .try_get_singleton_model_as_ref::() + .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| { if let UserWorkspacesEvent::TeamsChanged = event { me.sanitize_disabled_custom_model_preferences(ctx); @@ -735,6 +756,10 @@ impl LLMPreferences { #[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.inject_openai_models(ctx); me.ensure_default_model_present(); @@ -978,7 +1003,7 @@ impl LLMPreferences { let settings = AISettings::as_ref(ctx); self.inject_acp_models(ctx); - if !*settings.openai_enabled.value() { + if !settings.is_openai_provider_enabled() { return; } @@ -1163,7 +1188,7 @@ impl LLMPreferences { }, )]), discount_percentage: None, - context_window: openai_model_context_window(model), + context_window: openai_model_context_window(model, provider_kind), }; self.models_by_feature .agent_mode @@ -1447,7 +1472,7 @@ impl LLMPreferences { #[cfg(not(target_family = "wasm"))] pub fn fetch_openai_models_from_endpoint(&mut self, ctx: &mut ModelContext) { let settings = AISettings::as_ref(ctx); - if !*settings.openai_enabled.value() { + if !settings.is_openai_provider_enabled() { return; } @@ -1509,7 +1534,7 @@ impl LLMPreferences { ctx: &mut ModelContext, ) { let settings = AISettings::as_ref(ctx); - if !*settings.openai_enabled.value() { + if !settings.is_openai_provider_enabled() { return; } @@ -1716,7 +1741,7 @@ impl LLMPreferences { } let settings = AISettings::as_ref(ctx); - if !*settings.openai_enabled.value() + if !settings.is_openai_provider_enabled() || !settings.openai_providers.value().iter().any(|provider| { 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"))] -fn openai_model_context_window(model: &OpenAIModelConfig) -> LLMContextWindow { - let context_size = openai_model_context_size(model); +fn openai_model_context_window( + 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 { - is_configurable: false, - min: context_size, - max: context_size, - default_max: context_size, + is_configurable: max_context_size > default_context_size, + min: default_context_size, + max: max_context_size, + default_max: default_context_size, } } @@ -2822,18 +2855,23 @@ fn chatgpt_models_from_codex_response(body: &serde_json::Value) -> Vec bool { // Galaxy does not require Warp authentication for AI. - // AI is enabled only when the user hasn't explicitly disabled it, at least one local - // runtime is enabled, and there's no org policy blocking it. - *self.is_any_ai_enabled - && self.has_enabled_ai_runtime() - && !self.is_ai_disabled_due_to_remote_session_org_policy(app) + // Configuring and enabling a provider is the single source of truth for AI availability. + 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 { @@ -2409,11 +2434,11 @@ impl AISettings { /// Returns whether Galaxy has a local model provider or agent runtime enabled. pub fn has_enabled_ai_runtime(&self) -> bool { - *self.bedrock_enabled.value() - || *self.openai_enabled.value() + (*self.bedrock_enabled.value() && !self.bedrock_models.value().is_empty()) + || self.has_enabled_openai_provider() || (cfg!(unix) && FeatureFlag::AgentClientProtocol.is_enabled() - && *self.acp_enabled.value()) + && !self.enabled_acp_providers().is_empty()) } 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 { - self.is_any_ai_enabled(app) - && *self.is_active_ai_enabled_internal - && AppExecutionMode::as_ref(app).allows_active_ai() + self.is_any_ai_enabled(app) && AppExecutionMode::as_ref(app).allows_active_ai() } pub fn is_prompt_suggestions_enabled(&self, app: &galaxyui::AppContext) -> bool { diff --git a/app/src/settings/ai_tests.rs b/app/src/settings/ai_tests.rs index b412b059..d862f9c8 100644 --- a/app/src/settings/ai_tests.rs +++ b/app/src/settings/ai_tests.rs @@ -433,6 +433,21 @@ fn orchestration_is_enabled_when_ai_is_enabled() { initialize_settings_for_tests(&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| { assert!(settings.is_orchestration_enabled(ctx)); }); diff --git a/app/src/settings/initializer.rs b/app/src/settings/initializer.rs index c6f0861a..3982da37 100644 --- a/app/src/settings/initializer.rs +++ b/app/src/settings/initializer.rs @@ -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 // `ThinkingDisplayMode` enum setting. // diff --git a/app/src/settings/theme.rs b/app/src/settings/theme.rs index 4d0e41d5..90a2b836 100644 --- a/app/src/settings/theme.rs +++ b/app/src/settings/theme.rs @@ -14,8 +14,7 @@ use crate::themes::theme::{RespectSystemTheme, SelectedSystemThemes, ThemeKind}; define_settings_group!(ThemeSettings, settings: [ theme_kind: Theme { type: ThemeKind, - // Note that for new users, we now override this default value in SettingsInitializer - // to set the default theme to Phenomenon. + // New installations start with Galaxy's built-in brand theme. default: ThemeKind::default(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Never, diff --git a/app/src/settings_view/about_page.rs b/app/src/settings_view/about_page.rs index 37655446..12ca4e20 100644 --- a/app/src/settings_view/about_page.rs +++ b/app/src/settings_view/about_page.rs @@ -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 warpui::assets::asset_cache::AssetSource; -use warpui::elements::{Align, CacheOption, ConstrainedBox, Element, Image}; use super::settings_page::{ MatchData, PageType, SettingsPageEvent, SettingsPageMeta, SettingsPageViewHandle, @@ -48,7 +53,7 @@ impl SettingsWidget for AboutPageWidget { fn render( &self, _view: &AboutPageView, - _appearance: &Appearance, + appearance: &Appearance, app: &AppContext, ) -> Box { let icon_file = @@ -62,16 +67,54 @@ impl SettingsWidget for AboutPageWidget { _ => "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( ConstrainedBox::new( - Image::new( - AssetSource::Bundled { path: image_path }, - CacheOption::BySize, + Container::new( + Flex::column() + .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(), ) - .with_max_height(144.) - .with_max_width(144.) + .with_max_width(560.) .finish(), ) .finish() diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index 7c5a2978..cdc97657 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -101,7 +101,7 @@ use crate::settings::{ BedrockEnabled, BedrockModelConfig, CodeSettings, CodebaseContextEnabled, CrosscheckEnabled, FileBasedMcpEnabled, GitOperationsAutogenEnabled, IncludeAgentCommandsInHistory, InputSettings, IntelligentAutosuggestionsEnabled, LongRunningCommandSubmissionMode, MemoryEnabled, - NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, OpenAIEnabled, OpenAIModelConfig, + NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, OpenAIModelConfig, OpenAIProviderConfig, OrchestrationMessageDisplayMode, PromptSubmissionMode, RuleSuggestionsEnabled, SharedBlockTitleGenerationEnabled, ShouldRenderCLIAgentToolbar, ShouldRenderUseAgentToolbarForUserCommands, ShowAgentTips, ShowConversationHistory, @@ -129,8 +129,6 @@ use crate::{ }; const CONTENT_FONT_SIZE: f32 = 12.; -const PRIMARY_HEADER_FONT_SIZE: f32 = 24.; - const AI_SETTINGS_DROPDOWN_WIDTH: f32 = 250.; const AI_SETTINGS_DROPDOWN_MAX_HEIGHT: f32 = 250.; 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). #[derive(Clone, Copy, Debug, PartialEq, Eq)] 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, /// Agent profiles and permissions. Profiles, @@ -217,28 +215,6 @@ pub fn init_actions_from_parent_view( 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( vec![ToggleSettingActionPair::new( if FeatureFlag::AgentView.is_enabled() { @@ -2115,7 +2091,6 @@ impl AISettingsPageView { match subpage { None => { // Full page: all widgets (legacy behavior) - widgets.push(Box::new(GlobalAIWidget::default())); if ai_settings .intelligent_autosuggestions_enabled_internal .is_supported_on_current_platform() @@ -2158,8 +2133,7 @@ impl AISettingsPageView { widgets.push(Box::new(OtherAIWidget::default())); } Some(AISubpage::WarpAgent) => { - // Galaxy Agent page: global toggle + Active AI + Input + Other - widgets.push(Box::new(GlobalAIWidget::default())); + // Galaxy Agent page: suggestions, input, and other agent settings. if ai_settings .intelligent_autosuggestions_enabled_internal .is_supported_on_current_platform() @@ -4127,85 +4101,8 @@ fn render_ai_list( .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 { - 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)] struct ActiveAIWidget { - active_ai_toggle: SwitchStateHandle, intelligent_autosuggestions_toggle: SwitchStateHandle, prompt_suggestions_toggle: SwitchStateHandle, code_suggestions_toggle: SwitchStateHandle, @@ -4452,38 +4349,12 @@ impl SettingsWidget for ActiveAIWidget { appearance: &Appearance, app: &AppContext, ) -> Box { - let ai_settings = AISettings::as_ref(app); - let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app); let mut column = Flex::column() .with_child(render_separator(appearance)) .with_child( - Container::new( - Flex::row() - .with_main_axis_size(MainAxisSize::Max) - .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(), + Container::new(build_sub_header(appearance, "AI suggestions", None).finish()) + .with_padding_bottom(HEADER_PADDING) + .finish(), ); if self.is_next_command_toggleable(app) { @@ -7230,7 +7101,6 @@ struct AcpProviderCardState { struct ProviderSettingsWidget { provider_type: ProviderSetupProviderType, - enabled_toggle: SwitchStateHandle, bedrock_enabled_toggle: SwitchStateHandle, add_openai_provider_button: ViewHandle, add_litellm_provider_button: ViewHandle, @@ -7342,7 +7212,6 @@ impl ProviderSettingsWidget { }); Self { provider_type, - enabled_toggle: SwitchStateHandle::default(), bedrock_enabled_toggle: SwitchStateHandle::default(), add_openai_provider_button, add_litellm_provider_button, @@ -7990,16 +7859,6 @@ impl SettingsWidget for ProviderSettingsWidget { ProviderSetupProviderType::OpenAI | ProviderSetupProviderType::LiteLLM | ProviderSetupProviderType::ChatGPTSubscription => { - column.add_child(render_ai_setting_toggle::( - "Enable direct providers", - AISettingsPageAction::ToggleOpenAIEnabled, - *settings.openai_enabled.value(), - true, - self.enabled_toggle.clone(), - &RefCell::new(HashMap::new()), - app, - )); - if is_setup_visible { column.add_child(Self::render_inline_setup(appearance, view)); } diff --git a/app/src/settings_view/mod.rs b/app/src/settings_view/mod.rs index 75bffd57..a11fa726 100644 --- a/app/src/settings_view/mod.rs +++ b/app/src/settings_view/mod.rs @@ -104,8 +104,8 @@ pub use code_page::CodeSettingsPageView; pub use features_page::FeaturesPageAction; pub use privacy_page::PrivacyPageAction; pub use settings_page::{ - render_body_item_label, render_info_icon, render_input_list, render_separator, AdditionalInfo, - InputListItem, LocalOnlyIconState, ToggleState, + render_body_item_label, render_input_list, render_separator, AdditionalInfo, InputListItem, + LocalOnlyIconState, ToggleState, }; pub use teams_page::{OpenTeamsSettingsModalArgs, TeamsInviteOption}; @@ -1216,11 +1216,6 @@ impl SettingsView { let warp_drive_page_handle = 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 let mcp_servers_page_handle = ctx.add_typed_action_view(MCPServersSettingsPageView::new); ctx.subscribe_to_view(&mcp_servers_page_handle, |me, _, event, ctx| { @@ -1261,7 +1256,6 @@ impl SettingsView { SettingsPage::new(appearance_page_handle), SettingsPage::new(features_page_handle), SettingsPage::new(keybindings_handle), - SettingsPage::new(platform_page_handle), SettingsPage::new(warpify_page_handle), SettingsPage::new(warp_drive_page_handle), ]; @@ -1730,9 +1724,6 @@ impl SettingsView { ctx: &mut ViewContext, ) { match event { - PrivacyPageViewEvent::LaunchNetworkLogging => { - ctx.emit(SettingsViewEvent::LaunchNetworkLogging); - } PrivacyPageViewEvent::ShowAddRegexModal => { // Modal rendering is handled in get_modal_content_for_page ctx.notify(); @@ -1744,23 +1735,6 @@ impl SettingsView { } } - fn handle_platform_page_event( - &mut self, - event: &platform_page::PlatformPageViewEvent, - ctx: &mut ViewContext, - ) { - 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( &mut self, event: &MCPServersSettingsPageEvent, diff --git a/app/src/settings_view/privacy_page.rs b/app/src/settings_view/privacy_page.rs index b32e9e4e..916e927c 100644 --- a/app/src/settings_view/privacy_page.rs +++ b/app/src/settings_view/privacy_page.rs @@ -4,13 +4,12 @@ use std::collections::{HashMap, HashSet}; use std::sync::LazyLock; use std::time::Duration; -use galaxy_core::context_flag::ContextFlag; use galaxy_core::features::FeatureFlag; use galaxy_core::ui::theme::color::internal_colors; use galaxy_core::ui::theme::GalaxyTheme; use galaxyui::elements::{ - Align, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, - Empty, Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, + ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty, + Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect, Shrinkable, Stack, Text, }; @@ -79,9 +78,7 @@ const TELEMETRY_DESCRIPTION_OLD: &str = const TELEMETRY_TITLE: &str = "Help improve Galaxy"; const TELEMETRY_DESCRIPTION: &str = "App analytics help us make the product better for you. We may collect \ - certain console interactions to improve Warp's AI capabilities."; -const TELEMETRY_DOCS_URL: &str = - "https://docs.warp.dev/support-and-community/privacy-and-security/privacy#what-telemetry-data-does-warp-collect-and-why"; + certain app interactions to improve Galaxy."; pub struct PrivacyPageView { page: PageType, @@ -102,7 +99,6 @@ pub struct PrivacyPageView { #[derive(Clone, Copy)] pub enum PrivacyPageViewEvent { - LaunchNetworkLogging, ShowAddRegexModal, HideAddRegexModal, } @@ -203,14 +199,12 @@ impl PrivacyPageView { } fn build_page() -> PageType { - let mut widgets: Vec>> = vec![ + let widgets: Vec>> = vec![ + Box::new(AIProviderPrivacyWidget), Box::new(SecretRedactionWidget::default()), Box::new(AppAnalyticsWidget::default()), Box::new(CrashReportsWidget::default()), ]; - if ContextFlag::NetworkLogConsole.is_enabled() { - widgets.push(Box::new(NetworkLogWidget::default())); - } PageType::new_uncategorized(widgets, Some("Privacy")) } @@ -343,10 +337,6 @@ impl PrivacyPageView { ctx.notify(); } - fn launch_network_logging(&mut self, ctx: &mut ViewContext) { - ctx.emit(PrivacyPageViewEvent::LaunchNetworkLogging); - } - fn show_add_regex_modal(&mut self, ctx: &mut ViewContext) { self.add_regex_modal_state.open(ctx); ctx.emit(PrivacyPageViewEvent::ShowAddRegexModal); @@ -443,7 +433,6 @@ pub enum PrivacyPageAction { ToggleHideSecretsInBlockList, SetSecretDisplayMode(SecretDisplayMode), ToggleTelemetry, - LaunchNetworkLogging, RemoveCustomRegex(usize), AddAllRecommendedRegexes, ShowAddRegexModal, @@ -532,7 +521,6 @@ impl TypedActionView for PrivacyPageView { }); ctx.notify(); } - PrivacyPageAction::LaunchNetworkLogging => self.launch_network_logging(ctx), PrivacyPageAction::RemoveCustomRegex(idx) => { self.queue_regex_removal(*idx, ctx); } @@ -1321,7 +1309,6 @@ impl SettingsWidget for SecretRedactionWidget { #[derive(Default)] struct AppAnalyticsWidget { switch_state: SwitchStateHandle, - docs_link_mouse_state: MouseStateHandle, zdr_badge_mouse_state: MouseStateHandle, } @@ -1491,24 +1478,6 @@ impl SettingsWidget for AppAnalyticsWidget { .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() } } @@ -1565,16 +1534,13 @@ impl SettingsWidget for CrashReportsWidget { } } -#[derive(Default)] -struct NetworkLogWidget { - link_mouse_state: MouseStateHandle, -} +struct AIProviderPrivacyWidget; -impl SettingsWidget for NetworkLogWidget { +impl SettingsWidget for AIProviderPrivacyWidget { type View = PrivacyPageView; fn search_terms(&self) -> &str { - "network log audit console data collection" + "local privacy data ai providers storage conversations terminal drive" } fn render( @@ -1583,62 +1549,18 @@ impl SettingsWidget for NetworkLogWidget { appearance: &Appearance, _app: &AppContext, ) -> Box { - let ui_builder = appearance.ui_builder(); - Flex::column() - .with_child(render_body_item::( - "Network log console".into(), - None, - // Not rendering a setting, so no need to show local only icon state. - LocalOnlyIconState::Hidden, - ToggleState::Enabled, - appearance, - Empty::new().finish(), - 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() + render_body_item::( + "Local-first data".into(), + None, + LocalOnlyIconState::Hidden, + ToggleState::Enabled, + appearance, + Empty::new().finish(), + Some( + "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." + .into(), + ), + ) } } diff --git a/app/src/settings_view/settings_page.rs b/app/src/settings_view/settings_page.rs index c80fbf35..66f9fb63 100644 --- a/app/src/settings_view/settings_page.rs +++ b/app/src/settings_view/settings_page.rs @@ -59,7 +59,6 @@ const ALTERNATING_LIST_ITEM_PADDING: f32 = 8.0; const GREY_TEXT_OPACITY: u8 = 60; const MIN_PAGE_WIDTH: f32 = 520.; 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). pub(super) const NAV_ITEM_LEFT_MARGIN: f32 = 12.; @@ -529,61 +528,6 @@ impl LocalOnlyIconState { } } -pub fn render_info_icon( - appearance: &Appearance, - additional_info: AdditionalInfo, -) -> Box { - 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( appearance: &Appearance, mouse_state: MouseStateHandle, @@ -675,8 +619,6 @@ pub fn render_body_item_label_internal( let label = label.finish(); 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 = if let Some(secondary_text) = additional_info.secondary_text.clone() { let warp_theme = appearance.theme(); @@ -705,8 +647,7 @@ pub fn render_body_item_label_internal( let mut row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(label) - .with_child(render_info_icon(appearance, additional_info)); + .with_child(label); if let LocalOnlyIconState::Visible { mouse_state, custom_tooltip, diff --git a/app/src/settings_view/warp_drive_page.rs b/app/src/settings_view/warp_drive_page.rs index 21a1d24c..6e614c5a 100644 --- a/app/src/settings_view/warp_drive_page.rs +++ b/app/src/settings_view/warp_drive_page.rs @@ -7,11 +7,10 @@ use galaxyui::ui_components::switch::SwitchStateHandle; use galaxyui::{ id, Action, AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, }; -use warpui::elements::{Element, MouseStateHandle}; +use warpui::elements::Element; use super::settings_page::{ - render_body_item, AdditionalInfo, MatchData, PageType, SettingsPageMeta, - SettingsPageViewHandle, SettingsWidget, + render_body_item, MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget, }; use super::{ flags, LocalOnlyIconState, SettingActionPairContexts, SettingActionPairDescriptions, @@ -23,7 +22,6 @@ use crate::drive::settings::WarpDriveSettings; #[derive(Debug, Clone)] pub enum WarpDriveSettingsPageAction { ToggleShowWarpDrive, - OpenUrl(String), } pub fn init_actions_from_parent_view( @@ -78,9 +76,6 @@ impl TypedActionView for WarpDriveSettingsPageView { }); ctx.notify(); } - WarpDriveSettingsPageAction::OpenUrl(url) => { - ctx.open_url(url.as_str()); - } } } } @@ -126,7 +121,6 @@ impl From> for SettingsPageViewHandle { #[derive(Default)] struct WarpDriveToggleWidget { switch_state: SwitchStateHandle, - info_icon_mouse_state: MouseStateHandle, } impl SettingsWidget for WarpDriveToggleWidget { @@ -146,14 +140,7 @@ impl SettingsWidget for WarpDriveToggleWidget { render_body_item::( "Galaxy Drive".into(), - Some(AdditionalInfo { - 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, - }), + None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, @@ -166,7 +153,7 @@ impl SettingsWidget for WarpDriveToggleWidget { ctx.dispatch_typed_action(WarpDriveSettingsPageAction::ToggleShowWarpDrive); }) .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()), ) } } diff --git a/app/src/terminal/input/slash_command_model_tests.rs b/app/src/terminal/input/slash_command_model_tests.rs index 24afe869..3c68c118 100644 --- a/app/src/terminal/input/slash_command_model_tests.rs +++ b/app/src/terminal/input/slash_command_model_tests.rs @@ -124,9 +124,11 @@ fn test_non_ai_commands_remain_active_when_ai_is_disabled() { let slash_command_data_source = 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| { - 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, _| { diff --git a/app/src/themes/theme.rs b/app/src/themes/theme.rs index 0a291381..9c3d7f5f 100644 --- a/app/src/themes/theme.rs +++ b/app/src/themes/theme.rs @@ -47,13 +47,13 @@ pub enum ThemeKind { ReceivedReferralReward, #[schemars(description = "Adeberry")] Adeberry, + #[default] #[schemars(description = "Galaxy Dark")] GalaxyDark, #[schemars(description = "Galaxy Day")] GalaxyDay, #[schemars(description = "Phenomenon")] Phenomenon, - #[default] #[schemars(description = "Dark")] Dark, #[schemars(description = "Dracula")] @@ -566,8 +566,8 @@ impl RespectSystemTheme { impl Default for SelectedSystemThemes { fn default() -> Self { Self { - light: ThemeKind::Light, - dark: ThemeKind::Dark, + light: ThemeKind::GalaxyDay, + dark: ThemeKind::GalaxyDark, } } } diff --git a/script/compile_icon b/script/compile_icon index 3022eb12..b3f90f12 100755 --- a/script/compile_icon +++ b/script/compile_icon @@ -36,6 +36,19 @@ echo "Compiling .icon bundle for $CHANNEL channel" BUNDLED_RESOURCES_DIR="$APP_BUNDLE_PATH/Contents/Resources" 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 xcrun actool \ @@ -44,7 +57,7 @@ xcrun actool \ --minimum-deployment-target 10.14 \ --app-icon AppIcon \ --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 if [[ ! -f "$BUNDLED_RESOURCES_DIR/Assets.car" ]]; then