diff --git a/backend/apps/nine_router.py b/backend/apps/nine_router.py index a6ba9534..584f1aae 100644 --- a/backend/apps/nine_router.py +++ b/backend/apps/nine_router.py @@ -60,19 +60,17 @@ def _find_9router_dir() -> str | None: def _find_node() -> str | None: """Find a Node.js binary (works in both dev and packaged mode).""" - _is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" + # Check system node first + node = shutil.which("node") + if node: + return node - if _is_packaged: - # Electron bundles Node.js — find its binary - # Electron's node is the electron binary itself with ELECTRON_RUN_AS_NODE=1 - # But we can also check for system node - node = shutil.which("node") - if node: - return node - # Electron's own node can be used with ELECTRON_RUN_AS_NODE - return None - else: - return shutil.which("node") + # In packaged Electron app, use the Electron binary with ELECTRON_RUN_AS_NODE=1 + electron_path = os.environ.get("OPENSWARM_ELECTRON_PATH") + if electron_path and os.path.exists(electron_path): + return electron_path + + return None async def ensure_running(): @@ -101,6 +99,9 @@ async def ensure_running(): cmd = [node, standalone_server] cwd = os.path.join(_9router_dir, ".next", "standalone") env = {**os.environ, "PORT": str(NINE_ROUTER_PORT), "NODE_ENV": "production"} + # If using Electron binary as node, enable ELECTRON_RUN_AS_NODE + if node == os.environ.get("OPENSWARM_ELECTRON_PATH"): + env["ELECTRON_RUN_AS_NODE"] = "1" elif _9router_dir: # Dev mode with bundled 9Router — use next dev diff --git a/electron/main.js b/electron/main.js index c209718e..fc30398d 100644 --- a/electron/main.js +++ b/electron/main.js @@ -152,6 +152,7 @@ async function startBackend() { PATH: shellPath, OPENSWARM_PACKAGED: isPackaged ? '1' : '0', OPENSWARM_PORT: String(backendPort), + OPENSWARM_ELECTRON_PATH: process.execPath, PYTHONDONTWRITEBYTECODE: '1', }; diff --git a/frontend/src/app/components/OnboardingModal.tsx b/frontend/src/app/components/OnboardingModal.tsx index efc2c9f0..bcb734b1 100644 --- a/frontend/src/app/components/OnboardingModal.tsx +++ b/frontend/src/app/components/OnboardingModal.tsx @@ -15,7 +15,6 @@ const OnboardingModal: React.FC = () => { const c = useClaudeTokens(); const settings = useAppSelector((s) => s.settings); const [open, setOpen] = useState(false); - const [dismissed, setDismissed] = useState(false); const [connecting, setConnecting] = useState(null); const [nineRouterStatus, setNineRouterStatus] = useState(null); @@ -32,7 +31,7 @@ const OnboardingModal: React.FC = () => { fetch(`${API_BASE}/agents/subscriptions/status`) .then((r) => r.json()) .then(setNineRouterStatus) - .catch(() => setNineRouterStatus(null)); + .catch(() => setNineRouterStatus({ running: false, providers: [], models: [] })); }, []); const hasSubscription = (() => { @@ -41,14 +40,17 @@ const OnboardingModal: React.FC = () => { return connections.some((p: any) => p.isActive); })(); - // Show modal if no keys AND no subscriptions AND not dismissed + // Show once: if no subscription connected AND not previously dismissed (persisted in localStorage) useEffect(() => { - if (!hasAnyKey && !hasSubscription && !dismissed && nineRouterStatus !== null) { + const alreadySeen = localStorage.getItem('openswarm_onboarding_seen'); + if (alreadySeen === 'true') return; + if (nineRouterStatus === null) return; // still loading + + // Show if no subscription, regardless of API key status + if (!hasSubscription) { setOpen(true); - } else { - setOpen(false); } - }, [hasAnyKey, hasSubscription, dismissed, nineRouterStatus]); + }, [hasSubscription, nineRouterStatus]); const handleConnect = async (providerId: string) => { setConnecting(providerId); @@ -75,7 +77,7 @@ const OnboardingModal: React.FC = () => { if (pd.success) { clearInterval(timer); setConnecting(null); - setOpen(false); + dismiss(); } } catch {} }, 5000); @@ -101,7 +103,7 @@ const OnboardingModal: React.FC = () => { }); } catch {} setConnecting(null); - setOpen(false); + dismiss(); } }; window.addEventListener('message', msgHandler); @@ -115,7 +117,7 @@ const OnboardingModal: React.FC = () => { clearInterval(statusPoller); window.removeEventListener('message', msgHandler); setConnecting(null); - setOpen(false); + dismiss(); } } catch {} }, 2000); @@ -126,15 +128,13 @@ const OnboardingModal: React.FC = () => { } }; - const handleApiKey = () => { - setDismissed(true); + const dismiss = () => { + localStorage.setItem('openswarm_onboarding_seen', 'true'); setOpen(false); }; - const handleSkip = () => { - setDismissed(true); - setOpen(false); - }; + const handleApiKey = () => dismiss(); + const handleSkip = () => dismiss(); if (!open) return null;