diff --git a/apps/native/src-tauri/src/managed_edits/homebrew_adopt.rs b/apps/native/src-tauri/src/managed_edits/homebrew_adopt.rs index e6a8b5d98..8ba35ba45 100644 --- a/apps/native/src-tauri/src/managed_edits/homebrew_adopt.rs +++ b/apps/native/src-tauri/src/managed_edits/homebrew_adopt.rs @@ -31,7 +31,7 @@ const NIX_EVAL_HOMEBREW_APPLY: &str = r#"cfg: { }"#; const NIX_EVAL_HOMEBREW_ATTR_TEMPLATE: &str = - r#".#darwinConfigurations."{hostname}".config.homebrew"#; + r#".#darwinConfigurations.{hostname}.config.homebrew"#; /// Checks if Homebrew is installed by trying to run `brew --version`. fn is_homebrew_installed() -> bool { @@ -845,6 +845,17 @@ mod tests { std::fs::write(path, content).expect("failed to write test file"); } + #[test] + fn nix_eval_homebrew_attr_quotes_hostname_once() { + let safe_host_attr = serde_json::to_string("Coopers-MacBook-Pro").unwrap(); + let attr = NIX_EVAL_HOMEBREW_ATTR_TEMPLATE.replace("{hostname}", &safe_host_attr); + + assert_eq!( + attr, + r#".#darwinConfigurations."Coopers-MacBook-Pro".config.homebrew"# + ); + } + #[test] #[ignore = "Runs against the local system; enable explicitly when debugging the nix eval homebrew."] #[cfg(target_os = "macos")] diff --git a/apps/native/src/components/widget/onboarding/lib/inference.ts b/apps/native/src/components/widget/onboarding/lib/inference.ts index d7da2e50e..426d71729 100644 --- a/apps/native/src/components/widget/onboarding/lib/inference.ts +++ b/apps/native/src/components/widget/onboarding/lib/inference.ts @@ -15,6 +15,16 @@ interface ApiKeyProviderLike { }; } +interface InferenceProvider { + id: "openrouter" | "openai"; + name: string; + defaultModel: string; + prefsKeyField: "openrouterApiKey" | "openaiApiKey"; + keyPrefix: string; + keyPlaceholder: string; + docsHint: string; +} + /** * Bring-your-own-key providers, aligned to what the native backend actually * supports (OpenRouter + OpenAI direct). Keys persist to UiPrefs and the diff --git a/apps/native/src/components/widget/onboarding/onboarding-step-content.tsx b/apps/native/src/components/widget/onboarding/onboarding-step-content.tsx index 7cf305fd0..1572f6b5c 100644 --- a/apps/native/src/components/widget/onboarding/onboarding-step-content.tsx +++ b/apps/native/src/components/widget/onboarding/onboarding-step-content.tsx @@ -7,6 +7,7 @@ import { NixSetupStep } from "@/components/widget/onboarding/steps/nix-setup-ste import { PermissionsStep } from "@/components/widget/onboarding/steps/permissions-step"; import { SetupStep } from "@/components/widget/onboarding/steps/setup-step"; import { useOnboardingProgress } from "@/hooks/use-onboarding-progress"; +import { hasConfiguredInference } from "@/lib/providers/ai-provider-validation"; import { onboardingActions, useOnboarding, useViewModel } from "@nixmac/state"; interface OnboardingStepContentProps { @@ -17,12 +18,11 @@ interface OnboardingStepContentProps { export function OnboardingStepContent({ currentStep, title }: OnboardingStepContentProps) { const trackedCustomizations = useOnboarding((s) => s.trackedCustomizations); const trackedCustomizationSources = useOnboarding((s) => s.trackedCustomizationSources); - // Inference readiness is a durable fact: provider + model are persisted to - // GlobalPreferences by InferenceSetup, and the login decision is recorded - // separately. The build step only needs to know inference is configured. + // Inference readiness is a durable fact: provider/model preferences are + // persisted by InferenceSetup, and the login decision is recorded separately. const evolveProvider = useViewModel((s) => s.preferences?.evolveProvider ?? null); const evolveModel = useViewModel((s) => s.preferences?.evolveModel ?? null); - const hasInference = Boolean(evolveProvider) && Boolean(evolveModel); + const hasInference = hasConfiguredInference(evolveProvider, evolveModel); const { markMacScanned, markLoginDecided } = useOnboardingProgress(); return ( diff --git a/apps/native/src/components/widget/onboarding/use-onboarding-flow.ts b/apps/native/src/components/widget/onboarding/use-onboarding-flow.ts index b396f38c0..08a285ca8 100644 --- a/apps/native/src/components/widget/onboarding/use-onboarding-flow.ts +++ b/apps/native/src/components/widget/onboarding/use-onboarding-flow.ts @@ -6,6 +6,7 @@ import { type StepId, } from "@/components/widget/onboarding/lib/onboarding"; import { settings } from "@/lib/env"; +import { hasConfiguredInference } from "@/lib/providers/ai-provider-validation"; import { onboardingActions, useOnboarding, useViewModel } from "@nixmac/state"; import { useCallback, useEffect, useMemo, useRef } from "react"; @@ -54,7 +55,7 @@ export function useOnboardingFlow(): { flakeReady, macScanned: macScannedAt !== null, loginDecided, - hasInference: Boolean(evolveProvider) && Boolean(evolveModel), + hasInference: hasConfiguredInference(evolveProvider, evolveModel), buildComplete: lastBuildAt !== null, inferenceDeferred, }), diff --git a/apps/native/src/components/widget/settings/ai-models-tab.tsx b/apps/native/src/components/widget/settings/ai-models-tab.tsx index cf9ba2a0b..19f6b096e 100644 --- a/apps/native/src/components/widget/settings/ai-models-tab.tsx +++ b/apps/native/src/components/widget/settings/ai-models-tab.tsx @@ -31,12 +31,6 @@ interface AiModelsTabProps { form: ReactFormExtendedApi; } -const CLI_PROVIDERS = [ - { value: "claude", label: "Claude CLI" }, - { value: "codex", label: "Codex CLI" }, - { value: "opencode", label: "OpenCode CLI" }, -] as const; - function isPlainInputCliProvider(provider: string): boolean { return provider === "claude" || provider === "codex"; } diff --git a/apps/native/src/lib/providers/ai-provider-validation.test.ts b/apps/native/src/lib/providers/ai-provider-validation.test.ts index f275e47ee..a8ded5206 100644 --- a/apps/native/src/lib/providers/ai-provider-validation.test.ts +++ b/apps/native/src/lib/providers/ai-provider-validation.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { getProviderConfigInvalidReason, + hasConfiguredInference, resolveOpenAiCompatibleProvider, } from "./ai-provider-validation"; @@ -124,3 +125,17 @@ describe("getProviderConfigInvalidReason", () => { ).toBe("No model set"); }); }); + +describe("hasConfiguredInference", () => { + it("accepts CLI providers without a model so onboarding can finish with CLI defaults", () => { + expect(hasConfiguredInference("claude", "")).toBe(true); + expect(hasConfiguredInference("codex", null)).toBe(true); + expect(hasConfiguredInference("opencode", " ")).toBe(true); + }); + + it("still requires non-CLI providers to persist a model", () => { + expect(hasConfiguredInference("openrouter", "")).toBe(false); + expect(hasConfiguredInference("nixmac", "openai/gpt-4o-mini")).toBe(true); + expect(hasConfiguredInference(null, "openai/gpt-4o-mini")).toBe(false); + }); +}); diff --git a/apps/native/src/lib/providers/ai-provider-validation.ts b/apps/native/src/lib/providers/ai-provider-validation.ts index 966993f7c..a76631f0e 100644 --- a/apps/native/src/lib/providers/ai-provider-validation.ts +++ b/apps/native/src/lib/providers/ai-provider-validation.ts @@ -12,6 +12,19 @@ export function isCliProvider(provider: string): boolean { return CLI_PROVIDER_VALUES.includes(provider as (typeof CLI_PROVIDER_VALUES)[number]); } +export function hasConfiguredInference( + provider: string | null | undefined, + model: string | null | undefined, +): boolean { + if (!provider) { + return false; + } + if (isCliProvider(provider)) { + return true; + } + return hasValue(model); +} + export function resolveOpenAiCompatibleProvider( provider: string | null | undefined, prefs: Pick,