From d3a0281062b39be7513b64fe125c3b2baa311ea4 Mon Sep 17 00:00:00 2001 From: Viren Mohindra Date: Sun, 4 Jan 2026 05:22:06 +0530 Subject: [PATCH] fix: prevent crash on macos 26.x beta during startup (#473) defer apple intelligence availability check from app initialization to when the user actually tries to use the feature. accessing SystemLanguageModel.default during early app startup causes SIGABRT on macOS 26.x (tahoe) beta. - remove availability check from default_post_process_providers() - always include apple intelligence provider on macos arm64 - add checkAppleIntelligenceAvailable command for lazy checking - show error message if apple intelligence unavailable when selected - clear error on any dropdown selection for better UX --- .github/workflows/pr-test-build.yml | 2 +- src-tauri/src/commands/mod.rs | 15 ++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/settings.rs | 46 ++++++++++++++----- src/bindings.ts | 9 +++- .../usePostProcessProviderState.ts | 28 ++++++++--- .../PostProcessingSettings.tsx | 8 ++++ src/i18n/locales/de/translation.json | 3 +- src/i18n/locales/en/translation.json | 3 +- src/i18n/locales/es/translation.json | 3 +- src/i18n/locales/fr/translation.json | 3 +- src/i18n/locales/it/translation.json | 3 +- src/i18n/locales/ja/translation.json | 3 +- src/i18n/locales/pl/translation.json | 3 +- src/i18n/locales/ru/translation.json | 3 +- src/i18n/locales/vi/translation.json | 3 +- src/i18n/locales/zh/translation.json | 3 +- 17 files changed, 109 insertions(+), 30 deletions(-) diff --git a/.github/workflows/pr-test-build.yml b/.github/workflows/pr-test-build.yml index 738b423..2134258 100644 --- a/.github/workflows/pr-test-build.yml +++ b/.github/workflows/pr-test-build.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to build' + description: "PR number to build" required: true type: string diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 35feee0..803cf6f 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -115,3 +115,18 @@ pub fn open_app_data_dir(app: AppHandle) -> Result<(), String> { Ok(()) } + +/// Check if Apple Intelligence is available on this device. +/// Called by the frontend when the user selects Apple Intelligence provider. +#[specta::specta] +#[tauri::command] +pub fn check_apple_intelligence_available() -> bool { + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + crate::apple_intelligence::check_apple_intelligence_availability() + } + #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] + { + false + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index be98784..6472167 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -274,6 +274,7 @@ pub fn run() { commands::open_recordings_folder, commands::open_log_dir, commands::open_app_data_dir, + commands::check_apple_intelligence_available, commands::models::get_available_models, commands::models::get_model_info, commands::models::download_model, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 8ec885d..1bd0d71 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -97,6 +97,10 @@ pub struct PostProcessProvider { pub id: String, pub label: String, pub base_url: String, + #[serde(default)] + pub allow_base_url_edit: bool, + #[serde(default)] + pub models_endpoint: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] @@ -374,45 +378,63 @@ fn default_post_process_providers() -> Vec { id: "openai".to_string(), label: "OpenAI".to_string(), base_url: "https://api.openai.com/v1".to_string(), + allow_base_url_edit: false, + models_endpoint: Some("/models".to_string()), }, PostProcessProvider { id: "openrouter".to_string(), label: "OpenRouter".to_string(), base_url: "https://openrouter.ai/api/v1".to_string(), + allow_base_url_edit: false, + models_endpoint: Some("/models".to_string()), }, PostProcessProvider { id: "anthropic".to_string(), label: "Anthropic".to_string(), base_url: "https://api.anthropic.com/v1".to_string(), + allow_base_url_edit: false, + models_endpoint: Some("/models".to_string()), }, PostProcessProvider { id: "groq".to_string(), label: "Groq".to_string(), base_url: "https://api.groq.com/openai/v1".to_string(), + allow_base_url_edit: false, + models_endpoint: Some("/models".to_string()), }, PostProcessProvider { id: "cerebras".to_string(), label: "Cerebras".to_string(), base_url: "https://api.cerebras.ai/v1".to_string(), - }, - PostProcessProvider { - id: "custom".to_string(), - label: "Custom".to_string(), - base_url: "http://localhost:11434/v1".to_string(), + allow_base_url_edit: false, + models_endpoint: Some("/models".to_string()), }, ]; + // Note: We always include Apple Intelligence on macOS ARM64 without checking availability + // at startup. The availability check is deferred to when the user actually tries to use it + // (in actions.rs). This prevents crashes on macOS 26.x beta where accessing + // SystemLanguageModel.default during early app initialization causes SIGABRT. #[cfg(all(target_os = "macos", target_arch = "aarch64"))] { - if crate::apple_intelligence::check_apple_intelligence_availability() { - providers.push(PostProcessProvider { - id: APPLE_INTELLIGENCE_PROVIDER_ID.to_string(), - label: "Apple Intelligence".to_string(), - base_url: "apple-intelligence://local".to_string(), - }); - } + providers.push(PostProcessProvider { + id: APPLE_INTELLIGENCE_PROVIDER_ID.to_string(), + label: "Apple Intelligence".to_string(), + base_url: "apple-intelligence://local".to_string(), + allow_base_url_edit: false, + models_endpoint: None, + }); } + // Custom provider always comes last + providers.push(PostProcessProvider { + id: "custom".to_string(), + label: "Custom".to_string(), + base_url: "http://localhost:11434/v1".to_string(), + allow_base_url_edit: true, + models_endpoint: Some("/models".to_string()), + }); + providers } diff --git a/src/bindings.ts b/src/bindings.ts index 24aef75..2877b1b 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -343,6 +343,13 @@ async openAppDataDir() : Promise> { else return { status: "error", error: e as any }; } }, +/** + * Check if Apple Intelligence is available on this device. + * Called by the frontend when the user selects Apple Intelligence provider. + */ +async checkAppleIntelligenceAvailable() : Promise { + return await TAURI_INVOKE("check_apple_intelligence_available"); +}, async getAvailableModels() : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_available_models") }; @@ -635,7 +642,7 @@ export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5" export type OverlayPosition = "none" | "top" | "bottom" export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v" -export type PostProcessProvider = { id: string; label: string; base_url: string } +export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null } export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3" export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string } export type SoundTheme = "marimba" | "pop" | "custom" diff --git a/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts b/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts index c5c7364..3bd5e79 100644 --- a/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts +++ b/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts @@ -1,7 +1,6 @@ -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useSettings } from "../../../hooks/useSettings"; -import { useSettingsStore } from "../../../stores/settingsStore"; -import type { PostProcessProvider } from "@/bindings"; +import { commands, type PostProcessProvider } from "@/bindings"; import type { ModelOption } from "./types"; import type { DropdownOption } from "../../ui/Dropdown"; @@ -12,6 +11,7 @@ type PostProcessProviderState = { selectedProvider: PostProcessProvider | undefined; isCustomProvider: boolean; isAppleProvider: boolean; + appleIntelligenceUnavailable: boolean; baseUrl: string; handleBaseUrlChange: (value: string) => void; isBaseUrlUpdating: boolean; @@ -60,6 +60,8 @@ export const usePostProcessProviderState = (): PostProcessProviderState => { }, [providers, selectedProviderId]); const isAppleProvider = selectedProvider?.id === APPLE_PROVIDER_ID; + const [appleIntelligenceUnavailable, setAppleIntelligenceUnavailable] = + useState(false); // Use settings directly as single source of truth const baseUrl = selectedProvider?.base_url ?? ""; @@ -74,10 +76,23 @@ export const usePostProcessProviderState = (): PostProcessProviderState => { }, [providers]); const handleProviderSelect = useCallback( - (providerId: string) => { - if (providerId !== selectedProviderId) { - void setPostProcessProvider(providerId); + async (providerId: string) => { + // Clear error state on any selection attempt (allows dismissing the error) + setAppleIntelligenceUnavailable(false); + + if (providerId === selectedProviderId) return; + + // Check Apple Intelligence availability before selecting + if (providerId === APPLE_PROVIDER_ID) { + const available = await commands.checkAppleIntelligenceAvailable(); + if (!available) { + setAppleIntelligenceUnavailable(true); + // Don't return - still set the provider so dropdown shows the selection + // The backend gracefully handles unavailable Apple Intelligence + } } + + void setPostProcessProvider(providerId); }, [selectedProviderId, setPostProcessProvider], ); @@ -182,6 +197,7 @@ export const usePostProcessProviderState = (): PostProcessProviderState => { selectedProvider, isCustomProvider, isAppleProvider, + appleIntelligenceUnavailable, baseUrl, handleBaseUrlChange, isBaseUrlUpdating, diff --git a/src/components/settings/post-processing/PostProcessingSettings.tsx b/src/components/settings/post-processing/PostProcessingSettings.tsx index 2db6188..0c521bd 100644 --- a/src/components/settings/post-processing/PostProcessingSettings.tsx +++ b/src/components/settings/post-processing/PostProcessingSettings.tsx @@ -56,6 +56,14 @@ const PostProcessingSettingsApiComponent: React.FC = () => { + {state.appleIntelligenceUnavailable ? ( +
+

+ {t("settings.postProcessing.api.appleIntelligence.unavailable")} +

+
+ ) : null} + {state.isAppleProvider ? (