[eric] [eric] fix 9Router startup for users without Node.js, fix onboarding modal

- nine_router.py: use Electron binary as Node.js fallback (ELECTRON_RUN_AS_NODE=1)
  - electron/main.js: pass OPENSWARM_ELECTRON_PATH to backend
  - OnboardingModal: show once for users without subscription (persisted via localStorage)
  - settingsSlice: sync frontend default system prompt with backend
This commit is contained in:
ciregenz
2026-03-25 17:23:13 -07:00
parent d22e992148
commit dcd20eed39
3 changed files with 30 additions and 28 deletions
+13 -12
View File
@@ -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
+1
View File
@@ -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',
};
+16 -16
View File
@@ -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<string | null>(null);
const [nineRouterStatus, setNineRouterStatus] = useState<any>(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;