Merge branch 'main' into merge/main-into-dev
# Conflicts: # packages/app/src/components/combined-model-selector.tsx # packages/app/src/components/message-input.tsx # packages/app/src/components/sidebar-workspace-list.tsx # packages/app/src/screens/agent/draft-agent-screen.tsx # packages/app/src/screens/settings-screen.tsx # packages/desktop/src/main.ts # packages/server/src/server/agent/provider-launch-config.ts # packages/server/src/server/session.ts # packages/server/src/server/session.workspaces.test.ts # packages/server/src/shared/messages.ts # packages/server/src/utils/checkout-git.ts
This commit is contained in:
@@ -7,6 +7,7 @@ import { autoUpdater, type UpdateInfo } from "electron-updater";
|
||||
|
||||
export type AppUpdateCheckResult = {
|
||||
hasUpdate: boolean;
|
||||
readyToInstall: boolean;
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
body: string | null;
|
||||
@@ -24,19 +25,84 @@ export type AppUpdateInstallResult = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let cachedUpdateInfo: UpdateInfo | null = null;
|
||||
let downloadedUpdateVersion: string | null = null;
|
||||
let downloading = false;
|
||||
let autoUpdaterConfigured = false;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function configureAutoUpdater(): void {
|
||||
// Don't auto-download — the user triggers install explicitly.
|
||||
autoUpdater.autoDownload = false;
|
||||
// Download updates in the background and only prompt once they are ready to install.
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
// Suppress built-in dialogs; the renderer handles UI.
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
|
||||
if (autoUpdaterConfigured) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdaterConfigured = true;
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
cachedUpdateInfo = info;
|
||||
downloadedUpdateVersion = null;
|
||||
downloading = true;
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
cachedUpdateInfo = info;
|
||||
downloadedUpdateVersion = info.version;
|
||||
downloading = false;
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
cachedUpdateInfo = null;
|
||||
downloadedUpdateVersion = null;
|
||||
downloading = false;
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (error) => {
|
||||
downloading = false;
|
||||
console.error("[auto-updater] Updater event failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function isReadyToInstallVersion(version: string): boolean {
|
||||
return downloadedUpdateVersion === version;
|
||||
}
|
||||
|
||||
function buildCheckResult(input: {
|
||||
currentVersion: string;
|
||||
hasUpdate: boolean;
|
||||
readyToInstall: boolean;
|
||||
info?: UpdateInfo | null;
|
||||
}): AppUpdateCheckResult {
|
||||
const { currentVersion, hasUpdate, readyToInstall, info } = input;
|
||||
|
||||
return {
|
||||
hasUpdate,
|
||||
readyToInstall,
|
||||
currentVersion,
|
||||
latestVersion: info?.version ?? currentVersion,
|
||||
body: typeof info?.releaseNotes === "string" ? info.releaseNotes : null,
|
||||
date: typeof info?.releaseDate === "string" ? info.releaseDate : null,
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleQuitAndInstall(onBeforeQuit?: () => Promise<void>): void {
|
||||
// Use a short delay to allow the renderer to receive the response.
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (onBeforeQuit) await onBeforeQuit();
|
||||
autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true);
|
||||
} catch (error) {
|
||||
console.error("[auto-updater] quitAndInstall failed:", error);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -45,28 +111,34 @@ function configureAutoUpdater(): void {
|
||||
|
||||
export async function checkForAppUpdate(currentVersion: string): Promise<AppUpdateCheckResult> {
|
||||
if (!app.isPackaged) {
|
||||
return {
|
||||
hasUpdate: false,
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
body: null,
|
||||
date: null,
|
||||
};
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
}
|
||||
|
||||
configureAutoUpdater();
|
||||
|
||||
const cachedVersion = cachedUpdateInfo?.version ?? null;
|
||||
if (cachedVersion && cachedVersion !== currentVersion) {
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
hasUpdate: true,
|
||||
readyToInstall: isReadyToInstallVersion(cachedVersion),
|
||||
info: cachedUpdateInfo,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
|
||||
if (!result || !result.updateInfo) {
|
||||
return {
|
||||
hasUpdate: false,
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
body: null,
|
||||
date: null,
|
||||
};
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
}
|
||||
|
||||
const info = result.updateInfo;
|
||||
@@ -75,24 +147,31 @@ export async function checkForAppUpdate(currentVersion: string): Promise<AppUpda
|
||||
|
||||
if (hasUpdate) {
|
||||
cachedUpdateInfo = info;
|
||||
downloading = !isReadyToInstallVersion(latestVersion);
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
hasUpdate: true,
|
||||
readyToInstall: isReadyToInstallVersion(latestVersion),
|
||||
info,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hasUpdate,
|
||||
cachedUpdateInfo = null;
|
||||
downloadedUpdateVersion = null;
|
||||
downloading = false;
|
||||
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
body: typeof info.releaseNotes === "string" ? info.releaseNotes : null,
|
||||
date: typeof info.releaseDate === "string" ? info.releaseDate : null,
|
||||
};
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[auto-updater] Failed to check for updates:", error);
|
||||
return {
|
||||
hasUpdate: false,
|
||||
return buildCheckResult({
|
||||
currentVersion,
|
||||
latestVersion: currentVersion,
|
||||
body: null,
|
||||
date: null,
|
||||
};
|
||||
hasUpdate: false,
|
||||
readyToInstall: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,14 +187,6 @@ export async function downloadAndInstallUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
return {
|
||||
installed: false,
|
||||
version: currentVersion,
|
||||
message: "Update already in progress.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!cachedUpdateInfo) {
|
||||
return {
|
||||
installed: false,
|
||||
@@ -126,24 +197,35 @@ export async function downloadAndInstallUpdate(
|
||||
|
||||
configureAutoUpdater();
|
||||
|
||||
const readyVersion = cachedUpdateInfo.version;
|
||||
if (isReadyToInstallVersion(readyVersion)) {
|
||||
scheduleQuitAndInstall(onBeforeQuit);
|
||||
return {
|
||||
installed: true,
|
||||
version: readyVersion,
|
||||
message: "Update downloaded. The app will restart shortly.",
|
||||
};
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
return {
|
||||
installed: false,
|
||||
version: currentVersion,
|
||||
message: "Update is still being prepared. Try again in a moment.",
|
||||
};
|
||||
}
|
||||
|
||||
downloading = true;
|
||||
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
// quitAndInstall restarts the app with the new version.
|
||||
// Use a short delay to allow the renderer to receive the response.
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (onBeforeQuit) await onBeforeQuit();
|
||||
autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true);
|
||||
} catch (error) {
|
||||
console.error("[auto-updater] quitAndInstall failed:", error);
|
||||
}
|
||||
}, 1500);
|
||||
downloadedUpdateVersion = readyVersion;
|
||||
downloading = false;
|
||||
scheduleQuitAndInstall(onBeforeQuit);
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
version: cachedUpdateInfo.version,
|
||||
version: readyVersion,
|
||||
message: "Update downloaded. The app will restart shortly.",
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ inheritLoginShellEnv();
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { existsSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { app, BrowserWindow, ipcMain, nativeImage, net, protocol } from "electron";
|
||||
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
||||
import {
|
||||
@@ -38,6 +39,38 @@ const APP_SCHEME = "paseo";
|
||||
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
||||
app.setName("Paseo");
|
||||
|
||||
// In dev mode, detect git worktrees and isolate each instance so multiple
|
||||
// Electron windows can run side-by-side (separate userData = separate lock).
|
||||
let devWorktreeName: string | null = null;
|
||||
if (!app.isPackaged) {
|
||||
try {
|
||||
const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 3000,
|
||||
}).trim();
|
||||
devWorktreeName = path.basename(topLevel);
|
||||
// Main checkout (e.g. "paseo") gets default userData — only worktrees diverge.
|
||||
const commonDir = path.resolve(
|
||||
topLevel,
|
||||
execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
||||
cwd: topLevel,
|
||||
encoding: "utf-8",
|
||||
timeout: 3000,
|
||||
}).trim(),
|
||||
);
|
||||
const isWorktree = path.resolve(topLevel, ".git") !== commonDir;
|
||||
if (isWorktree) {
|
||||
app.setPath("userData", path.join(app.getPath("appData"), `Paseo-${devWorktreeName}`));
|
||||
log.info("[worktree] isolated userData for worktree:", devWorktreeName);
|
||||
} else {
|
||||
devWorktreeName = null;
|
||||
}
|
||||
} catch {
|
||||
devWorktreeName = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Allow users to pass Chromium flags via PASEO_ELECTRON_FLAGS for debugging
|
||||
// rendering issues (e.g. "--disable-gpu --ozone-platform=x11").
|
||||
// Must run before app.whenReady().
|
||||
@@ -127,7 +160,9 @@ async function createMainWindow(): Promise<void> {
|
||||
const iconPath = getWindowIconPath();
|
||||
const systemTheme = resolveSystemWindowTheme();
|
||||
|
||||
const title = devWorktreeName ? `Paseo (${devWorktreeName})` : "Paseo";
|
||||
const mainWindow = new BrowserWindow({
|
||||
title,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
@@ -144,6 +179,10 @@ async function createMainWindow(): Promise<void> {
|
||||
},
|
||||
});
|
||||
|
||||
if (devWorktreeName) {
|
||||
app.dock?.setBadge(devWorktreeName);
|
||||
}
|
||||
|
||||
setupWindowResizeEvents(mainWindow);
|
||||
setupDefaultContextMenu(mainWindow);
|
||||
setupDragDropPrevention(mainWindow);
|
||||
|
||||
Reference in New Issue
Block a user