Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions collab-electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ app.whenReady().then(async () => {
setupUpdateIPC();
updateManager.init({
onBeforeQuit: () => shutdownBackgroundServices(),
allowPrerelease: getPref(config, "updateChannel") === "early-access",
});

try {
Expand Down
20 changes: 19 additions & 1 deletion collab-electron/src/main/updater/update-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class UpdateManager {
private errorResetTimeout: NodeJS.Timeout | null = null;
private checkInterval: NodeJS.Timeout | null = null;
private onBeforeQuit: (() => Promise<void>) | null = null;
private allowPrerelease = false;

private shouldIgnoreMissingReleaseMetadataError(message: string): boolean {
if (!isMissingReleaseMetadataError(message)) {
Expand All @@ -61,12 +62,14 @@ class UpdateManager {
return true;
}

init(opts?: { onBeforeQuit?: () => Promise<void> }): void {
init(opts?: { onBeforeQuit?: () => Promise<void>; 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
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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();
});
}
1 change: 1 addition & 0 deletions collab-electron/src/preload/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
67 changes: 65 additions & 2 deletions collab-electron/src/windows/settings/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
Faders,
GearSix,
Keyboard,
Palette,
Expand All @@ -25,6 +26,7 @@ interface SettingsApi {
getAgents: () => Promise<AgentStatus[]>;
installSkill: (agentId: string) => Promise<{ ok: boolean }>;
uninstallSkill: (agentId: string) => Promise<{ ok: boolean }>;
updateSetChannel: (channel: string) => Promise<unknown>;
close: () => void;
}

Expand Down Expand Up @@ -175,6 +177,65 @@ function ThemeToggle({
);
}

type UpdateChannel = "default" | "early-access";

function GeneralPane() {
const [updateChannel, setUpdateChannel] = useState<UpdateChannel>("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 (
<div className="space-y-6 p-6">
<div className="space-y-1">
<h2 className="text-base font-semibold">General</h2>
<p className="text-sm text-muted-foreground">
App-wide preferences.
</p>
</div>

<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<p className="text-sm font-medium">Update Channel</p>
<p className="text-xs text-muted-foreground" style={{ maxWidth: 240 }}>
By default, get notified for stable updates only. Early Access
builds may be unstable for production work.
</p>
</div>
<select
value={updateChannel}
onChange={(e) => { void handleUpdateChannelChange(e.target.value as UpdateChannel); }}
className="shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium cursor-pointer appearance-none"
style={{
backgroundColor: "color-mix(in srgb, var(--foreground) 8%, transparent)",
color: "var(--foreground)",
border: "1px solid color-mix(in srgb, var(--foreground) 20%, transparent)",
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23888' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2 4l4 4 4-4'/%3E%3C/svg%3E")`,
backgroundRepeat: "no-repeat",
backgroundPosition: "right 8px center",
paddingRight: "28px",
}}
>
<option value="default">Default</option>
<option value="early-access">Early Access</option>
</select>
</div>
</div>
);
}

function AppearancePane() {
const [theme, setTheme] = useState<ThemeMode>("system");
const [canvasOpacity, setCanvasOpacity] = useState(0);
Expand Down Expand Up @@ -590,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 },
Expand Down Expand Up @@ -638,7 +700,7 @@ function CloseButton({ onClick }: { onClick: () => void }) {

export default function App() {
const [activePane, setActivePane] =
useState<Pane>("appearance");
useState<Pane>("general");
const [appVersion, setAppVersion] = useState("");
const paneRef =
useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -721,6 +783,7 @@ export default function App() {

{/* Content */}
<div className="flex-1 overflow-auto">
{activePane === "general" && <GeneralPane />}
{activePane === "appearance" && <AppearancePane />}
{activePane === "terminal" && <TerminalPane />}
{activePane === "integrations" && <IntegrationsPane />}
Expand Down
Loading