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
2 changes: 2 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import type { ExternalAppsPreferences } from "@posthog/workspace-server/services
import { foldersModule } from "@posthog/workspace-server/services/folders/folders.module";
import { GitService } from "@posthog/workspace-server/services/git/service";
import { TaskPrStatusService } from "@posthog/workspace-server/services/git/task-pr-status";
import { githubReleasesModule } from "@posthog/workspace-server/services/github-releases/github-releases.module";
import {
HANDOFF_GIT_GATEWAY,
HANDOFF_LOG_GATEWAY,
Expand Down Expand Up @@ -584,6 +585,7 @@ container.load(posthogPluginModule);
container.bind(MAIN_POSTHOG_PLUGIN_SERVICE).toService(POSTHOG_PLUGIN_SERVICE);
container.load(skillsModule);
container.load(skillsMarketplaceModule);
container.load(githubReleasesModule);
container.load(onboardingImportModule);
container.load(additionalDirectoriesModule);
container.bind(MAIN_SLEEP_SERVICE).to(SleepService);
Expand Down
65 changes: 60 additions & 5 deletions apps/code/src/main/platform-adapters/electron-updater.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import type { IUpdater } from "@posthog/platform/updater";
import type {
IUpdater,
UpdateAvailableInfo,
UpdateDownloadProgress,
} from "@posthog/platform/updater";
import { app } from "electron";
import log from "electron-log/main";
import { autoUpdater, type UpdateInfo } from "electron-updater";
import {
autoUpdater,
type ProgressInfo,
type UpdateInfo,
} from "electron-updater";
import { injectable } from "inversify";

function normalizeReleaseNotes(
notes: UpdateInfo["releaseNotes"],
): string | null {
if (!notes) return null;
if (typeof notes === "string") return notes;
const joined = notes
.map((n) => n.note ?? "")
.filter((n) => n.length > 0)
.join("\n\n");
return joined.length > 0 ? joined : null;
}

@injectable()
export class ElectronUpdater implements IUpdater {
constructor() {
autoUpdater.logger = log;
autoUpdater.disableDifferentialDownload = true;
// Default to manual download; the "Download updates automatically" setting
// flips this via setAutoDownload(). A downloaded update always installs on the
// next quit, with an in-app Restart button for immediate install.
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
}

public isSupported(): boolean {
Expand All @@ -20,24 +45,54 @@ export class ElectronUpdater implements IUpdater {
}

public check(): void {
void autoUpdater.checkForUpdates();
void autoUpdater.checkForUpdates().catch(() => undefined);
}

public download(): void {
void autoUpdater.downloadUpdate().catch(() => undefined);
}

public quitAndInstall(): void {
autoUpdater.quitAndInstall(false, true);
}

public setAutoDownload(enabled: boolean): void {
autoUpdater.autoDownload = enabled;
}

public onCheckStart(handler: () => void): () => void {
autoUpdater.on("checking-for-update", handler);
return () => autoUpdater.off("checking-for-update", handler);
}

public onUpdateAvailable(handler: () => void): () => void {
const l = (_info: UpdateInfo) => handler();
public onUpdateAvailable(
handler: (info: UpdateAvailableInfo) => void,
): () => void {
const l = (info: UpdateInfo) =>
handler({
version: info.version,
releaseNotes: normalizeReleaseNotes(info.releaseNotes),
releaseDate: info.releaseDate,
releaseName: info.releaseName,
});
autoUpdater.on("update-available", l);
return () => autoUpdater.off("update-available", l);
}

public onDownloadProgress(
handler: (progress: UpdateDownloadProgress) => void,
): () => void {
const l = (info: ProgressInfo) =>
handler({
percent: info.percent,
bytesPerSecond: info.bytesPerSecond,
transferred: info.transferred,
total: info.total,
});
autoUpdater.on("download-progress", l);
return () => autoUpdater.off("download-progress", l);
}

public onUpdateDownloaded(handler: (version: string) => void): () => void {
const l = (info: UpdateInfo) => handler(info.version);
autoUpdater.on("update-downloaded", l);
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/trpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { freeformGenRouter } from "@posthog/host-router/routers/freeform-gen.rou
import { fsRouter } from "@posthog/host-router/routers/fs.router";
import { gitRouter } from "@posthog/host-router/routers/git.router";
import { githubIntegrationRouter } from "@posthog/host-router/routers/github-integration.router";
import { githubReleasesRouter } from "@posthog/host-router/routers/github-releases.router";
import { handoffRouter } from "@posthog/host-router/routers/handoff.router";
import { linearIntegrationRouter } from "@posthog/host-router/routers/linear-integration.router";
import { llmGatewayRouter } from "@posthog/host-router/routers/llm-gateway.router";
Expand Down Expand Up @@ -74,6 +75,7 @@ export const trpcRouter = router({
fs: fsRouter,
git: gitRouter,
githubIntegration: githubIntegrationRouter,
githubReleases: githubReleasesRouter,
handoff: handoffRouter,
linearIntegration: linearIntegrationRouter,
llmGateway: llmGatewayRouter,
Expand Down
62 changes: 52 additions & 10 deletions apps/code/src/renderer/platform-adapters/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import {
updateStore,
} from "@posthog/core/updates/updateStore";
import { resolveService } from "@posthog/di/container";
import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore";
import {
UPDATES_CLIENT,
type UpdatesClient,
} from "@posthog/ui/features/updates/updatesClient";
import { useWhatsNewStore } from "@posthog/ui/features/updates/whatsNewStore";
import { toast } from "@posthog/ui/primitives/toast";
import { logger } from "@posthog/ui/shell/logger";
import { hostTrpcClient } from "@renderer/trpc/client";

const log = logger.scope("updates-host");

Expand Down Expand Up @@ -44,11 +47,8 @@ void client
.getStatus()
.then((status) => {
const update = deriveUpdateUiStatus(status, store().status);
if (update?.status) {
store().setStatus(update.status);
}
if (update && "version" in update) {
store().setVersion(update.version ?? null);
if (update) {
store().applyStatusUpdate(update);
}
})
.catch((error: unknown) => {
Expand All @@ -58,11 +58,8 @@ void client
client.onStatus({
onData: (status) => {
const update = deriveUpdateUiStatus(status, store().status);
if (update?.status) {
store().setStatus(update.status);
}
if (update && "version" in update) {
store().setVersion(update.version ?? null);
if (update) {
store().applyStatusUpdate(update);
}

const outcome = resolveMenuCheckFromStatus(
Expand Down Expand Up @@ -119,3 +116,48 @@ client.onCheckFromMenu({
log.error("Update menu check subscription error", { error });
},
});

// Bridge the "download updates automatically" preference to the core updater.
let lastSyncedAutoDownload: boolean | null = null;
function syncAutoDownload(enabled: boolean): void {
if (enabled === lastSyncedAutoDownload) return;
lastSyncedAutoDownload = enabled;
void hostTrpcClient.updates.setAutoDownload
.mutate({ enabled })
.catch((error: unknown) =>
log.error("Failed to sync auto-download preference", { error }),
);
}

// Auto-show "What's New" once on the first launch after the version changes.
function maybeShowWhatsNew(): void {
void hostTrpcClient.os.getAppVersion
.query()
.then((currentVersion) => {
const settings = useSettingsStore.getState();
const lastSeen = settings.lastSeenChangelogVersion;
if (lastSeen && lastSeen !== currentVersion) {
useWhatsNewStore.getState().open();
}
if (lastSeen !== currentVersion) {
settings.setLastSeenChangelogVersion(currentVersion);
}
})
.catch((error: unknown) =>
log.error("Failed to evaluate What's New", { error }),
);
}

function onSettingsReady(): void {
syncAutoDownload(useSettingsStore.getState().downloadUpdatesAutomatically);
useSettingsStore.subscribe((state) =>
syncAutoDownload(state.downloadUpdatesAutomatically),
);
maybeShowWhatsNew();
}

if (useSettingsStore.persist.hasHydrated()) {
onSettingsReady();
} else {
useSettingsStore.persist.onFinishHydration(onSettingsReady);
}
6 changes: 6 additions & 0 deletions packages/core/src/updates/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ export const checkForUpdatesOutput = z.object({
export const updatesStatusOutput = z.object({
checking: z.boolean(),
downloading: z.boolean().optional(),
available: z.boolean().optional(),
upToDate: z.boolean().optional(),
updateReady: z.boolean().optional(),
installing: z.boolean().optional(),
version: z.string().optional(),
availableVersion: z.string().optional(),
releaseNotes: z.string().nullable().optional(),
releaseDate: z.string().optional(),
downloadPercent: z.number().optional(),
bytesPerSecond: z.number().optional(),
error: z.string().optional(),
});

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/updates/updateStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ describe("deriveUpdateUiStatus", () => {
it("maps checking + downloading to downloading", () => {
expect(
deriveUpdateUiStatus({ checking: true, downloading: true }, "idle"),
).toEqual({ status: "downloading" });
).toEqual({
status: "downloading",
availableVersion: null,
releaseNotes: null,
releaseDate: null,
downloadPercent: null,
bytesPerSecond: null,
});
});

it("maps checking to checking", () => {
Expand Down
60 changes: 59 additions & 1 deletion packages/core/src/updates/updateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStore } from "zustand/vanilla";

export type UpdateUiStatus =
| "idle"
| "available"
| "checking"
| "downloading"
| "ready"
Expand All @@ -11,6 +12,11 @@ export type UpdateUiStatus =
interface UpdateState {
status: UpdateUiStatus;
version: string | null;
availableVersion: string | null;
releaseNotes: string | null;
releaseDate: string | null;
downloadPercent: number | null;
bytesPerSecond: number | null;
isEnabled: boolean;
menuCheckPending: boolean;

Expand All @@ -19,11 +25,17 @@ interface UpdateState {
setEnabled: (isEnabled: boolean) => void;
setMenuCheckPending: (menuCheckPending: boolean) => void;
setReady: (version: string | null) => void;
applyStatusUpdate: (update: UpdateStatusUpdate) => void;
}

export const updateStore = createStore<UpdateState>((set) => ({
status: "idle",
version: null,
availableVersion: null,
releaseNotes: null,
releaseDate: null,
downloadPercent: null,
bytesPerSecond: null,
isEnabled: false,
menuCheckPending: false,

Expand All @@ -32,6 +44,31 @@ export const updateStore = createStore<UpdateState>((set) => ({
setEnabled: (isEnabled) => set({ isEnabled }),
setMenuCheckPending: (menuCheckPending) => set({ menuCheckPending }),
setReady: (version) => set({ status: "ready", version }),
applyStatusUpdate: (update) =>
set((state) => ({
status: update.status ?? state.status,
version: update.version !== undefined ? update.version : state.version,
availableVersion:
update.availableVersion !== undefined
? update.availableVersion
: state.availableVersion,
releaseNotes:
update.releaseNotes !== undefined
? update.releaseNotes
: state.releaseNotes,
releaseDate:
update.releaseDate !== undefined
? update.releaseDate
: state.releaseDate,
downloadPercent:
update.downloadPercent !== undefined
? update.downloadPercent
: state.downloadPercent,
bytesPerSecond:
update.bytesPerSecond !== undefined
? update.bytesPerSecond
: state.bytesPerSecond,
})),
}));

export const getUpdateUiStatus = () => updateStore.getState().status;
Expand All @@ -42,6 +79,11 @@ export const getMenuCheckPending = () =>
export interface UpdateStatusUpdate {
status?: UpdateUiStatus;
version?: string | null;
availableVersion?: string | null;
releaseNotes?: string | null;
releaseDate?: string | null;
downloadPercent?: number | null;
bytesPerSecond?: number | null;
}

export function deriveUpdateUiStatus(
Expand All @@ -57,7 +99,23 @@ export function deriveUpdateUiStatus(
}

if (payload.checking && payload.downloading) {
return { status: "downloading" };
return {
status: "downloading",
availableVersion: payload.availableVersion ?? null,
releaseNotes: payload.releaseNotes ?? null,
releaseDate: payload.releaseDate ?? null,
downloadPercent: payload.downloadPercent ?? null,
bytesPerSecond: payload.bytesPerSecond ?? null,
};
}

if (payload.available) {
return {
status: "available",
availableVersion: payload.availableVersion ?? null,
releaseNotes: payload.releaseNotes ?? null,
releaseDate: payload.releaseDate ?? null,
};
}

if (payload.checking) {
Expand Down
Loading
Loading