From 3583b5780914ec42f4b55490abc32af842c60f0d Mon Sep 17 00:00:00 2001 From: theblondealex Date: Tue, 7 Apr 2026 00:39:17 +0100 Subject: [PATCH 1/3] feat: add update channel selector to settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "Update Access" dropdown to Settings > Appearance that lets users opt into Early Access (pre-release) builds. Mirrors Cursor's update access UI pattern with "Default" and "Early Access" options. - UpdateManager.init() accepts allowPrerelease, applied to autoUpdater - New setAllowPrerelease() method updates the flag live and re-checks - IPC handler update:setChannel wires renderer → main - Shell preload exposes updateSetChannel() - Settings AppearancePane reads/writes updateChannel pref and calls updateSetChannel to apply immediately without a restart --- collab-electron/src/main/index.ts | 1 + .../src/main/updater/update-manager.ts | 20 ++++++++- collab-electron/src/preload/shell.ts | 1 + .../src/windows/settings/src/App.tsx | 43 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/collab-electron/src/main/index.ts b/collab-electron/src/main/index.ts index 36bb78ef..2aeb6bb5 100644 --- a/collab-electron/src/main/index.ts +++ b/collab-electron/src/main/index.ts @@ -813,6 +813,7 @@ app.whenReady().then(async () => { setupUpdateIPC(); updateManager.init({ onBeforeQuit: () => shutdownBackgroundServices(), + allowPrerelease: getPref(config, "updateChannel") === "early-access", }); try { diff --git a/collab-electron/src/main/updater/update-manager.ts b/collab-electron/src/main/updater/update-manager.ts index 4e42ff7e..a69bdcd4 100644 --- a/collab-electron/src/main/updater/update-manager.ts +++ b/collab-electron/src/main/updater/update-manager.ts @@ -52,6 +52,7 @@ class UpdateManager { private errorResetTimeout: NodeJS.Timeout | null = null; private checkInterval: NodeJS.Timeout | null = null; private onBeforeQuit: (() => Promise) | null = null; + private allowPrerelease = false; private shouldIgnoreMissingReleaseMetadataError(message: string): boolean { if (!isMissingReleaseMetadataError(message)) { @@ -61,12 +62,14 @@ class UpdateManager { return true; } - init(opts?: { onBeforeQuit?: () => Promise }): void { + init(opts?: { onBeforeQuit?: () => Promise; allowPrerelease?: boolean }): void { if (this.initialized) return; this.onBeforeQuit = opts?.onBeforeQuit ?? null; + this.allowPrerelease = opts?.allowPrerelease ?? false; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.allowPrerelease = this.allowPrerelease; // Per-platform/arch update channels so each build gets its own yml file. // Mac: latest-arm64-mac.yml / latest-x64-mac.yml @@ -212,6 +215,16 @@ class UpdateManager { autoUpdater.quitAndInstall(); } + setAllowPrerelease(allow: boolean): void { + this.allowPrerelease = allow; + autoUpdater.allowPrerelease = allow; + // Reset stale update state so the new channel is evaluated fresh. + if (this.state.status === "available" || this.state.status === "error") { + this.setState({ status: "idle", error: undefined, version: undefined }); + } + void this.checkForUpdates(); + } + getState(): UpdateState { return { ...this.state }; } @@ -278,4 +291,9 @@ export function setupUpdateIPC(): void { ipcMain.on("update:install", async () => { await updateManager.install(); }); + + ipcMain.handle("update:setChannel", (_event, channel: string) => { + updateManager.setAllowPrerelease(channel === "early-access"); + return updateManager.getState(); + }); } diff --git a/collab-electron/src/preload/shell.ts b/collab-electron/src/preload/shell.ts index 190a7700..585d18fc 100644 --- a/collab-electron/src/preload/shell.ts +++ b/collab-electron/src/preload/shell.ts @@ -142,6 +142,7 @@ contextBridge.exposeInMainWorld("shellApi", { updateCheck: () => ipcRenderer.invoke("update:check"), updateDownload: () => ipcRenderer.invoke("update:download"), updateInstall: () => ipcRenderer.send("update:install"), + updateSetChannel: (channel: string) => ipcRenderer.invoke("update:setChannel", channel), onUpdateStatus: (cb: (state: unknown) => void) => { const handler = (_event: unknown, state: unknown) => cb(state); ipcRenderer.on("update:status", handler); diff --git a/collab-electron/src/windows/settings/src/App.tsx b/collab-electron/src/windows/settings/src/App.tsx index 41b81c0c..7d03676b 100644 --- a/collab-electron/src/windows/settings/src/App.tsx +++ b/collab-electron/src/windows/settings/src/App.tsx @@ -25,6 +25,7 @@ interface SettingsApi { getAgents: () => Promise; installSkill: (agentId: string) => Promise<{ ok: boolean }>; uninstallSkill: (agentId: string) => Promise<{ ok: boolean }>; + updateSetChannel: (channel: string) => Promise; close: () => void; } @@ -175,9 +176,12 @@ function ThemeToggle({ ); } +type UpdateChannel = "default" | "early-access"; + function AppearancePane() { const [theme, setTheme] = useState("system"); const [canvasOpacity, setCanvasOpacity] = useState(0); + const [updateChannel, setUpdateChannel] = useState("default"); useEffect(() => { api.getPref("theme") @@ -191,6 +195,12 @@ function AppearancePane() { if (typeof v === "number") setCanvasOpacity(v); }) .catch(() => { }); + api.getPref("updateChannel") + .then((v) => { + if (v === "early-access") setUpdateChannel("early-access"); + else setUpdateChannel("default"); + }) + .catch(() => { }); }, []); async function handleThemeChange(mode: ThemeMode) { @@ -203,6 +213,12 @@ function AppearancePane() { await api.setPref("canvasOpacity", value); } + async function handleUpdateChannelChange(channel: UpdateChannel) { + setUpdateChannel(channel); + await api.setPref("updateChannel", channel); + await api.updateSetChannel(channel); + } + return (
@@ -232,6 +248,33 @@ function AppearancePane() { onChange={(v) => { void handleOpacityChange(v); }} />
+ +
+
+

Update Access

+

+ By default, get notified for stable updates only. Early Access + builds may be unstable for production work. +

+
+ +
); } From 4fad873101ba27b1a0070f23dc3dc7b0106c1300 Mon Sep 17 00:00:00 2001 From: theblondealex Date: Tue, 7 Apr 2026 00:39:17 +0100 Subject: [PATCH 2/3] feat: add update channel selector to settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "Update Access" dropdown to Settings > Appearance that lets users opt into Early Access (pre-release) builds. Mirrors Cursor's update access UI pattern with "Default" and "Early Access" options. - UpdateManager.init() accepts allowPrerelease, applied to autoUpdater - New setAllowPrerelease() method updates the flag live and re-checks - IPC handler update:setChannel wires renderer → main - Shell preload exposes updateSetChannel() - Settings AppearancePane reads/writes updateChannel pref and calls updateSetChannel to apply immediately without a restart --- collab-electron/src/main/index.ts | 1 + .../src/main/updater/update-manager.ts | 20 ++++++++- collab-electron/src/preload/shell.ts | 1 + .../src/windows/settings/src/App.tsx | 43 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/collab-electron/src/main/index.ts b/collab-electron/src/main/index.ts index 19e7d150..b0be1fbb 100644 --- a/collab-electron/src/main/index.ts +++ b/collab-electron/src/main/index.ts @@ -818,6 +818,7 @@ app.whenReady().then(async () => { setupUpdateIPC(); updateManager.init({ onBeforeQuit: () => shutdownBackgroundServices(), + allowPrerelease: getPref(config, "updateChannel") === "early-access", }); try { diff --git a/collab-electron/src/main/updater/update-manager.ts b/collab-electron/src/main/updater/update-manager.ts index 4e42ff7e..a69bdcd4 100644 --- a/collab-electron/src/main/updater/update-manager.ts +++ b/collab-electron/src/main/updater/update-manager.ts @@ -52,6 +52,7 @@ class UpdateManager { private errorResetTimeout: NodeJS.Timeout | null = null; private checkInterval: NodeJS.Timeout | null = null; private onBeforeQuit: (() => Promise) | null = null; + private allowPrerelease = false; private shouldIgnoreMissingReleaseMetadataError(message: string): boolean { if (!isMissingReleaseMetadataError(message)) { @@ -61,12 +62,14 @@ class UpdateManager { return true; } - init(opts?: { onBeforeQuit?: () => Promise }): void { + init(opts?: { onBeforeQuit?: () => Promise; allowPrerelease?: boolean }): void { if (this.initialized) return; this.onBeforeQuit = opts?.onBeforeQuit ?? null; + this.allowPrerelease = opts?.allowPrerelease ?? false; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.allowPrerelease = this.allowPrerelease; // Per-platform/arch update channels so each build gets its own yml file. // Mac: latest-arm64-mac.yml / latest-x64-mac.yml @@ -212,6 +215,16 @@ class UpdateManager { autoUpdater.quitAndInstall(); } + setAllowPrerelease(allow: boolean): void { + this.allowPrerelease = allow; + autoUpdater.allowPrerelease = allow; + // Reset stale update state so the new channel is evaluated fresh. + if (this.state.status === "available" || this.state.status === "error") { + this.setState({ status: "idle", error: undefined, version: undefined }); + } + void this.checkForUpdates(); + } + getState(): UpdateState { return { ...this.state }; } @@ -278,4 +291,9 @@ export function setupUpdateIPC(): void { ipcMain.on("update:install", async () => { await updateManager.install(); }); + + ipcMain.handle("update:setChannel", (_event, channel: string) => { + updateManager.setAllowPrerelease(channel === "early-access"); + return updateManager.getState(); + }); } diff --git a/collab-electron/src/preload/shell.ts b/collab-electron/src/preload/shell.ts index 2b78d32a..ef73b4ce 100644 --- a/collab-electron/src/preload/shell.ts +++ b/collab-electron/src/preload/shell.ts @@ -142,6 +142,7 @@ contextBridge.exposeInMainWorld("shellApi", { updateCheck: () => ipcRenderer.invoke("update:check"), updateDownload: () => ipcRenderer.invoke("update:download"), updateInstall: () => ipcRenderer.send("update:install"), + updateSetChannel: (channel: string) => ipcRenderer.invoke("update:setChannel", channel), onUpdateStatus: (cb: (state: unknown) => void) => { const handler = (_event: unknown, state: unknown) => cb(state); ipcRenderer.on("update:status", handler); diff --git a/collab-electron/src/windows/settings/src/App.tsx b/collab-electron/src/windows/settings/src/App.tsx index 41b81c0c..7d03676b 100644 --- a/collab-electron/src/windows/settings/src/App.tsx +++ b/collab-electron/src/windows/settings/src/App.tsx @@ -25,6 +25,7 @@ interface SettingsApi { getAgents: () => Promise; installSkill: (agentId: string) => Promise<{ ok: boolean }>; uninstallSkill: (agentId: string) => Promise<{ ok: boolean }>; + updateSetChannel: (channel: string) => Promise; close: () => void; } @@ -175,9 +176,12 @@ function ThemeToggle({ ); } +type UpdateChannel = "default" | "early-access"; + function AppearancePane() { const [theme, setTheme] = useState("system"); const [canvasOpacity, setCanvasOpacity] = useState(0); + const [updateChannel, setUpdateChannel] = useState("default"); useEffect(() => { api.getPref("theme") @@ -191,6 +195,12 @@ function AppearancePane() { if (typeof v === "number") setCanvasOpacity(v); }) .catch(() => { }); + api.getPref("updateChannel") + .then((v) => { + if (v === "early-access") setUpdateChannel("early-access"); + else setUpdateChannel("default"); + }) + .catch(() => { }); }, []); async function handleThemeChange(mode: ThemeMode) { @@ -203,6 +213,12 @@ function AppearancePane() { await api.setPref("canvasOpacity", value); } + async function handleUpdateChannelChange(channel: UpdateChannel) { + setUpdateChannel(channel); + await api.setPref("updateChannel", channel); + await api.updateSetChannel(channel); + } + return (
@@ -232,6 +248,33 @@ function AppearancePane() { onChange={(v) => { void handleOpacityChange(v); }} />
+ +
+
+

Update Access

+

+ By default, get notified for stable updates only. Early Access + builds may be unstable for production work. +

+
+ +
); } From 882c2b59d67dd55383d1fedabe4996afd846d661 Mon Sep 17 00:00:00 2001 From: Yiliu Shen-Burke Date: Mon, 13 Apr 2026 20:58:10 -0700 Subject: [PATCH 3/3] Update App.tsx --- .../src/windows/settings/src/App.tsx | 104 +++++++++++------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/collab-electron/src/windows/settings/src/App.tsx b/collab-electron/src/windows/settings/src/App.tsx index 7d03676b..6fefac06 100644 --- a/collab-electron/src/windows/settings/src/App.tsx +++ b/collab-electron/src/windows/settings/src/App.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { + Faders, GearSix, Keyboard, Palette, @@ -178,10 +179,66 @@ function ThemeToggle({ type UpdateChannel = "default" | "early-access"; +function GeneralPane() { + const [updateChannel, setUpdateChannel] = useState("default"); + + useEffect(() => { + api.getPref("updateChannel") + .then((v) => { + if (v === "early-access") setUpdateChannel("early-access"); + else setUpdateChannel("default"); + }) + .catch(() => { }); + }, []); + + async function handleUpdateChannelChange(channel: UpdateChannel) { + setUpdateChannel(channel); + await api.setPref("updateChannel", channel); + await api.updateSetChannel(channel); + } + + return ( +
+
+

General

+

+ App-wide preferences. +

+
+ +
+
+

Update Channel

+

+ By default, get notified for stable updates only. Early Access + builds may be unstable for production work. +

+
+ +
+
+ ); +} + function AppearancePane() { const [theme, setTheme] = useState("system"); const [canvasOpacity, setCanvasOpacity] = useState(0); - const [updateChannel, setUpdateChannel] = useState("default"); useEffect(() => { api.getPref("theme") @@ -195,12 +252,6 @@ function AppearancePane() { if (typeof v === "number") setCanvasOpacity(v); }) .catch(() => { }); - api.getPref("updateChannel") - .then((v) => { - if (v === "early-access") setUpdateChannel("early-access"); - else setUpdateChannel("default"); - }) - .catch(() => { }); }, []); async function handleThemeChange(mode: ThemeMode) { @@ -213,12 +264,6 @@ function AppearancePane() { await api.setPref("canvasOpacity", value); } - async function handleUpdateChannelChange(channel: UpdateChannel) { - setUpdateChannel(channel); - await api.setPref("updateChannel", channel); - await api.updateSetChannel(channel); - } - return (
@@ -248,33 +293,6 @@ function AppearancePane() { onChange={(v) => { void handleOpacityChange(v); }} />
- -
-
-

Update Access

-

- By default, get notified for stable updates only. Early Access - builds may be unstable for production work. -

-
- -
); } @@ -633,13 +651,14 @@ function IntegrationsPane() { ); } -type Pane = "appearance" | "terminal" | "integrations" | "controls"; +type Pane = "general" | "appearance" | "terminal" | "integrations" | "controls"; const NAV_ITEMS: { id: Pane; label: string; icon: typeof Palette; }[] = [ + { id: "general", label: "General", icon: Faders }, { id: "appearance", label: "Appearance", icon: Palette }, { id: "terminal", label: "Terminal", icon: Terminal }, { id: "integrations", label: "Integrations", icon: PuzzlePiece }, @@ -681,7 +700,7 @@ function CloseButton({ onClick }: { onClick: () => void }) { export default function App() { const [activePane, setActivePane] = - useState("appearance"); + useState("general"); const [appVersion, setAppVersion] = useState(""); const paneRef = useRef(null); @@ -764,6 +783,7 @@ export default function App() { {/* Content */}
+ {activePane === "general" && } {activePane === "appearance" && } {activePane === "terminal" && } {activePane === "integrations" && }