diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml
index c26130a1..562a2fdf 100644
--- a/.github/workflows/release-windows.yml
+++ b/.github/workflows/release-windows.yml
@@ -110,6 +110,16 @@ jobs:
$ErrorActionPreference = 'Stop'
$shouldPublish = ($env:GITHUB_EVENT_NAME -eq 'push') -or `
($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:PUBLISH_INPUT -eq 'true')
+ # electron-builder auto-detects prerelease from semver suffix in electron/package.json,
+ # but EP_PRE_RELEASE forces the GitHub Releases publisher to mark it Pre-release even
+ # when the runner's environment differs from local. Set it whenever the version has a "-" suffix.
+ $version = (Get-Content electron/package.json | ConvertFrom-Json).version
+ if ($version -match '-') {
+ $env:EP_PRE_RELEASE = 'true'
+ Write-Host "Version $version is EXPERIMENTAL; setting EP_PRE_RELEASE=true"
+ } else {
+ Write-Host "Version $version is STABLE"
+ }
if ($shouldPublish) {
Write-Host "Build mode: PUBLISH"
pwsh -NoProfile -File scripts\build-app-win.ps1 -Publish
diff --git a/backend/apps/settings/models.py b/backend/apps/settings/models.py
index 906bda42..747fa225 100644
--- a/backend/apps/settings/models.py
+++ b/backend/apps/settings/models.py
@@ -63,7 +63,8 @@ class AppSettings(BaseModel):
expand_new_chats_in_dashboard: bool = False
auto_reveal_sub_agents: bool = True
dev_mode: bool = False
- # Subscription tokens (from CLI tools — alternative to API keys)
+ allow_experimental_updates: bool = False
+ # Subscription tokens (from CLI tools, alternative to API keys)
claude_subscription_token: Optional[str] = None
openai_subscription_token: Optional[str] = None
gemini_subscription_token: Optional[str] = None
diff --git a/electron/main.js b/electron/main.js
index 6101908b..81a1d92b 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -719,6 +719,8 @@ function setupAutoUpdater() {
// running .app / locked .exe), so an active session is never disrupted.
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
+ // Renderer pushes the user's experimental-updates setting via IPC right after settings load.
+ autoUpdater.allowPrerelease = false;
autoUpdater.on('update-available', (info) => {
console.log(`Update available: ${info.version}`);
@@ -1280,6 +1282,20 @@ ipcMain.handle('download-update', async () => {
}
});
+ipcMain.handle('set-allow-prerelease', async (_e, value) => {
+ if (!autoUpdater) return { success: false, error: 'Updater not available' };
+ const next = Boolean(value);
+ if (autoUpdater.allowPrerelease === next) return { success: true, changed: false };
+ autoUpdater.allowPrerelease = next;
+ if (!isPackaged) return { success: true, changed: true };
+ try {
+ await autoUpdater.checkForUpdates();
+ } catch (err) {
+ return { success: false, changed: true, error: err?.message || String(err) };
+ }
+ return { success: true, changed: true };
+});
+
ipcMain.handle('install-update', () => {
if (!autoUpdater) return;
autoUpdater.quitAndInstall(false, true);
diff --git a/electron/preload.js b/electron/preload.js
index a92abb21..29b87f41 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -37,6 +37,7 @@ const { contextBridge, ipcRenderer } = require('electron');
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
+ setAllowPrerelease: (value) => ipcRenderer.invoke('set-allow-prerelease', value),
onUpdateAvailable: (cb) => {
const listener = (_event, info) => cb(info);
diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx
index e5454440..ab05e580 100644
--- a/frontend/src/app/Main.tsx
+++ b/frontend/src/app/Main.tsx
@@ -230,6 +230,7 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
const { setMode: setThemeMode } = useThemeMode();
const theme = useAppSelector((s) => s.settings.data.theme);
const loaded = useAppSelector((s) => s.settings.loaded);
+ const allowExperimentalUpdates = useAppSelector((s) => s.settings.data.allow_experimental_updates);
useEffect(() => {
dispatch(fetchSettings());
dispatch(fetchModels());
@@ -259,6 +260,11 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
useEffect(() => {
if (loaded) setThemeMode(theme as 'light' | 'dark');
}, [loaded, theme, setThemeMode]);
+
+ useEffect(() => {
+ if (!loaded) return;
+ (window as any).openswarm?.setAllowPrerelease?.(allowExperimentalUpdates);
+ }, [loaded, allowExperimentalUpdates]);
return <>{children}>;
};
diff --git a/frontend/src/app/pages/Settings/Settings.tsx b/frontend/src/app/pages/Settings/Settings.tsx
index d6d72ff0..64cf30a2 100644
--- a/frontend/src/app/pages/Settings/Settings.tsx
+++ b/frontend/src/app/pages/Settings/Settings.tsx
@@ -2052,7 +2052,7 @@ const Settings: React.FC = () => {
{/* ── Advanced ── */}
Advanced
-
+
Developer mode
Show transport details, environment variables, raw configs, and other technical metadata throughout the app.
@@ -2067,7 +2067,22 @@ const Settings: React.FC = () => {
/>
- {/* ── About ── */}
+
+
+ Experimental updates
+ Receive pre-release builds with new features earlier. These versions may be less stable than normal releases.
+
+ setForm({ ...form, allow_experimental_updates: e.target.checked })}
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
+ }}
+ />
+
+
+ {/* About */}
About
diff --git a/frontend/src/shared/state/settingsSlice.ts b/frontend/src/shared/state/settingsSlice.ts
index f2453a0d..e56a4790 100644
--- a/frontend/src/shared/state/settingsSlice.ts
+++ b/frontend/src/shared/state/settingsSlice.ts
@@ -54,6 +54,7 @@ export interface AppSettings {
expand_new_chats_in_dashboard: boolean;
auto_reveal_sub_agents: boolean;
dev_mode: boolean;
+ allow_experimental_updates: boolean;
// Optional managed-subscription state (surfaces only when user has
// subscribed via the cloud). Mirrors AppSettings on the backend.
connection_mode?: 'own_key' | 'openswarm-pro';
@@ -129,6 +130,7 @@ const initialState: SettingsState = {
expand_new_chats_in_dashboard: false,
auto_reveal_sub_agents: true,
dev_mode: false,
+ allow_experimental_updates: false,
},
loading: false,
loaded: false,
diff --git a/publish.sh b/publish.sh
index fccaa935..c5727222 100755
--- a/publish.sh
+++ b/publish.sh
@@ -1,5 +1,19 @@
#!/bin/bash
# The comment above is shebang, DO NOT REMOVE
+#
+# Publishes a macOS build to GitHub Releases. Channel is auto-detected from
+# the semver string in electron/package.json:
+#
+# Stable: "1.0.37" -> normal GitHub release
+# Experimental: "1.0.37-exp.1" -> marked Pre-release on GitHub; only reaches
+# users with "Experimental updates" enabled
+# in Settings > Advanced.
+#
+# To promote an experimental release to stable: un-check "This is a pre-release"
+# on the GitHub release page (native GitHub UI, no code change needed).
+#
+# Windows release: tag-triggered via .github/workflows/release-windows.yml,
+# which performs the same auto-detection.
PUBLISH_ABSPATH="$(readlink -f "${BASH_SOURCE[0]}")"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' 's/\r//g' "$PUBLISH_ABSPATH"
@@ -11,7 +25,18 @@ chmod +x "$PUBLISH_ABSPATH"
PROJECT_ROOT="$(dirname "$PUBLISH_ABSPATH")"
cd "$PROJECT_ROOT"
-echo "Building and deploying to Firebase Hosting..."
+# electron-builder auto-detects prerelease from semver suffix in electron/package.json
+# (e.g. "1.0.37-exp.1" publishes as GitHub Pre-release; "1.0.37" publishes as stable).
+# We also export EP_PRE_RELEASE for belt-and-suspenders so the GitHub Releases publisher
+# can't accidentally promote an experimental build.
+VERSION="$(node -p "require('./electron/package.json').version")"
+if [[ "$VERSION" == *-* ]]; then
+ export EP_PRE_RELEASE=true
+ echo "==> Publishing EXPERIMENTAL release: v$VERSION (will be marked Pre-release on GitHub)"
+else
+ echo "==> Publishing STABLE release: v$VERSION"
+fi
+
bash scripts/build-app.sh --publish
-cd -
\ No newline at end of file
+cd -